🧡 Initial commit with Sveltekit.

This commit is contained in:
pheralb 2023-03-14 21:08:22 +00:00
parent 494d2b5e01
commit 41d98985b4
221 changed files with 191 additions and 1526 deletions

13
.eslintignore Normal file
View 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
View 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
}
};

View File

@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals"]
}

56
.gitignore vendored
View File

@ -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-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

1
.nvmrc
View File

@ -1 +0,0 @@
v16.15.1

13
.prettierignore Normal file
View 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
View 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
View File

@ -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
View File

@ -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.

View File

@ -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",
},
};

View File

@ -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;

View File

@ -1,56 +1,33 @@
{
"name": "svgl",
"version": "2.0.1",
"description": "A beautiful library with SVG logos.",
"version": "0.0.1",
"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"
"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": {
"@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"
}
"@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"
}

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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
View 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
View 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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
View 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);
});
});

View File

@ -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;
}

View File

@ -1,8 +0,0 @@
export interface SvgData {
id: number;
slug: string;
title: string;
category: string;
categories?: string[];
url: string;
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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,
},
];

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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." });
}

View File

@ -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>
</>
);
}

View File

@ -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;

View File

@ -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
View 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>

View File

@ -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();
};

View File

@ -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=";

View File

@ -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;
}

View File

@ -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,
};

View File

@ -1,5 +0,0 @@
import Button from "./button";
export default {
Button,
};

View File

@ -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;

View File

@ -1,8 +0,0 @@
export const ToastTheme = {
icon: "🔔",
style: {
borderRadius: "10px",
background: "#1F2023",
color: "#fff",
},
};

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 187 KiB

View File

Before

Width:  |  Height:  |  Size: 633 B

After

Width:  |  Height:  |  Size: 633 B

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

View File

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 486 B

View File

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 678 B

View File

Before

Width:  |  Height:  |  Size: 1012 B

After

Width:  |  Height:  |  Size: 1012 B

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 532 B

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 441 B

After

Width:  |  Height:  |  Size: 441 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 617 B

After

Width:  |  Height:  |  Size: 617 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 494 B

View File

Before

Width:  |  Height:  |  Size: 1013 B

After

Width:  |  Height:  |  Size: 1013 B

View File

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 802 B

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 357 B

View File

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 336 B

Some files were not shown because too many files have changed in this diff Show More