🧡 Initial commit with Sveltekit.
13
.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
20
.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
|
||||
plugins: ['svelte3', '@typescript-eslint'],
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
settings: {
|
||||
'svelte3/typescript': () => require('typescript')
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"]
|
||||
}
|
56
.gitignore
vendored
@ -1,48 +1,10 @@
|
||||
# dependencies ->
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
package-lock.json
|
||||
|
||||
# testing ->
|
||||
/coverage
|
||||
|
||||
# next.js ->
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production ->
|
||||
/build
|
||||
|
||||
# misc ->
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug ->
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files ->
|
||||
.env*.local
|
||||
|
||||
# vercel ->
|
||||
.vercel
|
||||
|
||||
# typescript ->
|
||||
*.tsbuildinfo
|
||||
|
||||
# PWA files ->
|
||||
**/public/sw.js
|
||||
**/public/workbox-*.js
|
||||
**/public/worker-*.js
|
||||
**/public/sw.js.map
|
||||
**/public/workbox-*.js.map
|
||||
**/public/worker-*.js.map
|
||||
|
||||
# SWC files ->
|
||||
.swc
|
||||
|
||||
# PNPM files ->
|
||||
pnpm-lock.yaml
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
13
.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
110
README.md
@ -1,108 +1,38 @@
|
||||
<p align="center">
|
||||
<a href="https://svgl.vercel.app/" target="_blank">
|
||||
<img src="https://i.postimg.cc/1tzrP2rg/banner-corner.png" width="800px" alt="SVGL Banner" />
|
||||
</a>
|
||||
</p>
|
||||
# create-svelte
|
||||
|
||||
## 📦 Packages:
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
- ⚡️ [Nextjs](https://nextjs.org/) - The React Framework for Production.
|
||||
- ⚒️ [React 18](https://reactjs.org/) - A JavaScript library for building user interfaces.
|
||||
- 💙 [Typescript](https://www.typescriptlang.org/) - A superset of JavaScript.
|
||||
- ✅ [Vitest](https://vitest.dev/) - A blazing fast unit test framework.
|
||||
- 💅 [Chakra UI](https://chakra-ui.com/) - Create accessible React apps with speed.
|
||||
- 💥 [Framer Motion](https://www.framer.com/motion/) - Production-ready motion library.
|
||||
- 💖 [Phosphor Icons](https://phosphoricons.com/) - A flexible icon family for everyone.
|
||||
- ⬇️ [Next-PWA](https://github.com/shadowwalker/next-pwa) - Zero config PWA plugin for Next.js, with workbox.
|
||||
## Creating a project
|
||||
|
||||
## 🚀 Getting started:
|
||||
|
||||
You need:
|
||||
|
||||
- [Node.js 16+ (recommend: 16.15.1 LTS)](https://nodejs.org/en/)
|
||||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
|
||||
1. Clone the repository:
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
git@github.com:pheralb/svgl.git
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
## Developing
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. Run:
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
4. Test & Build:
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run ready
|
||||
# or
|
||||
yarn ready
|
||||
npm run build
|
||||
```
|
||||
|
||||
Open [localhost:3000](localhost:3000) with your browser to see the result.
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
## 🤔 Can I add my logo?
|
||||
|
||||
Yes! Here is a guide for you 🥳:
|
||||
|
||||
1. [Fork the repository](https://github.com/pheralb/svgl/fork).
|
||||
|
||||
2. Clone the forked repository:
|
||||
|
||||
```bash
|
||||
git@github.com:YOUR_USERNAME/svgl.git
|
||||
```
|
||||
|
||||
3. Add the **.svg** logo here: [`/public/library`](https://github.com/pheralb/svgl/tree/main/public/library).
|
||||
|
||||
4. Add your logo information here following the structure: [`/data/svgs.json`](https://github.com/pheralb/svgl/blob/main/data/svgs.json).
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"slug": "/library/your_logo.svg",
|
||||
"title": "Logo Title",
|
||||
"category": "Logo Category",
|
||||
"url": "Your Website / app url"
|
||||
}
|
||||
```
|
||||
|
||||
5. Create a commit and push:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "🥰 Added my logo"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
6. Create a pull request with your changes and 🥳 ready.
|
||||
|
||||
## 🚂 Api endpoints:
|
||||
|
||||
```bash
|
||||
- /api/all: returns all the logos.
|
||||
- /api/search?id=2: returns the logo with id 2.
|
||||
- /api/search?q=logo: returns the logo with query.
|
||||
```
|
||||
|
||||
## ⚒️ Shortcuts:
|
||||
|
||||
- ⭐ SVG Library: [/public/library/](https://github.com/pheralb/svgl/tree/main/public/library).
|
||||
- ✍️ SVG JSON logos: [/data/](https://github.com/pheralb/svgl/tree/main/data).
|
||||
|
||||
## 🔑 License:
|
||||
|
||||
- [MIT](https://github.com/pheralb/svgl/blob/main/LICENSE).
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
|
5
next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -1,57 +0,0 @@
|
||||
export default {
|
||||
title: "A beautiful library with SVG logos",
|
||||
titleTemplate: "%s - Svgl",
|
||||
description: "Svgl is a library of free and open source SVG logos.",
|
||||
defaultTitle: "svgl",
|
||||
additionalLinkTags: [
|
||||
{
|
||||
rel: "icon",
|
||||
href: "/icons/icon.ico",
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: "/icons/apple-touch-icon-180x180.png",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: "/icons/apple-touch-icon-152x152.png",
|
||||
sizes: "152x152",
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: "/icons/apple-touch-icon-114x114.png",
|
||||
sizes: "114x114",
|
||||
},
|
||||
{
|
||||
rel: "manifest",
|
||||
href: "/manifest.json",
|
||||
},
|
||||
{
|
||||
rel: "preload",
|
||||
href: "/fonts/Inter-Regular.woff2",
|
||||
as: "font",
|
||||
type: "font/woff2",
|
||||
crossOrigin: "anonymous",
|
||||
},
|
||||
],
|
||||
openGraph: {
|
||||
site_name: "Svgl",
|
||||
url: "https://svgl.vercel.app/",
|
||||
type: "website",
|
||||
locale: "en_IE",
|
||||
images: [
|
||||
{
|
||||
url: "/images/banner.png",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
type: "image/png",
|
||||
}
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
handle: "@pheralb_",
|
||||
site: "@pheralb_",
|
||||
cardType: "summary_large_image",
|
||||
},
|
||||
};
|
@ -1,14 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const withPWA = require("next-pwa");
|
||||
|
||||
const nextConfig = withPWA({
|
||||
reactStrictMode: true,
|
||||
pwa: {
|
||||
dest: "public",
|
||||
register: true,
|
||||
skipWaiting: true,
|
||||
disable: process.env.NODE_ENV === "development",
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = nextConfig;
|
85
package.json
@ -1,56 +1,33 @@
|
||||
{
|
||||
"name": "svgl",
|
||||
"version": "2.0.1",
|
||||
"description": "A beautiful library with SVG logos.",
|
||||
"private": true,
|
||||
"author": "@pheralb_",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"svgs",
|
||||
"logos",
|
||||
"images",
|
||||
"library"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"ready": "vitest && next build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "2.2.4",
|
||||
"@emotion/react": "11.10.0",
|
||||
"@emotion/styled": "11.10.0",
|
||||
"@uiball/loaders": "1.2.6",
|
||||
"canvas-confetti": "1.5.1",
|
||||
"downloadjs": "1.4.7",
|
||||
"framer-motion": "6.5.1",
|
||||
"next": "12.2.3",
|
||||
"next-pwa": "5.5.4",
|
||||
"next-seo": "5.5.0",
|
||||
"nextjs-progressbar": "0.0.14",
|
||||
"phosphor-react": "1.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "2.3.0",
|
||||
"react-hotkeys-hook": "3.4.7",
|
||||
"swr": "1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "13.3.0",
|
||||
"@types/canvas-confetti": "1.4.3",
|
||||
"@types/downloadjs": "1.4.3",
|
||||
"@types/node": "18.6.3",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@vitejs/plugin-react": "2.0.0",
|
||||
"eslint": "8.21.0",
|
||||
"eslint-config-next": "12.2.3",
|
||||
"jsdom": "20.0.0",
|
||||
"typescript": "4.7.4",
|
||||
"vitest": "0.20.2"
|
||||
}
|
||||
"name": "svgl",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test:unit": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^3.0.1",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.3"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
import { test, expect, describe } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import Categories from "@/layout/header/categories";
|
||||
|
||||
describe("Categories", () => {
|
||||
test("renders learn react link", () => {
|
||||
render(<Categories />);
|
||||
const showText = screen.getByText(/software/i);
|
||||
expect(showText).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type ShowProps = {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
const Show = ({ children, delay }: ShowProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
delay: delay,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Show;
|
@ -1,16 +0,0 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type TapAnimation = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Tap = ({ children } : TapAnimation) => {
|
||||
return (
|
||||
<motion.div whileTap={{ scale: 0.97 }}>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tap;
|
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
import { LayoutProps } from "@/interfaces/components";
|
||||
import { SimpleGrid } from "@chakra-ui/react";
|
||||
|
||||
const Grid = (props: LayoutProps) => {
|
||||
return (
|
||||
<SimpleGrid minChildWidth="160px" spacing="30px" >
|
||||
{props.children}
|
||||
</SimpleGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Grid;
|
@ -1,18 +0,0 @@
|
||||
import React from "react";
|
||||
import { CustomIconBtnProps } from "@/interfaces/components";
|
||||
import { IconButton } from "@chakra-ui/react";
|
||||
|
||||
const CustomIconBtn = (props: CustomIconBtnProps) => {
|
||||
return (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
aria-label={props.title}
|
||||
icon={props.icon}
|
||||
onClick={props.onClick}
|
||||
mr={props.mr}
|
||||
ml={props.ml}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomIconBtn;
|
@ -1,23 +0,0 @@
|
||||
import React from "react";
|
||||
import { Link } from "@chakra-ui/react";
|
||||
import NextLink from "next/link";
|
||||
import { CustomLinkProps } from "@/interfaces/components";
|
||||
|
||||
const CustomLink = ({ href, children, external, font, mr, ml }: CustomLinkProps) => {
|
||||
return (
|
||||
<NextLink href={href} passHref>
|
||||
<Link
|
||||
isExternal={external}
|
||||
_hover={{ textDecoration: "none" }}
|
||||
_focus={{ border: "none" }}
|
||||
fontFamily={font}
|
||||
mr={mr}
|
||||
ml={ml}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomLink;
|
@ -1,47 +0,0 @@
|
||||
import { ErrorProps } from "@/interfaces/components";
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
HStack,
|
||||
Text,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { ArrowClockwise, ArrowSquareOut, Warning } from "phosphor-react";
|
||||
import { useRouter } from "next/router";
|
||||
import CustomLink from "@/common/link";
|
||||
|
||||
const Error = (props: ErrorProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleRefresh = () => {
|
||||
router.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<VStack>
|
||||
<Warning size={90} />
|
||||
<Heading fontSize="3xl">{props.title}</Heading>
|
||||
<Text>{props.description}</Text>
|
||||
<HStack>
|
||||
<Button
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
leftIcon={<ArrowClockwise />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<CustomLink href="https://github.com/pheralb/svgl/issues/new" external={true}>
|
||||
<Button variant="ghost" rightIcon={<ArrowSquareOut />}>
|
||||
Create issue
|
||||
</Button>
|
||||
</CustomLink>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default Error;
|
@ -1,16 +0,0 @@
|
||||
import { LoadingProps } from "@/interfaces/components";
|
||||
import { Center, Spinner, Text, VStack } from "@chakra-ui/react";
|
||||
import { LeapFrog } from "@uiball/loaders";
|
||||
|
||||
const Loading = (props: LoadingProps) => {
|
||||
return (
|
||||
<Center>
|
||||
<VStack spacing={3} mt="3">
|
||||
<LeapFrog size={32} speed={2.5} color="#4343E5" />
|
||||
<Text>{props.text}</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
@ -1,125 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Input,
|
||||
Text,
|
||||
Image,
|
||||
HStack,
|
||||
Box,
|
||||
Center,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import useDebounce from "@/hooks/useDebounce";
|
||||
import { SearchProps, SVGCardProps } from "@/interfaces/components";
|
||||
import CustomLink from "@/common/link";
|
||||
import { getSvgByQuery } from "@/services";
|
||||
import CustomIconBtn from "@/common/iconBtn";
|
||||
import { Trash } from "phosphor-react";
|
||||
import Tap from "@/animations/tap";
|
||||
|
||||
const Search = ({ availableFocus = false }: SearchProps) => {
|
||||
const [search, setSearch] = useState("");
|
||||
const [empty, setEmpty] = useState(false);
|
||||
const [results, setResults] = useState<SVGCardProps[]>([]);
|
||||
const debouncedSearch = useDebounce(search, 500);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch) {
|
||||
fetch(getSvgByQuery + debouncedSearch).then((res) => {
|
||||
if (res.ok) {
|
||||
res.json().then((data) => {
|
||||
setEmpty(data.length === 0);
|
||||
setResults(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [debouncedSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const isFocusAvailable = availableFocus && searchRef.current;
|
||||
|
||||
if (!isFocusAvailable) return;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
searchRef.current?.focus();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [availableFocus]);
|
||||
|
||||
const handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmpty(false);
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSearch("");
|
||||
setResults([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
width="full"
|
||||
variant="flushed"
|
||||
size="lg"
|
||||
placeholder="Search svgs..."
|
||||
value={search}
|
||||
onChange={handleFilter}
|
||||
ref={searchRef}
|
||||
/>
|
||||
{search && !empty && results.length === 0 && (
|
||||
<Box pt="4">
|
||||
<Spinner />
|
||||
</Box>
|
||||
)}
|
||||
{search && empty && <Box pt="3">No results found!</Box>}
|
||||
{results && results.length > 0 && (
|
||||
<>
|
||||
<HStack
|
||||
spacing={4}
|
||||
mt={4}
|
||||
overflowX="auto"
|
||||
overflowY="hidden"
|
||||
alignItems="start"
|
||||
>
|
||||
{results.map((item: SVGCardProps) => (
|
||||
<Tap key={item.title}>
|
||||
<CustomLink href={`/svg/${item.id}`}>
|
||||
<Box
|
||||
mb="2"
|
||||
p="3"
|
||||
shadow="sm"
|
||||
borderWidth="1px"
|
||||
borderRadius="5px"
|
||||
width="100%"
|
||||
>
|
||||
<Center>
|
||||
<Image
|
||||
width="25px"
|
||||
mb="2"
|
||||
src={item.slug}
|
||||
alt={item.title}
|
||||
/>
|
||||
</Center>
|
||||
<Text>{item.title}</Text>
|
||||
</Box>
|
||||
</CustomLink>
|
||||
</Tap>
|
||||
))}
|
||||
</HStack>
|
||||
<Box p="3">
|
||||
<CustomIconBtn
|
||||
title="clear"
|
||||
icon={<Trash size={16} />}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
@ -1,47 +0,0 @@
|
||||
import React from "react";
|
||||
import { SVGCardProps } from "@/interfaces/components";
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Image,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import Tap from "@/animations/tap";
|
||||
import CustomLink from "@/common/link";
|
||||
import { Smiley } from "phosphor-react";
|
||||
|
||||
const SVGCard = (props: SVGCardProps) => {
|
||||
const bg = useColorModeValue("bg.light", "bg.dark");
|
||||
const color = useColorModeValue("rgb(0,0,0, .1)", "rgb(255,255,255, .1)");
|
||||
return (
|
||||
<>
|
||||
<Tap>
|
||||
<CustomLink href={`/svg/${props.id}`}>
|
||||
<Box
|
||||
bg={bg}
|
||||
p={4}
|
||||
cursor="pointer"
|
||||
borderRadius="10px"
|
||||
borderWidth="1px"
|
||||
mb="2"
|
||||
_hover={{
|
||||
border:`1px solid ${color}`,
|
||||
transform: "scale(1.03)",
|
||||
}}
|
||||
transition="all 0.2s" >
|
||||
<Center>
|
||||
<Image height="40px" src={props.svg} alt={props.title} />
|
||||
</Center>
|
||||
<Text mt="3" fontWeight="light" textAlign="center">
|
||||
{props.title}
|
||||
</Text>
|
||||
</Box>
|
||||
</CustomLink>
|
||||
</Tap>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SVGCard;
|
@ -1,108 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Image,
|
||||
Link,
|
||||
} from "@chakra-ui/react";
|
||||
import { ArrowSquareOut, Copy, DownloadSimple } from "phosphor-react";
|
||||
import confetti from "canvas-confetti";
|
||||
import download from "downloadjs";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { ToastTheme } from "@/theme/toast";
|
||||
import { SVGCardProps } from "@/interfaces/components";
|
||||
|
||||
// Download SVG =>
|
||||
const downloadSvg = (url?: string) => {
|
||||
confetti({
|
||||
particleCount: 200,
|
||||
startVelocity: 30,
|
||||
spread: 300,
|
||||
gravity: 1.2,
|
||||
origin: { y: 0 },
|
||||
});
|
||||
download(url || "");
|
||||
};
|
||||
|
||||
|
||||
const MIMETYPE = 'text/plain';
|
||||
|
||||
// Return content of svg as blob =>
|
||||
const getSvgContent = async (url: string | undefined, isSupported: boolean) => {
|
||||
const response = await fetch(url || "");
|
||||
const content = await response.text();
|
||||
|
||||
// It was necessary to use blob because in chrome there were issues with the copy to clipboard
|
||||
const blob = new Blob([content], { type: MIMETYPE });
|
||||
return isSupported ? blob : content;
|
||||
}
|
||||
|
||||
// Copy to clipboard =>
|
||||
const copyToClipboard = async (url?: string) => {
|
||||
const data = {
|
||||
[MIMETYPE]: getSvgContent(url, true)
|
||||
};
|
||||
try {
|
||||
const clipboardItem = new ClipboardItem(data);
|
||||
await navigator.clipboard.write([clipboardItem]);
|
||||
} catch (error) {
|
||||
// This section works as a fallback on Firefox
|
||||
const content = await getSvgContent(url, false) as string;
|
||||
await navigator.clipboard.writeText(content);
|
||||
}
|
||||
toast("Copied to clipboard", ToastTheme);
|
||||
};
|
||||
|
||||
const SVGInfo = (props: SVGCardProps) => {
|
||||
return (
|
||||
<Flex
|
||||
pt="7"
|
||||
pb="7"
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
borderWidth="1px"
|
||||
borderRadius="10px"
|
||||
>
|
||||
<Image
|
||||
src={props.slug}
|
||||
alt={props.title}
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
width="85px"
|
||||
/>
|
||||
<Heading mt={6} mb={6} fontSize="4xl">
|
||||
{props.title}
|
||||
</Heading>
|
||||
<Flex direction={{ base: "column", md: "row" }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
borderWidth="1px"
|
||||
leftIcon={<Copy />}
|
||||
onClick={() => copyToClipboard(props.slug)}
|
||||
mb={{ base: "2", md: "0" }}
|
||||
mr={{ base: "0", md: "3" }}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<DownloadSimple />}
|
||||
variant="primary"
|
||||
onClick={() => downloadSvg(props.slug)}
|
||||
mb={{ base: "2", md: "0" }}
|
||||
mr={{ base: "0", md: "3" }}
|
||||
>
|
||||
Download .svg
|
||||
</Button>
|
||||
<Link href={props.url} isExternal={true}>
|
||||
{props.title} website <Icon as={ArrowSquareOut} mt="2" />
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SVGInfo;
|
@ -1,17 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
7
src/index.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
@ -1,48 +0,0 @@
|
||||
export interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface CustomLinkProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
external?: boolean;
|
||||
font?: string;
|
||||
mr?: string;
|
||||
ml?: string;
|
||||
}
|
||||
|
||||
export interface CustomIconBtnProps {
|
||||
title: string;
|
||||
icon: React.ReactElement;
|
||||
mr?: string;
|
||||
ml?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface SVGCardProps {
|
||||
id: number;
|
||||
svg: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface SidebarContentProps {
|
||||
display?: object;
|
||||
w?: string;
|
||||
borderRight?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface LoadingProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ErrorProps {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SearchProps {
|
||||
availableFocus?: boolean;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
export interface SvgData {
|
||||
id: number;
|
||||
slug: string;
|
||||
title: string;
|
||||
category: string;
|
||||
categories?: string[];
|
||||
url: string;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import CustomLink from "@/common/link";
|
||||
import { Flex, Heading, HStack, Icon, Text } from "@chakra-ui/react";
|
||||
import { RocketLaunch, TwitterLogo } from "phosphor-react";
|
||||
import React from "react";
|
||||
|
||||
type Props = {};
|
||||
|
||||
const Index = (props: Props) => {
|
||||
return (
|
||||
<Flex direction="column" pt="8" pb="8" justifyContent="center" alignItems="center">
|
||||
<HStack>
|
||||
<Icon as={RocketLaunch} />
|
||||
<CustomLink href="https://twitter.com/pheralb_" external={true}>
|
||||
Created by Pablo
|
||||
</CustomLink>
|
||||
</HStack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
@ -1,48 +0,0 @@
|
||||
import React from "react";
|
||||
import useSWR from "swr";
|
||||
import { getCategorySvgs } from "@/services";
|
||||
import CustomLink from "@/common/link";
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { RaceBy } from "@uiball/loaders";
|
||||
import Tap from "@/animations/tap";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const Categories = () => {
|
||||
const { data, error } = useSWR(getCategorySvgs);
|
||||
const color = useColorModeValue("rgb(0,0,0, .1)", "rgb(255,255,255, .1)");
|
||||
const router = useRouter();
|
||||
if (error) return <div>failed to load</div>;
|
||||
if (!data)
|
||||
return <RaceBy size={52} lineWeight={3} speed={1.4} color="#4343E5" />;
|
||||
return (
|
||||
<>
|
||||
{data.map((category: string) => (
|
||||
<Tap key={category}>
|
||||
<CustomLink
|
||||
href={`/category/${category}`}>
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="4px"
|
||||
borderWidth="1px"
|
||||
__css={
|
||||
router.asPath === `/category/${category}`
|
||||
? {
|
||||
backgroundColor: '#4343e5',
|
||||
color: '#fff'
|
||||
}
|
||||
: {}
|
||||
}
|
||||
_hover={{
|
||||
border:`1px solid ${color}`,
|
||||
transform: "scale(0.98)",
|
||||
}}>
|
||||
{category}
|
||||
</Box>
|
||||
</CustomLink>
|
||||
</Tap>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Categories;
|
@ -1,120 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
useColorModeValue,
|
||||
HStack,
|
||||
Container,
|
||||
Heading,
|
||||
Icon,
|
||||
useDisclosure,
|
||||
Collapse,
|
||||
} from '@chakra-ui/react'
|
||||
import { ArrowSquareOut, MagnifyingGlass, Sticker, X } from 'phosphor-react'
|
||||
import Theme from './theme'
|
||||
import Tap from '@/animations/tap'
|
||||
import Mobile from './mobile'
|
||||
import { Links } from './links'
|
||||
import CustomLink from '@/common/link'
|
||||
import Categories from './categories'
|
||||
import Search from '@/components/search'
|
||||
import CustomIconBtn from '@/common/iconBtn'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
const Header = () => {
|
||||
const bg = useColorModeValue('bg.light', 'bg.dark')
|
||||
const { isOpen, onToggle } = useDisclosure()
|
||||
useHotkeys('ctrl+k', (e) => {
|
||||
e.preventDefault()
|
||||
onToggle()
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
as='header'
|
||||
position='sticky'
|
||||
top='0'
|
||||
bg={bg}
|
||||
borderBottomWidth='1px'
|
||||
w='full'
|
||||
py={6}
|
||||
zIndex={1}
|
||||
shadow='sm'
|
||||
>
|
||||
<Container maxW={{ base: 'full', md: '70%' }}>
|
||||
<Flex alignItems='center' justifyContent='space-between' mx='auto'>
|
||||
<CustomLink href='/'>
|
||||
<Tap>
|
||||
<HStack spacing={3} cursor='pointer'>
|
||||
<Sticker size={32} color='#4343e5' weight='bold' />
|
||||
<Heading fontSize='19px'>svgl</Heading>
|
||||
</HStack>
|
||||
</Tap>
|
||||
</CustomLink>
|
||||
<HStack display='flex' alignItems='center' spacing={2}>
|
||||
<Box display={{ base: 'none', md: 'inline-flex' }}>
|
||||
{Links.map((link) => (
|
||||
<CustomLink
|
||||
key={link.title}
|
||||
href={link.slug}
|
||||
external={link.external}
|
||||
font='Inter-Semibold'
|
||||
mr='4'
|
||||
ml='3'
|
||||
>
|
||||
<Tap>
|
||||
{link.title}
|
||||
{link.external ? (
|
||||
<Icon as={ArrowSquareOut} ml='2' />
|
||||
) : null}
|
||||
</Tap>
|
||||
</CustomLink>
|
||||
))}
|
||||
</Box>
|
||||
<HStack
|
||||
spacing={1}
|
||||
mr={1}
|
||||
display={{ base: 'none', md: 'inline-flex' }}
|
||||
>
|
||||
<CustomIconBtn
|
||||
title='Toggle Search bar'
|
||||
icon={
|
||||
isOpen ? <X size={22} /> : <MagnifyingGlass size={22} />
|
||||
}
|
||||
onClick={onToggle}
|
||||
/>
|
||||
<Theme />
|
||||
</HStack>
|
||||
<Box display={{ base: 'inline-flex', md: 'none' }}>
|
||||
<Mobile />
|
||||
</Box>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Collapse in={isOpen} animateOpacity>
|
||||
<Box mt='3' display={{ base: 'none', md: 'block' }}>
|
||||
<Search availableFocus={isOpen} />
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Box mt='2' display={{ base: 'block', md: 'none' }}>
|
||||
<Search />
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
<Box p='4' overflowX='hidden' overflowY='auto'>
|
||||
<HStack
|
||||
justifyContent={{ base: 'none', lg: 'center' }}
|
||||
flexWrap={{ base: 'initial', lg: 'wrap' }}
|
||||
spacing={4}
|
||||
overflowX='auto'
|
||||
overflowY='hidden'
|
||||
bg={bg}
|
||||
pb='4'
|
||||
borderBottomWidth='1px'
|
||||
>
|
||||
<Categories />
|
||||
</HStack>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
@ -1,12 +0,0 @@
|
||||
export const Links = [
|
||||
{
|
||||
title: "Github",
|
||||
slug: "https://github.com/pheralb/svgl",
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: "Twitter",
|
||||
slug: "https://twitter.com/pheralb_",
|
||||
external: true,
|
||||
},
|
||||
];
|
@ -1,59 +0,0 @@
|
||||
import CustomLink from "@/common/link";
|
||||
import {
|
||||
Button,
|
||||
CloseButton,
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { List } from "phosphor-react";
|
||||
import { Links } from "./links";
|
||||
import Theme from "./theme";
|
||||
|
||||
const Mobile = () => {
|
||||
const bg = useColorModeValue("bg.light", "bg.dark");
|
||||
const mobileNav = useDisclosure();
|
||||
return (
|
||||
<>
|
||||
<Theme />
|
||||
<IconButton
|
||||
display={{ base: "flex", md: "none" }}
|
||||
aria-label="Open menu navbar"
|
||||
variant="ghost"
|
||||
icon={<List size={22} />}
|
||||
onClick={mobileNav.onOpen}
|
||||
ml="2"
|
||||
/>
|
||||
<VStack
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
display={mobileNav.isOpen ? "flex" : "none"}
|
||||
flexDirection="column"
|
||||
p={4}
|
||||
pb={4}
|
||||
bg={bg}
|
||||
spacing={5}
|
||||
rounded="sm"
|
||||
shadow="sm"
|
||||
borderWidth="1px"
|
||||
zIndex={2}
|
||||
>
|
||||
<CloseButton aria-label="Close menu" onClick={mobileNav.onClose} />
|
||||
{Links.map((link) => (
|
||||
<CustomLink
|
||||
key={link.title}
|
||||
href={link.slug}
|
||||
external={link.external}
|
||||
>
|
||||
{link.title}
|
||||
</CustomLink>
|
||||
))}
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mobile;
|
@ -1,31 +0,0 @@
|
||||
import React from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useColorMode, useColorModeValue } from "@chakra-ui/react";
|
||||
import CustomIconBtn from "@/common/iconBtn";
|
||||
import { Moon, Sun } from "phosphor-react";
|
||||
|
||||
const Theme = () => {
|
||||
const { toggleColorMode } = useColorMode();
|
||||
const key = useColorModeValue("light", "dark");
|
||||
const icon = useColorModeValue(<Moon size={22} />, <Sun size={22} />);
|
||||
|
||||
return (
|
||||
<AnimatePresence exitBeforeEnter initial={false}>
|
||||
<motion.div
|
||||
key={key}
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 20, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<CustomIconBtn
|
||||
title="Toggle theme"
|
||||
icon={icon}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default Theme;
|
@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
import { LayoutProps } from "@/interfaces/components";
|
||||
import { Container } from "@chakra-ui/react";
|
||||
import Header from "./header";
|
||||
import Footer from "./footer";
|
||||
|
||||
const Index = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Container maxW={{ base: "100%", md: "70%" }} mt={{ base: "1", md: "3" }}>{children}</Container>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
@ -1,21 +0,0 @@
|
||||
import CustomLink from "@/common/link";
|
||||
import { Flex, Center, Heading, Text, Button } from "@chakra-ui/react";
|
||||
import { House } from "phosphor-react";
|
||||
|
||||
const Error = () => {
|
||||
return (
|
||||
<>
|
||||
<Center>
|
||||
<Flex direction="column" justifyContent="center" alignItems="center">
|
||||
<Heading mb="2">Error 404</Heading>
|
||||
<Text mb="3">The page you are trying to access does not exist.</Text>
|
||||
<CustomLink href="/">
|
||||
<Text fontFamily="Inter-Semibold">Go home</Text>
|
||||
</CustomLink>
|
||||
</Flex>
|
||||
</Center>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Error;
|
@ -1,64 +0,0 @@
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
// Chakra UI & custom styles ->
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import theme from "@/theme";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
// Layout ->
|
||||
import Layout from "@/layout";
|
||||
|
||||
// Nextjs Progressbar ->
|
||||
import NextNProgress from "nextjs-progressbar";
|
||||
|
||||
// Framer ->
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
// SWR Config & services ->
|
||||
import { SWRConfig } from "swr";
|
||||
import { fetcher } from "@/services/fetcher";
|
||||
|
||||
// React Hot Toast ->
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import nextSeoConfig from "next-seo.config";
|
||||
|
||||
function MyApp({ Component, pageProps, router }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<DefaultSeo {...nextSeoConfig} />
|
||||
<NextNProgress
|
||||
color="#4343E5"
|
||||
startPosition={0.3}
|
||||
stopDelayMs={200}
|
||||
height={2}
|
||||
showOnShallow={true}
|
||||
options={{ showSpinner: false }}
|
||||
/>
|
||||
<ChakraProvider theme={theme}>
|
||||
<SWRConfig value={{ fetcher }}>
|
||||
<Layout>
|
||||
<motion.div
|
||||
key={router.route}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
variants={{
|
||||
initial: {
|
||||
opacity: 0,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</motion.div>
|
||||
</Layout>
|
||||
</SWRConfig>
|
||||
</ChakraProvider>
|
||||
<Toaster position="bottom-center" reverseOrder={false} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
@ -1,18 +0,0 @@
|
||||
import { ColorModeScript } from "@chakra-ui/react";
|
||||
import NextDocument, { Html, Head, Main, NextScript } from "next/document";
|
||||
import theme from "@/theme";
|
||||
|
||||
export default class Document extends NextDocument {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import db from "data/svgs.json";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { SvgData } from "@/interfaces/svgData";
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<SvgData[]>
|
||||
) {
|
||||
// Begin with the last id in the db:
|
||||
const svgs = db.sort((a, b) => b.id - a.id);
|
||||
return res.status(200).json(svgs);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import db from "data/svgs.json";
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get unique categories:
|
||||
const categories = db
|
||||
.map((svg) => svg.category)
|
||||
.filter((category, index, array) => array.indexOf(category) === index);
|
||||
res.status(200).json(categories);
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import db from "data/svgs.json";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id, q, c } = req.query;
|
||||
|
||||
// 🔎 Search by id (ex: ?id=1) ->
|
||||
if (id) {
|
||||
const item = db.find((item) => item.id === +id);
|
||||
return res.status(200).json(item);
|
||||
}
|
||||
|
||||
// 🔎 Search by query (ex: ?q=d) ->
|
||||
if (q) {
|
||||
const results = db.filter((product) => {
|
||||
const { title } = product;
|
||||
return title.toLowerCase().includes(q.toString().toLowerCase());
|
||||
});
|
||||
return res.status(200).json(results);
|
||||
}
|
||||
|
||||
// 🔎 Search by category (ex: ?c=library) ->
|
||||
if (c) {
|
||||
const results = db.filter((product) => {
|
||||
const { category } = product;
|
||||
return category.toLowerCase().includes(c.toString().toLowerCase());
|
||||
});
|
||||
return res.status(200).json(results);
|
||||
}
|
||||
|
||||
// ✖ Error ->
|
||||
res.status(400).json({ info: "[/api/search] Error: api query not found." });
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { getSvgByCategory } from "@/services";
|
||||
import Loading from "@/components/loading";
|
||||
import Grid from "@/common/grid";
|
||||
import SVGCard from "@/components/svgCard";
|
||||
import { SvgData } from "@/interfaces/svgData";
|
||||
import { Center, Heading } from "@chakra-ui/react";
|
||||
import Show from "@/animations/show";
|
||||
|
||||
export default function Category() {
|
||||
const router = useRouter();
|
||||
const { data, error } = useSWR(
|
||||
() => router.query.category && `${getSvgByCategory}${router.query.category}`
|
||||
);
|
||||
|
||||
if (error) router.push("/404");
|
||||
if (!data) return <Loading text="Loading..." />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{router.query.category} logos - svgl</title>
|
||||
</Head>
|
||||
<Show>
|
||||
<Center>
|
||||
<Heading mb="5">{router.query.category}</Heading>
|
||||
</Center>
|
||||
</Show>
|
||||
|
||||
<Grid>
|
||||
{data.map((svg: SvgData) => (
|
||||
<SVGCard key={svg.id} id={svg.id} svg={svg.slug} title={svg.title} />
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import type { NextPage } from "next";
|
||||
import useSWR from "swr";
|
||||
import { getAllSvgs } from "@/services";
|
||||
import { SvgData } from "@/interfaces/svgData";
|
||||
import SVGCard from "@/components/svgCard";
|
||||
import Grid from "@/common/grid";
|
||||
import Loading from "@/components/loading";
|
||||
import Error from "@/components/error";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const { data, error } = useSWR(getAllSvgs);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<Error title="Error" description="An unexpected error has occurred" />
|
||||
);
|
||||
if (!data) return <Loading text="Loading..." />;
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
{data.map((svg: SvgData) => (
|
||||
<SVGCard key={svg.id} id={svg.id} svg={svg.slug} title={svg.title} />
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
@ -1,29 +0,0 @@
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
|
||||
import Show from "@/animations/show";
|
||||
import { getSvgById } from "@/services";
|
||||
import Loading from "@/components/loading";
|
||||
import SVGInfo from "@/components/svgInfo";
|
||||
|
||||
export default function Icon() {
|
||||
const router = useRouter();
|
||||
const { data, error } = useSWR(
|
||||
() => router.query.id && `${getSvgById}${router.query.id}`
|
||||
);
|
||||
|
||||
if (error) router.push("/404");
|
||||
if (!data) return <Loading text="Loading..." />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{data.title} - svgl</title>
|
||||
</Head>
|
||||
<Show>
|
||||
<SVGInfo {...data} />
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
2
src/routes/+page.svelte
Normal file
@ -0,0 +1,2 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
@ -1,10 +0,0 @@
|
||||
export const fetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = new Error("An error occurred while fetching the data.");
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.json();
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
export const githubVersionPackage = 'https://api.github.com/repos/pheralb/svgl/releases/latest';
|
||||
export const getAllSvgs = "/api/all";
|
||||
export const getCategorySvgs = "/api/categories";
|
||||
export const getSvgById = "/api/search?id=";
|
||||
export const getSvgByQuery = "/api/search?q=";
|
||||
export const getSvgByCategory = "/api/search?c=";
|
@ -1,16 +0,0 @@
|
||||
/* Fonts -> */
|
||||
@font-face {
|
||||
font-family: "Inter-Regular";
|
||||
src: url("/fonts/Inter-Regular.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter-Semibold";
|
||||
src: url("/fonts/Inter-SemiBold.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
const baseStyle = {
|
||||
borderRadius: "md",
|
||||
fontWeight: "light",
|
||||
};
|
||||
|
||||
function variantPrimary() {
|
||||
const disabled = {
|
||||
bg: "purple.900",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
const loading = {
|
||||
bg: "purple.800",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
return {
|
||||
bg: "brand.purple",
|
||||
color: "white",
|
||||
_hover: {
|
||||
bg: "purple.900",
|
||||
_disabled: {
|
||||
...disabled,
|
||||
_loading: loading,
|
||||
},
|
||||
},
|
||||
_active: {
|
||||
bg: "purple.700",
|
||||
},
|
||||
_disabled: {
|
||||
...disabled,
|
||||
_loading: loading,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const variants = {
|
||||
primary: variantPrimary,
|
||||
};
|
||||
|
||||
export default {
|
||||
baseStyle,
|
||||
variants,
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
import Button from "./button";
|
||||
|
||||
export default {
|
||||
Button,
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
import { ChakraProps, extendTheme } from "@chakra-ui/react";
|
||||
import { mode } from "@chakra-ui/theme-tools";
|
||||
import components from "./components";
|
||||
|
||||
const theme = extendTheme(
|
||||
{
|
||||
components,
|
||||
},
|
||||
{
|
||||
config: {
|
||||
initialColorMode: "light",
|
||||
useSystemColorMode: false,
|
||||
},
|
||||
colors: {
|
||||
bg: {
|
||||
light: "#F2F2F2",
|
||||
dark: "#1F2023",
|
||||
},
|
||||
full: {
|
||||
light: "#ffffff",
|
||||
dark: "#000000",
|
||||
},
|
||||
brand: {
|
||||
purple: "#4343E5",
|
||||
},
|
||||
},
|
||||
fonts: {
|
||||
body: "Inter-Regular, sans-serif",
|
||||
heading: "Inter-Semibold, sans-serif",
|
||||
},
|
||||
styles: {
|
||||
global: (props: ChakraProps) => ({
|
||||
"html, body": {
|
||||
height: "100%",
|
||||
maxHeight: "100vh",
|
||||
bg: mode("bg.light", "bg.dark")(props),
|
||||
fontSize: "14px",
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default theme;
|
@ -1,8 +0,0 @@
|
||||
export const ToastTheme = {
|
||||
icon: "🔔",
|
||||
style: {
|
||||
borderRadius: "10px",
|
||||
background: "#1F2023",
|
||||
color: "#fff",
|
||||
},
|
||||
};
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 633 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 569 B |
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
Before Width: | Height: | Size: 1012 B After Width: | Height: | Size: 1012 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 532 B |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 494 B |
Before Width: | Height: | Size: 1013 B After Width: | Height: | Size: 1013 B |
Before Width: | Height: | Size: 802 B After Width: | Height: | Size: 802 B |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 357 B |
Before Width: | Height: | Size: 336 B After Width: | Height: | Size: 336 B |