Merge branch 'develop'

This commit is contained in:
pheralb 2022-06-28 18:55:23 +01:00
commit ccd1a80189
106 changed files with 8055 additions and 6358 deletions

View File

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

34
.gitignore vendored
View File

@ -1,42 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies ->
# dependencies
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
package-lock.json
# testing # testing ->
/coverage /coverage
# next.js # next.js ->
/.next/ /.next/
/out/ /out/
# production # production ->
/build /build
# misc # misc ->
.DS_Store .DS_Store
*.pem *.pem
# debug # debug ->
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
.pnpm-debug.log*
# local env files # local env files ->
.env.local .env*.local
.env.development.local
.env.test.local
.env.production.local
# PWA files # vercel ->
.vercel
# typescript ->
*.tsbuildinfo
# PWA files ->
**/public/sw.js **/public/sw.js
**/public/workbox-*.js **/public/workbox-*.js
**/public/worker-*.js **/public/worker-*.js
**/public/sw.js.map **/public/sw.js.map
**/public/workbox-*.js.map **/public/workbox-*.js.map
**/public/worker-*.js.map **/public/worker-*.js.map
# vercel
.vercel

View File

@ -1,27 +1,108 @@
<p align="center"> <p align="center">
<a href="https://svgl.vercel.app/"> <a href="https://svgl.vercel.app/" target="_blank">
<img src="https://raw.githubusercontent.com/pheralb/svgl/main/public/images/banner.png" width="800px" alt="svgl preview" /> <img src="https://i.postimg.cc/1tzrP2rg/banner-corner.png" width="800px" alt="SVGL Banner" />
</a> </a>
</p> </p>
## 🚀 Getting started: ## 📦 Packages:
[SVGL](https://svgl.vercel.app/) is a beautiful collection of SVG logos. Free and open source. - ⚡️ [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.
## 🚀 Getting started:
You need: You need:
- [Node.js 16+ (recommend: 16.14.0 LTS)](https://nodejs.org/en/) - [Node.js 16+ (recommend: 16.15.1 LTS)](https://nodejs.org/en/)
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
and run: 1. Clone the repository:
```bash
git@github.com:pheralb/svgl.git
```
2. Install dependencies:
```bash ```bash
npm install npm install
npm run dev # or
yarn install
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 3. Run:
```bash
npm run dev
# or
yarn dev
```
4. Test & Build:
```bash
npm run ready
# or
yarn ready
```
Open [localhost:3000](localhost:3000) with your browser to see the result.
## 🤔 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/tree/main/public/library).
```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: ## 🔑 License:
MIT - [MIT](https://github.com/pheralb/svgl/blob/main/LICENSE).

View File

@ -1,82 +0,0 @@
import { motion } from "framer-motion";
import React from "react";
const LoadingDot = {
display: "block",
width: "1rem",
height: "1rem",
backgroundColor: "#6748E6",
borderRadius: "50%",
};
const LoadingContainer = {
width: "5rem",
height: "5rem",
display: "flex",
justifyContent: "space-around",
};
const ContainerVariants = {
initial: {
transition: {
staggerChildren: 0.2,
},
},
animate: {
transition: {
staggerChildren: 0.2,
},
},
};
const DotVariants = {
initial: {
y: "0%",
},
animate: {
y: "100%",
},
};
const DotTransition = {
duration: 0.5,
yoyo: Infinity,
ease: "easeInOut",
};
export default function Loader() {
return (
<div
style={{
paddingTop: "5rem",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<motion.div
style={LoadingContainer}
variants={ContainerVariants}
initial="initial"
animate="animate"
>
<motion.span
style={LoadingDot}
variants={DotVariants}
transition={DotTransition}
/>
<motion.span
style={LoadingDot}
variants={DotVariants}
transition={DotTransition}
/>
<motion.span
style={LoadingDot}
variants={DotVariants}
transition={DotTransition}
/>
</motion.div>
</div>
);
}

View File

@ -1,12 +0,0 @@
import React from "react";
import { motion } from "framer-motion";
const Tap = ({ children }) => {
return (
<motion.div whileHover={{ scale: 1.040 }} whileTap={{ scale: 0.98 }}>
{children}
</motion.div>
);
};
export default Tap;

View File

@ -1,23 +0,0 @@
import React from "react";
import { motion } from "framer-motion";
const Transitions = ({ children }) => {
return (
<motion.div
initial="initial"
animate="animate"
variants={{
initial: {
opacity: 0,
},
animate: {
opacity: 1,
},
}}
>
{children}
</motion.div>
);
};
export default Transitions;

View File

@ -1,26 +0,0 @@
import { Box, VStack, Icon, Text, Divider } from "@chakra-ui/react";
import React from "react";
const Design = ({ icon, title, children }) => {
return (
<Box
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
_hover={{ shadow: "md", transition: "all .2s" }}
>
<Box p="6">
<VStack spacing={3}>
<Icon as={icon} w={16} h={16} />
<Text fontSize="3xl" fontWeight="semibold">
{title}
</Text>
<Divider />
<Box fontSize="2xl">{children}</Box>
</VStack>
</Box>
</Box>
);
};
export default Design;

View File

@ -1,23 +0,0 @@
import { Box, Image } from "@chakra-ui/react";
import React from "react";
const CardImage = ({title, image, children}) => {
return (
<Box maxW="sm" borderWidth="1px" borderRadius="lg" overflow="hidden">
<Image src={image} alt={title} width="60" />
<Box p="6">
<Box
mt="1"
as="h4"
lineHeight="tight"
isTruncated
>
{title}
</Box>
{children}
</Box>
</Box>
);
};
export default CardImage;

View File

@ -1,87 +0,0 @@
import React from "react";
import Link from "next/link";
import {
Box,
Text,
Image,
Center,
HStack,
IconButton,
useColorModeValue,
} from "@chakra-ui/react";
import { IoCloudDownloadOutline } from "react-icons/io5";
import { FiExternalLink } from "react-icons/fi";
import download from "downloadjs";
import toast from "react-hot-toast";
import Tap from "animations/tap";
const Index = ({ title, url, href }) => {
const toastBg = useColorModeValue("#F2F2F2", "#1D1D1D");
const toastColor = useColorModeValue("black", "white");
const bgImage = useColorModeValue("transparent", "#E9E9E9");
const borderRds = useColorModeValue("0", "15px");
const downloadSvg = (url) => {
toast(`Downloading ${title}...`, {
icon: "🥳",
style: {
borderRadius: "10px",
background: toastBg,
color: toastColor,
},
});
download(url);
};
return (
<Box
p={4}
borderRadius="10px"
borderWidth="1px"
mb="2"
_hover={{
shadow: "md",
}}
transition="all 0.2s"
>
<Center>
<Image
src={href}
alt={title}
boxSize="45px"
bg={bgImage}
borderRadius={borderRds}
p="1"
/>
</Center>
<Text mt="2" fontWeight="light" textAlign="center">
{title}
</Text>
<Center>
<HStack spacing="1" mt="1">
<Tap>
<IconButton
as="button"
variant="ghost"
aria-label="Download SVG"
icon={<IoCloudDownloadOutline size="16" />}
onClick={() => downloadSvg(href)}
/>
</Tap>
<Tap>
<Link href={url} passHref>
<IconButton
as="a"
variant="ghost"
aria-label="Go to Vue SVG page"
icon={<FiExternalLink size="16" />}
/>
</Link>
</Tap>
</HStack>
</Center>
</Box>
);
};
export default Index;

View File

@ -1,38 +0,0 @@
import React from "react";
import { Box, Flex, Button, Container, Text, Icon } from "@chakra-ui/react";
import { IoHome, IoShapesOutline } from "react-icons/io5";
import Link from "next/link";
import Show from "animations/show";
const Error = () => {
return (
<>
<Show delay="0">
<Box px={{ base: 4, lg: 20 }} py={{ base: "3", md: "24" }}>
<Flex align="center" justify="center" direction="column" w="full">
<Icon name="error" boxSize="80px" mb="3" as={IoShapesOutline} />
<Text fontSize="40px" mb="2">
Oh no!
</Text>
<Text fontSize="20px" mb="3">
This page does not exist.
</Text>
<Link href="/" passHref>
<Button
leftIcon={<IoHome />}
borderWidth="1px"
variant="outline"
fontWeight="light"
mb="4"
>
Go home
</Button>
</Link>
</Flex>
</Box>
</Show>
</>
);
};
export default Error;

View File

@ -1,12 +0,0 @@
import React from 'react'
import { SimpleGrid } from '@chakra-ui/react'
const Index = ({children}) => {
return (
<SimpleGrid minChildWidth='200px' columns={3} spacing={5}>
{children}
</SimpleGrid>
)
}
export default Index

View File

@ -1,32 +0,0 @@
import React from "react";
import useSWR from "swr";
import Grid from "components/grid";
import Card from "components/card";
import Loader from "animations/loader";
const fetcher = (url) => fetch(url).then((res) => res.json());
const All = () => {
const { data, error } = useSWR("/api/all", fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <Loader />;
return (
<>
<Grid>
{data.map((link) => (
<>
<div key={link}>
<Card
title={link.title}
url={`/svg/${link.id}`}
href={link.href}
/>
</div>
</>
))}
</Grid>
</>
);
};
export default All;

View File

@ -1,32 +0,0 @@
import React from "react";
import useSWR from "swr";
import Grid from "components/grid";
import Library from "components/card/library";
import Loader from "animations/loader";
const fetcher = (url) => fetch(url).then((res) => res.json());
const Libraries = () => {
const { data, error } = useSWR("/api/icons", fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <Loader />;
return (
<>
<Grid>
{data.map((link) => (
<>
<div key={link}>
<Library
image={link.image}
title={link.title}
url={link.url}
/>
</div>
</>
))}
</Grid>
</>
);
};
export default Libraries;

View File

@ -1,12 +0,0 @@
import React from "react";
import { Box } from "@chakra-ui/react";
const Index = ({ children }) => {
return (
<Box as="main" px={{ base: 6, md: 16 }} pl={{ base: 6, md: 16 }}>
{children}
</Box>
);
};
export default Index;

View File

@ -1,133 +0,0 @@
import { useMemo, useRef, useState } from "react";
import { createAutocomplete } from "@algolia/autocomplete-core";
import {
Box,
Input,
InputLeftElement,
InputGroup,
Flex,
HStack,
Text,
Image,
Icon,
Link,
} from "@chakra-ui/react";
import { IoSearch } from "react-icons/io5";
import { FiExternalLink } from "react-icons/fi";
import { Algolia } from "components/svg";
import NextLink from "next/link";
const AutocompleteItem = ({ id, title, href, url }) => {
return (
<>
<NextLink href={`/svg/${id}`} passHref>
<Link
href={`/svg/${id}`}
style={{ textDecoration: "none" }}
_focus={{ outline: "0" }}
>
<Box
id={id}
w="100%"
borderWidth="1px"
borderRadius="6px"
mt="3"
cursor="pointer"
_hover={{ shadow: "md" }}
transition="all 0.2s"
>
<HStack py={6} px={6} spacing={2}>
<Image src={href} alt={title} boxSize="20px" mr="2" />
<Text fontSize="18px" fontWeight="light">
{title}
</Text>
<Icon as={FiExternalLink} />
</HStack>
</Box>
</Link>
</NextLink>
</>
);
};
export default function Search(props) {
const [autocompleteState, setAutocompleteState] = useState({
collections: [],
});
const autocomplete = useMemo(
() =>
createAutocomplete({
placeholder: "Search svgs...",
onStateChange: ({ state }) => setAutocompleteState(state),
getSources: () => [
{
sourceId: "svgs-next-api",
getItems: ({ query }) => {
if (!!query) {
return fetch(`/api/search?q=${query}`).then((res) =>
res.json()
);
}
},
},
],
...props,
}),
[props]
);
const formRef = useRef(null);
const inputRef = useRef(null);
const formProps = autocomplete.getFormProps({
inputElement: inputRef.current,
});
const inputProps = autocomplete.getInputProps({
inputElement: inputRef.current,
});
return (
<form ref={formRef} {...formProps}>
<Flex>
<InputGroup w="full">
<InputLeftElement pointerEvents="none" mt="1">
<IoSearch size="20" />
</InputLeftElement>
<Input
w="100%"
shadow="none"
size="lg"
type="tel"
placeholder="Search icons..."
_focus={{ shadow: "md" }}
ref={inputRef}
autoFocus
{...inputProps}
/>
</InputGroup>
<Box mt="4" ml="3" mr="2" cursor="pointer">
<Link href="https://www.algolia.com/" passHref>
<Algolia width="70px" />
</Link>
</Box>
</Flex>
<>
{autocompleteState.collections.map((collection, index) => {
const { items } = collection;
return (
<div key={`${index}`}>
{items.length > 0 && (
<ul {...autocomplete.getListProps()}>
{items.map((item) => (
<AutocompleteItem key={item.id} {...item} />
))}
</ul>
)}
</div>
);
})}
</>
</form>
);
}

View File

@ -1,41 +0,0 @@
import React from "react";
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
IconButton,
Button,
useColorModeValue,
} from "@chakra-ui/react";
import Search from "components/search";
import { IoSearchOutline } from "react-icons/io5";
import Item from "components/sidebar/item";
const ModalSearch = (props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const bg = useColorModeValue("light.100", "dark.800");
return (
<>
<Item icon={IoSearchOutline} onClick={onOpen}>
Search
</Item>
<Modal isOpen={isOpen} onClose={onClose} motionPreset="slideInBottom">
<ModalOverlay />
<ModalContent bg={bg}>
<ModalHeader fontWeight="light">Search</ModalHeader>
<ModalCloseButton />
<ModalBody pb="5">
<Search />
</ModalBody>
</ModalContent>
</Modal>
</>
);
};
export default ModalSearch;

View File

@ -1,25 +0,0 @@
import React from "react";
import {
Icon,
Link,
Center,
useColorModeValue,
HStack,
} from "@chakra-ui/react";
import { IoRocketOutline } from "react-icons/io5";
const Index = () => {
const color = useColorModeValue("gray.400", "gray.600");
return (
<>
<HStack color={color} ml="6" spacing="3">
<Icon boxSize="6" as={IoRocketOutline} />
<Link href="https://github.com/pheralb" isExternal="true">
Built by Pablo
</Link>
</HStack>
</>
);
};
export default Index;

View File

@ -1,22 +0,0 @@
import React from "react";
import { useColorMode, useColorModeValue } from "@chakra-ui/react";
import { IoMoonOutline, IoSunnyOutline } from "react-icons/io5";
import Item from "./item";
const Index = () => {
const { colorMode, toggleColorMode } = useColorMode();
const iconChange = useColorModeValue(IoSunnyOutline, IoMoonOutline);
const theme = useColorModeValue("Light", "Dark");
function toggleTheme() {
toggleColorMode();
}
return (
<Item icon={iconChange} onClick={toggleTheme}>
{theme}
</Item>
);
};
export default Index;

View File

@ -1,115 +0,0 @@
import React from "react";
import {
Box,
Drawer,
DrawerContent,
DrawerOverlay,
DrawerCloseButton,
DrawerHeader,
DrawerBody,
Flex,
IconButton,
useColorModeValue,
useDisclosure,
Link,
} from "@chakra-ui/react";
import NextLink from "next/link";
import { IoApps } from "react-icons/io5";
import Logo from "components/sidebar/logo";
import Item from "components/sidebar/item";
import SidebarLinks from "components/sidebar/links";
import Dark from "components/sidebar/dark";
import By from "components/sidebar/by";
import ModalSearch from "components/search/modal";
export default function Index({ children }) {
const sidebar = useDisclosure();
const border = useColorModeValue("gray.200", "dark.800");
const bg = useColorModeValue("gray.100", "lightDark.900");
const SidebarContent = (props) => (
<Box
as="nav"
pos="fixed"
top="0"
left="0"
zIndex="sticky"
h="full"
pb="10"
overflowX="hidden"
overflowY="auto"
borderColor={border}
borderRightWidth="1px"
shadow="sm"
w="56"
{...props}
>
<Box px="5" pt="8" pb="5" align="center">
<Logo />
</Box>
<Flex direction="column" as="nav" aria-label="Main Navigation">
{SidebarLinks.map((link) => (
<NextLink key={link.id} href={link.href} passHref>
<Link
href={link.href}
isExternal={link.external}
style={{ textDecoration: "none" }}
>
<Item icon={link.icon} href={link.href} external={link.external}>
{link.title}
</Item>
</Link>
</NextLink>
))}
<ModalSearch />
<Dark />
</Flex>
<Box mt="8" align="center">
<By />
</Box>
</Box>
);
return (
<Box as="section" minH="100vh">
<SidebarContent display={{ base: "none", md: "unset" }} />
<Drawer
isOpen={sidebar.isOpen}
onClose={sidebar.onClose}
placement="left"
>
<DrawerOverlay />
<DrawerContent bg={bg}>
<DrawerCloseButton borderWidth="1px" />
<DrawerBody>
<SidebarContent pt="6" borderRight="none" />
</DrawerBody>
</DrawerContent>
</Drawer>
<Box ml={{ base: 0, md: 56 }} transition=".3s ease">
<Box
as="header"
align="center"
justify="space-between"
w="full"
p="5"
display={{ base: "inline-flex", md: "none" }}
>
<IconButton
aria-label="Menu"
onClick={sidebar.onOpen}
icon={<IoApps />}
size="md"
w="100%"
variant="ghost"
borderWidth="1px"
/>
</Box>
<Box as="main" pt={{ base: "0", md: "6" }} pb={{ base: "0", md: "6" }}>
{children}
</Box>
</Box>
</Box>
);
}

View File

@ -1,35 +0,0 @@
import React from "react";
import { useRouter } from 'next/router';
import { Flex, Icon, useColorModeValue } from "@chakra-ui/react";
import { FiExternalLink } from "react-icons/fi";
import Tap from "animations/tap";
const Item = (props) => {
const { icon, external, children, href, ...rest } = props;
const { pathname } = useRouter();
const isActive = pathname === href;
const borderColor = useColorModeValue("dark.800", "white");
return (
<Tap>
<Flex
align="center"
px="5"
pl="4"
py="4"
cursor="pointer"
transition=".15s ease"
borderColor={borderColor}
borderLeftWidth={isActive ? "2px" : ''}
{...rest}
>
{icon && <Icon ml="2" mr="4" boxSize="6" as={icon} />}
{children}
{external && <Icon ml="3" mr="4" boxSize="4" as={FiExternalLink} />}
</Flex>
</Tap>
);
};
export default Item;

View File

@ -1,28 +0,0 @@
import { IoAppsOutline, IoLogoGithub, IoBookOutline } from "react-icons/io5";
import { FiTwitter } from "react-icons/fi";
const SidebarLinks = [
{
id: 1,
href: "/",
external: false,
title: "Browse",
icon: IoAppsOutline,
},
{
id: 2,
href: "https://github.com/pheralb/svgl/",
external: true,
title: "Github",
icon: IoLogoGithub,
},
{
id: 3,
href: "https://twitter.com/pheralb_",
external: true,
title: "Twitter",
icon: FiTwitter,
},
];
export default SidebarLinks;

View File

@ -1,30 +0,0 @@
import React from "react";
import Link from "next/link";
import { HStack, Icon, Text } from "@chakra-ui/react";
import { svgl } from "components/svg";
import Tap from "animations/tap";
const Logo = () => {
return (
<Tap>
<Link href="/" passHref>
<HStack cursor="pointer">
<Icon
as={svgl}
name="logo"
boxSize="30px"
mr="2"
ml="1"
borderRadius="full"
bg="transparent"
/>
<Text fontSize="2xl" ml="2">
svgl
</Text>
</HStack>
</Link>
</Tap>
);
};
export default Logo;

File diff suppressed because one or more lines are too long

View File

@ -1,347 +0,0 @@
const SVGSLogos = [
{
id: 1,
href: "/library/discord.svg",
title: "Discord",
category: "Videocall",
url: "https://discord.com/",
},
{
id: 2,
href: "/library/github.svg",
title: "Github",
category: "Repository",
url: "https://github.com/",
},
{
id: 3,
href: "/library/preact.svg",
title: "Preact",
category: "Library",
url: "https://preactjs.com/",
},
{
id: 4,
href: "/library/react.svg",
title: "React",
category: "Library",
url: "https://reactjs.org/",
},
{
id: 5,
href: "/library/vercel.svg",
title: "Vercel",
category: "Hosting",
url: "https://vercel.com/",
},
{
id: 6,
href: "/library/svelte.svg",
title: "Svelte",
category: "Framework",
url: "https://svelte.dev/",
},
{
id: 7,
href: "/library/vue.svg",
title: "Vue",
category: "Framework",
url: "https://vuejs.org/",
},
{
id: 8,
href: "/library/nuxt.svg",
title: "Nuxt",
category: "Framework",
url: "https://nuxtjs.org/",
},
{
id: 9,
href: "/library/nextjs.svg",
title: "Nextjs",
category: "Framework",
url: "https://nextjs.org/",
},
{
id: 10,
href: "/library/vscode.svg",
title: "VSCode",
category: "Text Editor",
url: "https://code.visualstudio.com/",
},
{
id: 11,
href: "/library/jwt.svg",
title: "JWT",
category: "Security",
url: "https://jwt.io/",
},
{
id: 12,
href: "/library/strapi.svg",
title: "Strapi",
category: "CMS",
url: "https://strapi.io/",
},
{
id: 13,
href: "/library/figma.svg",
title: "Figma",
category: "Design",
url: "https://www.figma.com/",
},
{
id: 14,
href: "/library/spotify.svg",
title: "Spotify",
category: "Music",
url: "https://www.spotify.com/",
},
{
id: 15,
href: "/library/postman.svg",
title: "Postman",
category: "API",
url: "https://www.getpostman.com/",
},
{
id: 16,
href: "/library/algolia.svg",
title: "Algolia",
category: "Search",
url: "https://www.algolia.com/",
},
{
id: 17,
href: "/library/bootstrap.svg",
title: "Bootstrap",
category: "CSS Framework",
url: "https://getbootstrap.com/",
},
{
id: 18,
href: "/library/firebase.svg",
title: "Firebase",
category: "Hosting",
url: "https://firebase.google.com/",
},
{
id: 19,
href: "/library/supabase.svg",
title: "Supabase",
category: "Database",
url: "https://supabase.com/",
},
{
id: 20,
href: "/library/vitejs.svg",
title: "Vite.js",
category: "JavaScript Compiler",
url: "https://vitejs.dev",
},
{
id: 21,
href: "/library/facebook.svg",
title: "Facebook",
category: "Social",
url: "https://www.facebook.com/",
},
{
id: 22,
href: "/library/twitter.svg",
title: "Twitter",
category: "Social",
url: "https://twitter.com/",
},
{
id: 23,
href: "/library/nodejs.svg",
title: "Node.js",
category: "JavaScript Runtime",
url: "https://nodejs.org/",
},
{
id: 24,
href: "/library/esbuild.svg",
title: "Esbuild",
category: "JavaScript Compiler",
url: "https://esbuild.github.io/",
},
{
id: 25,
href: "/library/deno.svg",
title: "Deno",
category: "JavaScript Runtime",
url: "https://deno.land/",
},
{
id: 26,
href: "/library/gatsby.svg",
title: "Gatsby",
category: "Static Site Generator",
url: "https://www.gatsbyjs.org/",
},
{
id: 27,
href: "/library/npm.svg",
title: "NPM",
category: "Package Manager",
url: "https://www.npmjs.com/",
},
{
id: 28,
href: "/library/homebrew.svg",
title: "Homebrew",
category: "Package Manager",
url: "https://brew.sh/",
},
{
id: 29,
href: "/library/sublimetext.svg",
title: "Sublime Text",
category: "Text Editor",
url: "https://www.sublimetext.com/",
},
{
id: 30,
href: "/library/turborepo.svg",
title: "TurboRepo",
category: "Package Manager",
url: "https://turborepo.org/",
},
{
id: 31,
href: "/library/tailwindcss.svg",
title: "Tailwind CSS",
category: "CSS Framework",
url: "https://tailwindcss.com/",
},
{
id: 32,
href: "/library/styledcomponents.svg",
title: "Styled Components",
category: "CSS-in-JS",
url: "https://styled-components.com/",
},
{
id: 33,
href: "/library/angular.svg",
title: "Angular",
category: "Framework",
url: "https://angular.io/",
},
{
id: 34,
href: "/library/blitzjs.svg",
title: "Blitz",
category: "Framework",
url: "https://blitzjs.com/",
},
{
id: 35,
href: "/library/lit.svg",
title: "Lit",
category: "Web Components",
url: "https://lit.dev/",
},
{
id: 36,
href: "/library/atom.svg",
title: "Atom",
category: "Text Editor",
url: "https://atom.io/",
},
{
id: 37,
href: "/library/youtube.svg",
title: "YouTube",
category: "Video Platform",
url: "https://www.youtube.com/",
},
{
id: 38,
href: "/library/astro.svg",
title: "Astro",
category: "Framework",
url: "https://astro.build/",
},
{
id: 39,
href: "/library/google.svg",
title: "Google",
category: "Search",
url: "https://www.google.com/",
},
{
id: 40,
href: "/library/framer.svg",
title: "Framer",
category: "Design",
url: "https://framer.com/",
},
{
id: 41,
href: "/library/netflix.svg",
title: "Netflix",
category: "Video Platform",
url: "https://www.netflix.com/",
},
{
id: 42,
href: "/library/firefox.svg",
title: "Firefox",
category: "Browser",
url: "https://www.mozilla.org/en-US/firefox/",
},
{
id: 43,
href: "/library/linkedin.svg",
title: "LinkedIn",
category: "Social",
url: "https://www.linkedin.com/",
},
{
id: 44,
href:"/library/telegram.svg",
title: "Telegram",
category: "Social",
url: "https://web.telegram.org/",
},
{
id: 45,
href: "/library/whatsapp.svg",
title: "WhatsApp",
category: "Social",
url: "https://web.whatsapp.com/",
},
{
id: 46,
href: "/library/headlessui.svg",
title: "Headless UI",
category: "Design",
url: "https://headlessui.dev/",
},
{
id: 47,
href: "/library/kotlin.svg",
title: "Kotlin",
category: "Java",
url: "https://kotlinlang.org/",
},
{
id: 48,
href: "/library/tiktok.svg",
title: "TikTok",
category: "Video Platform",
url: "https://tiktok.com/",
},
{
id: 49,
href: "/library/storybook.svg",
title: "Storybook",
category: "UI component explorer",
url: "https://storybook.js.org/",
}
];
export default SVGSLogos;

345
data/svgs.json Normal file
View File

@ -0,0 +1,345 @@
[
{
"id": 1,
"slug": "/library/discord.svg",
"title": "Discord",
"category": "Software",
"url": "https://discord.com/"
},
{
"id": 2,
"slug": "/library/github.svg",
"title": "Github",
"category": "Software",
"url": "https://github.com/"
},
{
"id": 3,
"slug": "/library/preact.svg",
"title": "Preact",
"category": "Library",
"url": "https://preactjs.com/"
},
{
"id": 4,
"slug": "/library/react.svg",
"title": "React",
"category": "Library",
"url": "https://reactjs.org/"
},
{
"id": 5,
"slug": "/library/vercel.svg",
"title": "Vercel",
"category": "Hosting",
"url": "https://vercel.com/"
},
{
"id": 6,
"slug": "/library/svelte.svg",
"title": "Svelte",
"category": "Library",
"url": "https://svelte.dev/"
},
{
"id": 7,
"slug": "/library/vue.svg",
"title": "Vue",
"category": "Framework",
"url": "https://vuejs.org/"
},
{
"id": 8,
"slug": "/library/nuxt.svg",
"title": "Nuxt",
"category": "Framework",
"url": "https://nuxtjs.org/"
},
{
"id": 9,
"slug": "/library/nextjs.svg",
"title": "Nextjs",
"category": "Framework",
"url": "https://nextjs.org/"
},
{
"id": 10,
"slug": "/library/vscode.svg",
"title": "VSCode",
"category": "Software",
"url": "https://code.visualstudio.com/"
},
{
"id": 11,
"slug": "/library/jwt.svg",
"title": "JWT",
"category": "Library",
"url": "https://jwt.io/"
},
{
"id": 12,
"slug": "/library/strapi.svg",
"title": "Strapi",
"category": "CMS",
"url": "https://strapi.io/"
},
{
"id": 13,
"slug": "/library/figma.svg",
"title": "Figma",
"category": "Software",
"url": "https://www.figma.com/"
},
{
"id": 14,
"slug": "/library/spotify.svg",
"title": "Spotify",
"category": "Software",
"url": "https://www.spotify.com/"
},
{
"id": 15,
"slug": "/library/postman.svg",
"title": "Postman",
"category": "Software",
"url": "https://www.getpostman.com/"
},
{
"id": 16,
"slug": "/library/algolia.svg",
"title": "Algolia",
"category": "Library",
"url": "https://www.algolia.com/"
},
{
"id": 17,
"slug": "/library/bootstrap.svg",
"title": "Bootstrap",
"category": "Framework",
"url": "https://getbootstrap.com/"
},
{
"id": 18,
"slug": "/library/firebase.svg",
"title": "Firebase",
"category": "Hosting",
"url": "https://firebase.google.com/"
},
{
"id": 19,
"slug": "/library/supabase.svg",
"title": "Supabase",
"category": "Database",
"url": "https://supabase.com/"
},
{
"id": 20,
"slug": "/library/vitejs.svg",
"title": "Vite.js",
"category": "Compiler",
"url": "https://vitejs.dev"
},
{
"id": 21,
"slug": "/library/facebook.svg",
"title": "Facebook",
"category": "Social",
"url": "https://www.facebook.com/"
},
{
"id": 22,
"slug": "/library/twitter.svg",
"title": "Twitter",
"category": "Social",
"url": "https://twitter.com/"
},
{
"id": 23,
"slug": "/library/nodejs.svg",
"title": "Node.js",
"category": "Library",
"url": "https://nodejs.org/"
},
{
"id": 24,
"slug": "/library/esbuild.svg",
"title": "Esbuild",
"category": "Compiler",
"url": "https://esbuild.github.io/"
},
{
"id": 25,
"slug": "/library/deno.svg",
"title": "Deno",
"category": "Library",
"url": "https://deno.land/"
},
{
"id": 26,
"slug": "/library/gatsby.svg",
"title": "Gatsby",
"category": "Framework",
"url": "https://www.gatsbyjs.org/"
},
{
"id": 27,
"slug": "/library/npm.svg",
"title": "NPM",
"category": "Software",
"url": "https://www.npmjs.com/"
},
{
"id": 28,
"slug": "/library/homebrew.svg",
"title": "Homebrew",
"category": "Software",
"url": "https://brew.sh/"
},
{
"id": 29,
"slug": "/library/sublimetext.svg",
"title": "Sublime Text",
"category": "Software",
"url": "https://www.sublimetext.com/"
},
{
"id": 30,
"slug": "/library/turborepo.svg",
"title": "TurboRepo",
"category": "Library",
"url": "https://turborepo.org/"
},
{
"id": 31,
"slug": "/library/tailwindcss.svg",
"title": "Tailwind CSS",
"category": "Framework",
"url": "https://tailwindcss.com/"
},
{
"id": 32,
"slug": "/library/styledcomponents.svg",
"title": "Styled Components",
"category": "Library",
"url": "https://styled-components.com/"
},
{
"id": 33,
"slug": "/library/angular.svg",
"title": "Angular",
"category": "Framework",
"url": "https://angular.io/"
},
{
"id": 34,
"slug": "/library/blitzjs.svg",
"title": "Blitz",
"category": "Framework",
"url": "https://blitzjs.com/"
},
{
"id": 35,
"slug": "/library/lit.svg",
"title": "Lit",
"category": "Library",
"url": "https://lit.dev/"
},
{
"id": 36,
"slug": "/library/atom.svg",
"title": "Atom",
"category": "Software",
"url": "https://atom.io/"
},
{
"id": 37,
"slug": "/library/youtube.svg",
"title": "YouTube",
"category": "Social",
"url": "https://www.youtube.com/"
},
{
"id": 38,
"slug": "/library/astro.svg",
"title": "Astro",
"category": "Framework",
"url": "https://astro.build/"
},
{
"id": 39,
"slug": "/library/google.svg",
"title": "Google",
"category": "Social",
"url": "https://www.google.com/"
},
{
"id": 40,
"slug": "/library/framer.svg",
"title": "Framer",
"category": "Software",
"url": "https://framer.com/"
},
{
"id": 41,
"slug": "/library/netflix.svg",
"title": "Netflix",
"category": "Entertainment",
"url": "https://www.netflix.com/"
},
{
"id": 42,
"slug": "/library/firefox.svg",
"title": "Firefox",
"category": "Software",
"url": "https://www.mozilla.org/en-US/firefox/"
},
{
"id": 43,
"slug": "/library/linkedin.svg",
"title": "LinkedIn",
"category": "Social",
"url": "https://www.linkedin.com/"
},
{
"id": 44,
"slug": "/library/telegram.svg",
"title": "Telegram",
"category": "Social",
"url": "https://web.telegram.org/"
},
{
"id": 45,
"slug": "/library/whatsapp.svg",
"title": "WhatsApp",
"category": "Social",
"url": "https://web.whatsapp.com/"
},
{
"id": 46,
"slug": "/library/headlessui.svg",
"title": "Headless UI",
"category": "Library",
"url": "https://headlessui.dev/"
},
{
"id": 47,
"slug": "/library/kotlin.svg",
"title": "Kotlin",
"category": "Language",
"url": "https://kotlinlang.org/"
},
{
"id": 48,
"slug": "/library/vitest.svg",
"title": "Vitest",
"category": "Framework",
"url": "https://vitest.dev/"
},
{
"id": 49,
"slug": "/library/storybook.svg",
"title": "Storybook",
"category": "Software",
"url": "https://storybook.js.org/"
}
]

View File

@ -1,5 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./"
}
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <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.

57
next-seo.config.ts Normal file
View File

@ -0,0 +1,57 @@
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,15 +1,12 @@
/** @type {import('next').NextConfig} */
const withPWA = require("next-pwa"); const withPWA = require("next-pwa");
const nextConfig = {
reactStrictMode: true,
};
module.exports = withPWA({ module.exports = withPWA({
reactStrictMode: true,
pwa: { pwa: {
dest: "public", dest: "public",
register: true, register: true,
skipWaiting: true, skipWaiting: true,
disable: process.env.NODE_ENV === "development", disable: process.env.NODE_ENV === "development",
}, },
nextConfig,
}); });

9617
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,55 @@
{ {
"name": "svgl", "name": "svgl",
"version": "1.2.0", "version": "2.0.0",
"description": "Beautiful SVG vector logos", "description": "A beautiful library with SVG logos.",
"author": "pheralb", "private": true,
"author": "@pheralb_",
"license": "MIT", "license": "MIT",
"keywords": [
"svgs",
"logos",
"images",
"library"
],
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"test": "vitest",
"ready": "vitest && next build"
}, },
"dependencies": { "dependencies": {
"@algolia/autocomplete-core": "^1.5.6", "@chakra-ui/react": "2.1.2",
"@chakra-ui/react": "^1.8.7", "@emotion/react": "11.9.0",
"@emotion/react": "^11.8.2", "@emotion/styled": "11.8.1",
"@emotion/styled": "^11.8.1", "@uiball/loaders": "1.2.6",
"canvas-confetti": "^1.5.1", "canvas-confetti": "1.5.1",
"downloadjs": "^1.4.7", "downloadjs": "1.4.7",
"framer-motion": "^6.2.8", "framer-motion": "6.3.9",
"next": "12.1.0", "next": "12.1.6",
"next-pwa": "^5.5.0", "next-pwa": "5.5.4",
"nextjs-progressbar": "^0.0.14", "next-seo": "5.4.0",
"react": "17.0.2", "nextjs-progressbar": "0.0.14",
"react-dom": "17.0.2", "phosphor-react": "1.4.1",
"react-hot-toast": "^2.2.0", "react": "18.1.0",
"react-icons": "^4.3.1", "react-dom": "18.1.0",
"swr": "^1.2.2" "react-hot-toast": "2.2.0",
"swr": "1.3.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "8.10.0", "@testing-library/jest-dom": "5.16.4",
"eslint-config-next": "12.1.0" "@testing-library/react": "13.3.0",
"@types/canvas-confetti": "1.4.2",
"@types/downloadjs": "1.4.3",
"@types/node": "17.0.38",
"@types/react": "18.0.10",
"@types/react-dom": "18.0.5",
"@vitejs/plugin-react": "1.3.2",
"eslint": "8.16.0",
"eslint-config-next": "12.1.6",
"jsdom": "20.0.0",
"typescript": "4.7.2",
"vitest": "0.15.2"
} }
} }

View File

@ -1,8 +0,0 @@
import Error from 'components/error';
import React from 'react';
const Error404 = () => {
return <Error />;
};
export default Error404;

View File

@ -1,78 +0,0 @@
// 🖤 Next Head ->
import Head from "next/head";
// 🌿 Chakra UI ->
import { ChakraProvider, Container, useColorModeValue } from "@chakra-ui/react";
// ➡️ Nextjs Progressbar ->
import NextNProgress from "nextjs-progressbar";
// 📦 Components ->
import Sidebar from "components/sidebar";
import Layout from "components/layout";
import Footer from "components/sidebar/by";
// 💙 Global CSS ->
import "styles/globals.css";
// 🎨 Theme ->
import theme from "styles/theme";
// 🐢 Animations ->
import Transitions from "animations/transitions";
import { Toaster } from "react-hot-toast";
function MyApp({ Component, pageProps, router }) {
const progress = useColorModeValue("#7B7B7B", "#D4D4D4");
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SVGL - Beautiful SVG vector logos</title>
<meta property="og:title" content="SVGL - Beautiful SVG vector logos" />
<meta
property="og:description"
content="Beautiful SVG logos. Free and open source."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://svgl.vercel.app/" />
<meta
property="og:image"
content="https://svgl.vercel.app/images/banner.png"
/>
<meta name="twitter:site" content="@pheralb_" />
<meta
property="twitter:title"
content="SVGL - Beautiful SVG vector logos"
/>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:creator" content="@pheralb" />
<meta
property="twitter:description"
content="Beautiful SVG logos. Free and open source."
/>
<meta
name="twitter:image"
content="https://svgl.vercel.app/images/banner.png"
/>
<meta name="keywords" content="svg,vector,logo,logos,download" />
<meta content="#16161a" name="theme-color" />
<link rel="icon" href="/icons/icon.ico" />
</Head>
<ChakraProvider theme={theme}>
<Sidebar>
<NextNProgress color={progress}/>
<Layout>
<Transitions key={router.route}>
<Component {...pageProps} />
</Transitions>
</Layout>
</Sidebar>
</ChakraProvider>
<Toaster position="bottom-center" reverseOrder={false} />
</>
);
}
export default MyApp;

View File

@ -1,38 +0,0 @@
import React from "react";
import { Box, Flex, Button, Text, Icon } from "@chakra-ui/react";
import { IoHome, IoWarning } from "react-icons/io5";
import Link from "next/link";
import Show from "animations/show";
const Offline = () => {
return (
<>
<Show delay="0">
<Box px={{ base: 4, lg: 20 }} py={{ base: "3", md: "24" }}>
<Flex align="center" justify="center" direction="column" w="full">
<Icon name="error" boxSize="80px" mb="3" as={IoWarning} />
<Text fontSize="40px" mb="2">
Oh no!
</Text>
<Text fontSize="20px" mb="3">
No internet connection
</Text>
<Link href="/" passHref>
<Button
leftIcon={<IoHome />}
borderWidth="1px"
variant="outline"
fontWeight="light"
mb="4"
>
Refresh
</Button>
</Link>
</Flex>
</Box>
</Show>
</>
);
};
export default Offline;

View File

@ -1,6 +0,0 @@
import db from "data/svgs";
// 📦 Show all content ->
export default function handler(req, res) {
res.status(200).json(db);
}

View File

@ -1,13 +0,0 @@
import db from "data/svgs";
// 📦 Show categories ->
export default function handler(req, res) {
try {
const categories = db
.map((item) => item.category)
.filter((category, index, self) => self.indexOf(category) === index);
return res.status(200).json(categories);
} catch (err) {
res.status(400).json({ message: err });
}
}

View File

@ -1,116 +0,0 @@
import React from "react";
import Head from "next/head";
import download from "downloadjs";
import { Box, Button, Flex, HStack, Link, Text } from "@chakra-ui/react";
import Show from "animations/show";
import Grid from "components/grid";
import DesignCard from "components/card/design";
import { RiFontSize, RiPaletteLine } from "react-icons/ri";
import { IoMdImages } from "react-icons/io";
import { HiOutlineExternalLink } from "react-icons/hi";
import { IoCloudDownloadOutline } from "react-icons/io5";
import CardImage from "components/card/image";
const Design = () => {
const downloadSvg = (url) => {
download(url);
};
return (
<>
<Head>
<title>Design - SVGL</title>
</Head>
<Box mt="6">
<Box w="full" border="solid 1px transparent">
<Show>
<Text
as="h1"
fontSize={{ base: "25px", sm: "35px", md: "5xl", lg: "6xl" }}
letterSpacing="tight"
lineHeight="short"
fontWeight="extrabold"
mb="3"
>
Design
</Text>
</Show>
<Show delay={0.3}>
<Box mt={{ base: 4, md: 5 }}>
<Grid>
<DesignCard title="Fonts" icon={RiFontSize}>
<HStack spacing={2}>
<Link
href="https://fonts.google.com/specimen/Poppins"
isExternal
>
Poppins
</Link>
<HiOutlineExternalLink size="20px" />
</HStack>
</DesignCard>
<DesignCard title="Colors" icon={RiPaletteLine}>
<Flex color="white">
<Box bg="lightDark.900" p="2">
<Text fontSize="15px">#16161a</Text>
</Box>
<Box bg="light.100" p="2">
<Text fontSize="15px" color="black">
#f9f9f9
</Text>
</Box>
<Box bg="#6748E6" p="2">
<Text fontSize="15px" color="white">
#6748E6
</Text>
</Box>
</Flex>
</DesignCard>
</Grid>
<Box mt="4">
<DesignCard title="Images" icon={IoMdImages}>
<HStack spacing={3}>
<CardImage title="Banner" image="/images/banner.png">
<Button
w={{ base: "100%", md: "auto" }}
leftIcon={<IoCloudDownloadOutline />}
variant="primary"
fontWeight="light"
mt="2"
onClick={() =>
downloadSvg(
"https://svgl.vercel.app/images/banner.png"
)
}
>
Download
</Button>
</CardImage>
<CardImage title="Logo" image="/images/logo.png">
<Button
w={{ base: "100%", md: "auto" }}
leftIcon={<IoCloudDownloadOutline />}
variant="primary"
fontWeight="light"
mt="2"
onClick={() =>
downloadSvg(
"https://svgl.vercel.app/images/logo.png"
)
}
>
Download
</Button>
</CardImage>
</HStack>
</DesignCard>
</Box>
</Box>
</Show>
</Box>
</Box>
</>
);
};
export default Design;

View File

@ -1,19 +0,0 @@
import { chakra, Box } from "@chakra-ui/react";
import Search from "components/search";
import Items from "components/items/all";
import Loader from "animations/loader";
export default function Index() {
return (
<>
<Box mt="6">
<Box w="full" border="solid 1px transparent">
<Search />
<Box mt={{ base: 4, md: 8 }}>
<Items />
</Box>
</Box>
</Box>
</>
);
}

View File

@ -1,134 +0,0 @@
import Head from "next/head";
import {
chakra,
Box,
Flex,
SimpleGrid,
Button,
Image,
Center,
Link,
useColorModeValue,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import useSWR from "swr";
import Error from "components/error";
import { IoArrowBackOutline, IoCloudDownloadOutline } from "react-icons/io5";
import { BiLinkExternal } from "react-icons/bi";
import Show from "animations/show";
import Loader from "animations/loader";
import confetti from "canvas-confetti";
import download from "downloadjs";
import NextLink from 'next/link';
const fetcher = async (url) => {
const res = await fetch(url);
const data = await res.json();
if (res.status !== 200) {
throw new Error(data.message);
}
return data;
};
export default function Icon() {
const { query } = useRouter();
const { data, error } = useSWR(
() => query.id && `/api/search?id=${query.id}`,
fetcher
);
const bgImage = useColorModeValue("transparent", "#E9E9E9");
const borderRds = useColorModeValue("0", "15px");
if (error) return <Error />;
if (!data) return <Loader />;
const downloadSvg = (url) => {
confetti({
particleCount: 200,
startVelocity: 30,
spread: 300,
gravity: 1.2,
origin: { y: 0 },
});
download(url);
};
return (
<>
<Head>
<title>{data.title} - SVGL</title>
</Head>
<Show delay="0">
<NextLink href="/" passHref>
<Button
leftIcon={<IoArrowBackOutline />}
fontWeight="light"
variant="ghost"
mb="4"
>
Continue discovering
</Button>
</NextLink>
<SimpleGrid columns={{ base: 1, md: 1, lg: 2 }} spacing={0}>
<Box py={{ base: "10", md: "24" }}>
<Center>
<Image
src={data.href}
alt={data.title}
w={{ base: "30%", md: "20%", lg: "30%" }}
fit="cover"
loading="lazy"
bg={bgImage}
borderRadius={borderRds}
p="1"
/>
</Center>
</Box>
<Flex
direction="column"
alignItems="start"
justifyContent="center"
px={{ base: 4, lg: 4 }}
py={{ base: "3", md: "0", lg: "10" }}
>
<chakra.h1
mb={3}
fontSize={{ base: "4xl", md: "4xl", lg: "5xl" }}
fontWeight="semibold"
lineHeight="shorter"
>
{data.title}
</chakra.h1>
<Flex direction={{ base: "column", md: "row" }} w="100%" mt="2">
<Button
w={{ base: "100%", md: "auto" }}
mb={{ base: "2", md: "0" }}
leftIcon={<IoCloudDownloadOutline />}
variant="primary"
fontWeight="light"
mr="2"
onClick={() => downloadSvg(data.href)}
>
Download .svg
</Button>
<Link
href={data.url}
style={{ textDecoration: "none" }}
isExternal
>
<Button
w={{ base: "100%", md: "auto" }}
fontWeight="light"
borderWidth="1px"
rightIcon={<BiLinkExternal />}
>
{data.title} website
</Button>
</Link>
</Flex>
</Flex>
</SimpleGrid>
</Show>
</>
);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 887 KiB

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@ -1 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z" fill="#3856cf" class="fill-000000"></path><circle cx="7.5" cy="10.5" r="1.5" fill="#3856cf" class="fill-000000"></circle><circle cx="10.5" cy="7.5" r="1.5" fill="#3856cf" class="fill-000000"></circle><circle cx="11.5" cy="11.5" r="1.5" fill="#3856cf" class="fill-000000"></circle></svg> <svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="#4343e5" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M128,92a20,20,0,0,0-40,0v24" fill="none" stroke="#4343e5" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"></path><path d="M168,108V92a20,20,0,0,0-40,0v32" fill="none" stroke="#4343e5" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"></path><path d="M88,148V116H68a20.1,20.1,0,0,0-20,20v16a80,80,0,0,0,160,0V108a20,20,0,0,0-40,0v16" fill="none" stroke="#4343e5" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"></path></svg>

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 633 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="234px" viewBox="0 0 256 234" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<title>vitest</title>
<g>
<path d="M192.115018,70.8083821 L130.914358,159.296327 C130.265568,160.234541 129.327185,160.937354 128.241634,161.298075 C127.156084,161.658796 125.982639,161.655409 124.899291,161.292995 C123.815942,160.928886 122.881962,160.222686 122.23876,159.282779 C121.595728,158.341178 121.278531,157.216676 121.335772,156.08032 L123.78512,107.225806 L84.2429694,98.839802 C83.3978997,98.6607962 82.6105792,98.2768738 81.950612,97.7222438 C81.2906447,97.1677833 80.7783529,96.4595505 80.4592925,95.6603753 C80.1402321,94.8612002 80.0242256,93.9958081 80.121434,93.1407466 C80.2186424,92.2858544 80.5260175,91.4675424 81.0164627,90.7584629 L142.217801,2.27034881 C142.86676,1.33162707 143.805143,0.628982928 144.890693,0.268769843 C145.976413,-0.0914432423 147.149858,-0.0895803631 148.233376,0.27435848 C149.316725,0.638127971 150.250874,1.34398981 150.893906,2.28491313 C151.536938,3.2256671 151.853966,4.34999934 151.796556,5.48737173 L149.347208,54.3423934 L188.88885,62.7277204 C189.73392,62.9067262 190.52141,63.2906486 191.181885,63.8451092 C191.842361,64.3997391 192.353806,65.1079719 192.672189,65.9071471 C192.992265,66.7063222 193.107425,67.5717143 193.010894,68.4266064 C192.914363,69.281668 192.606141,70.09998 192.115018,70.8090595 L192.115018,70.8083821 Z" fill="#FCC72B"></path>
<path d="M128.024524,233.537148 C126.396707,233.538835 124.784639,233.220452 123.280787,232.597234 C121.776936,231.974016 120.410937,231.059512 119.261541,229.907914 L61.4337079,172.084145 C59.1203507,169.758934 57.8236174,166.610668 57.8278409,163.330307 C57.8322544,160.049946 59.1372859,156.903374 61.4570785,154.584936 C63.7767018,152.264805 66.9217498,150.959096 70.20228,150.954015 C73.4829795,150.950628 76.631584,152.246176 78.9576426,154.559533 L128.024524,203.620996 L234.917207,96.7333937 C237.247499,94.4406975 240.388991,93.1617463 243.657497,93.1750213 C246.927697,93.1883347 250.059027,94.4928582 252.368997,96.8043525 C254.680661,99.1160161 255.98637,102.247177 256,105.516191 C256.011773,108.785206 254.73316,111.927036 252.440126,114.257159 L136.785306,229.907914 C135.636079,231.061206 134.270419,231.974016 132.766907,232.597234 C131.263563,233.220452 129.651834,233.538835 128.024524,233.537148 Z" fill="#729B1B"></path>
<path d="M127.974735,233.537148 C129.602552,233.538835 131.21462,233.220452 132.718472,232.597234 C134.222323,231.974016 135.588152,231.059512 136.737718,229.907914 L194.565551,172.084145 C196.878908,169.758934 198.17615,166.610668 198.171084,163.330307 C198.167682,160.049946 196.861973,156.903374 194.541842,154.584936 C192.221711,152.264805 189.076832,150.959096 185.796471,150.954015 C182.51611,150.950628 179.367844,152.246176 177.040939,154.559533 L127.974735,203.620996 L21.0822215,96.7333937 C18.751929,94.4406975 15.6100986,93.1617463 12.3412538,93.1750213 C9.07223962,93.1883347 5.94090913,94.4928582 3.62943176,96.8043525 C1.31790358,99.1160161 0.013413991,102.247177 -1.20332295e-14,105.516191 C-0.0132082455,108.785206 1.26574296,111.927036 3.55837139,114.257159 L119.213783,229.907914 C120.36318,231.061206 121.72884,231.974016 123.232183,232.597234 C124.735696,233.220452 126.347425,233.538835 127.974735,233.537148 Z" fill-opacity="0.5" fill="#729B1B"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,6 +1,6 @@
{ {
"theme_color": "#36558F", "theme_color": "#4343E5",
"background_color": "#36558F", "background_color": "#050505",
"display": "minimal-ui", "display": "minimal-ui",
"scope": "/", "scope": "/",
"start_url": "/", "start_url": "/",
@ -27,6 +27,12 @@
"src": "/icons/icon-512x512.png", "src": "/icons/icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
},
{
"src": "/icons/maskable_icon.png",
"sizes": "196x196",
"type": "image/png",
"purpose": "maskable"
} }
] ]
} }

View File

@ -0,0 +1,13 @@
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,7 +1,12 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
const Show = ({ children, delay }) => { type ShowProps = {
children: React.ReactNode;
delay?: number;
};
const Show = ({ children, delay }: ShowProps) => {
return ( return (
<motion.div <motion.div
initial={{ y: 10, opacity: 0 }} initial={{ y: 10, opacity: 0 }}

16
src/animations/tap.tsx Normal file
View File

@ -0,0 +1,16 @@
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;

13
src/common/grid.tsx Normal file
View File

@ -0,0 +1,13 @@
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;

16
src/common/iconBtn.tsx Normal file
View File

@ -0,0 +1,16 @@
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}
/>
);
};
export default CustomIconBtn;

20
src/common/link.tsx Normal file
View File

@ -0,0 +1,20 @@
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 }: CustomLinkProps) => {
return (
<NextLink href={href} passHref>
<Link
isExternal={external}
_hover={{ textDecoration: "none" }}
_focus={{ border: "none" }}
>
{children}
</Link>
</NextLink>
);
};
export default CustomLink;

47
src/components/error.tsx Normal file
View File

@ -0,0 +1,47 @@
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

@ -0,0 +1,16 @@
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;

84
src/components/search.tsx Normal file
View File

@ -0,0 +1,84 @@
import { useEffect, useState } from "react";
import { Input, Text, Image, HStack, Box, Center } from "@chakra-ui/react";
import useDebounce from "@/hooks/useDebounce";
import { 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 = () => {
const [search, setSearch] = useState("");
const [results, setResults] = useState<SVGCardProps[]>([]);
const debouncedSearch = useDebounce(search, 500);
useEffect(() => {
if (debouncedSearch) {
fetch(getSvgByQuery + debouncedSearch).then((res) => {
if (res.ok) {
res.json().then((data) => {
setResults(data);
});
}
});
}
}, [debouncedSearch]);
const handleClear = () => {
setSearch("");
setResults([]);
};
return (
<>
<Input
width="full"
variant="flushed"
size="lg"
placeholder="Search svgs..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{results && results.length > 0 && (
<>
<HStack spacing={4} mt={4} overflowX="auto" overflowY="hidden">
{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

@ -0,0 +1,36 @@
import React from "react";
import { SVGCardProps } from "@/interfaces/components";
import { Box, Center, Image, Text, useColorModeValue } from "@chakra-ui/react";
import Tap from "@/animations/tap";
import CustomLink from "@/common/link";
const SVGCard = (props: SVGCardProps) => {
const bg = useColorModeValue("bg.light", "bg.dark");
return (
<Tap>
<CustomLink href={`/svg/${props.id}`}>
<Box
bg={bg}
p={4}
cursor="pointer"
borderRadius="10px"
borderWidth="1px"
mb="2"
_hover={{
shadow: "md",
}}
transition="all 0.2s"
>
<Center>
<Image boxSize="50px" src={props.svg} alt={props.title} />
</Center>
<Text mt="2" fontWeight="light" textAlign="center">
{props.title}
</Text>
</Box>
</CustomLink>
</Tap>
);
};
export default SVGCard;

View File

@ -0,0 +1,89 @@
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 || "");
};
// Copy to clipboard =>
const copyToClipboard = (url?: string) => {
fetch(url || "").then((response) => {
response.text().then((content) => {
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;

17
src/hooks/useDebounce.tsx Normal file
View File

@ -0,0 +1,17 @@
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;

View File

@ -0,0 +1,39 @@
export interface LayoutProps {
children: React.ReactNode;
}
export interface CustomLinkProps {
href: string;
children: React.ReactNode;
external?: boolean;
}
export interface CustomIconBtnProps {
title: string;
icon: React.ReactElement;
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;
}

View File

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

View File

@ -0,0 +1,21 @@
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

@ -0,0 +1,26 @@
import React from "react";
import useSWR from "swr";
import { getCategorySvgs } from "@/services";
import CustomLink from "@/common/link";
import { Box } from "@chakra-ui/react";
import { RaceBy } from "@uiball/loaders";
const Categories = () => {
const { data, error } = useSWR(getCategorySvgs);
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) => (
<Box key={category} p="4" borderRadius="5px" borderWidth="1px">
<CustomLink href={`/category/${category}`}>{category}</CustomLink>
</Box>
))}
</>
);
};
export default Categories;

111
src/layout/header/index.tsx Normal file
View File

@ -0,0 +1,111 @@
import React from "react";
import {
Box,
Flex,
useColorModeValue,
HStack,
Button,
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";
const Header = () => {
const bg = useColorModeValue("bg.light", "bg.dark");
const { isOpen, onToggle } = useDisclosure();
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={1}>
<HStack
spacing={1}
mr={1}
display={{ base: "none", md: "inline-flex" }}
>
{Links.map((link) => (
<CustomLink
key={link.title}
href={link.slug}
external={link.external}
>
<Button variant="ghost" fontFamily="Inter-Semibold">
{link.title}
{link.external ? (
<Icon as={ArrowSquareOut} ml="2" />
) : null}
</Button>
</CustomLink>
))}
<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">
<Search />
</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", md: "center" }}
spacing={4}
overflowX="auto"
overflowY="hidden"
bg={bg}
pb="4"
borderBottomWidth="1px"
>
<Categories />
</HStack>
</Box>
</>
);
};
export default Header;

View File

@ -0,0 +1,12 @@
export const Links = [
{
title: "Github",
slug: "https://github.com/pheralb/svgl",
external: true,
},
{
title: "Twitter",
slug: "https://twitter.com/pheralb_",
external: true,
},
];

View File

@ -0,0 +1,57 @@
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";
const Mobile = () => {
const bg = useColorModeValue("bg.light", "bg.dark");
const mobileNav = useDisclosure();
return (
<>
<IconButton
display={{ base: "flex", md: "none" }}
aria-label="Open menu navbar"
variant="ghost"
icon={<List size={22} />}
onClick={mobileNav.onOpen}
/>
<VStack
pos="absolute"
top={0}
left={0}
right={0}
display={mobileNav.isOpen ? "flex" : "none"}
flexDirection="column"
p={4}
pb={4}
bg={bg}
spacing={3}
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}
>
<Button variant="ghost">{link.title}</Button>
</CustomLink>
))}
</VStack>
</>
);
};
export default Mobile;

View File

@ -0,0 +1,33 @@
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;

17
src/layout/index.tsx Normal file
View File

@ -0,0 +1,17 @@
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;

9
src/pages/404.tsx Normal file
View File

@ -0,0 +1,9 @@
import React from "react";
type Props = {};
const Error = (props: Props) => {
return <div>Error 404</div>;
};
export default Error;

63
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,63 @@
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}
/>
<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,16 +1,12 @@
import { ColorModeScript } from "@chakra-ui/react"; import { ColorModeScript } from "@chakra-ui/react";
import NextDocument, { Html, Head, Main, NextScript } from "next/document"; import NextDocument, { Html, Head, Main, NextScript } from "next/document";
import theme from "styles/theme"; import theme from "@/theme";
export default class Document extends NextDocument { export default class Document extends NextDocument {
render() { render() {
return ( return (
<Html> <Html lang="en">
<Head> <Head />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icons/icon-512x512.png"></link>
<meta name="theme-color" content="#36558F" />
</Head>
<body> <body>
<ColorModeScript initialColorMode={theme.config.initialColorMode} /> <ColorModeScript initialColorMode={theme.config.initialColorMode} />
<Main /> <Main />

12
src/pages/api/all.ts Normal file
View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,10 @@
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,6 +1,7 @@
import db from "data/svgs"; import db from "data/svgs.json";
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req, res) { export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { id, q, c } = req.query; const { id, q, c } = req.query;
// 🔎 Search by id (ex: ?id=1) -> // 🔎 Search by id (ex: ?id=1) ->
@ -13,7 +14,7 @@ export default function handler(req, res) {
if (q) { if (q) {
const results = db.filter((product) => { const results = db.filter((product) => {
const { title } = product; const { title } = product;
return title.toLowerCase().includes(q.toLowerCase()); return title.toLowerCase().includes(q.toString().toLowerCase());
}); });
return res.status(200).json(results); return res.status(200).json(results);
} }
@ -22,11 +23,11 @@ export default function handler(req, res) {
if (c) { if (c) {
const results = db.filter((product) => { const results = db.filter((product) => {
const { category } = product; const { category } = product;
return category.toLowerCase().includes(c.toLowerCase()); return category.toLowerCase().includes(c.toString().toLowerCase());
}); });
return res.status(200).json(results); return res.status(200).json(results);
} }
// ✖ Error -> // ✖ Error ->
res.status(400).json({ info: 'Error: api query not found.' }); res.status(400).json({ info: "[/api/search] Error: api query not found." });
} }

View File

@ -0,0 +1,39 @@
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>
</>
);
}

28
src/pages/index.tsx Normal file
View File

@ -0,0 +1,28 @@
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;

29
src/pages/svg/[id].tsx Normal file
View File

@ -0,0 +1,29 @@
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>
</>
);
}

10
src/services/fetcher.ts Normal file
View File

@ -0,0 +1,10 @@
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();
};

6
src/services/index.ts Normal file
View File

@ -0,0 +1,6 @@
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=";

16
src/styles/globals.css Normal file
View File

@ -0,0 +1,16 @@
/* 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

@ -0,0 +1,44 @@
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,
};

44
src/theme/index.ts Normal file
View File

@ -0,0 +1,44 @@
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;

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