├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components ├── Features.tsx ├── Footer.tsx ├── Form.tsx ├── Header.tsx ├── Preview.tsx ├── TriggerButton.tsx └── Widget.tsx ├── lib ├── constants.ts ├── getIcon.ts ├── isURL.ts └── types.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].js │ └── widget.ts ├── dashboard.tsx ├── index.tsx └── widget │ ├── panel │ └── [slug].tsx │ └── trigger.tsx ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico ├── scripts │ └── embed.min.js └── vercel.svg ├── scripts └── embed.js ├── styles ├── globals.css └── nprogress.css ├── tailwind.config.js ├── tsconfig.json └── types └── next-auth.d.ts /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | GITHUB_CLIENT_ID= 3 | GITHUB_CLIENT_SECRET= 4 | GOOGLE_CLIENT_ID= 5 | GOOGLE_CLIENT_SECRET= 6 | NEXTAUTH_SECRET= 7 | NEXT_PUBLIC_CLOUDINARY_PRESET= 8 | NEXT_PUBLIC_CLOUDINARY_NAME= 9 | NEXTAUTH_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | /*.env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ashish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Ponsor 2 | getting sponsored made easy through widgets 3 | 4 | ### Tech Stack 5 | A lot of different tech and soft wares were used to create Ponsor, here is a list of the tech I used - 6 | - [Next.js](https://nextjs.org) - Framework 7 | - [PlanetScale](https://planetscale.com) - Database 8 | - [Tailwind CSS](https://tailwindcss.com) - Styling 9 | - [VS Code](https://code.visualstudio.com/) - Code Editor 10 | - [Next Auth](https://next-auth.js.org/) - Auth 11 | - [GitHub](https://github.com) - Code Base 12 | - [Prisma](https://prisma.io) - ORM 13 | - [Vercel](https://vercel.app) - Host 14 | - [Nprogress](https://ricostacruz.com/nprogress/) - Progress Bars 15 | - [React Hot Toast](https://react-hot-toast.com/) - Toast Notifications 16 | - [React Icons / Feather Icons](https://react-icons.github.io/react-icons/icons?name=fi) - Icons 17 | - [Favmoji](https://github.com/asrvd/favmoji) - Favicon as a service 18 | 19 | ### Run Locally 20 | - Clone the repository 21 | ```bash 22 | git clone https://github.com/asrvd/ponsor.git 23 | ``` 24 | - Install dependencies 25 | ```bash 26 | cd ponsor 27 | pnpm i # or npm i 28 | ``` 29 | - Create a `.env` file and put these env variables in it 30 | ```env 31 | DATABASE_URL= 32 | GITHUB_CLIENT_ID= 33 | GITHUB_CLIENT_SECRET= 34 | GOOGLE_CLIENT_ID= 35 | GOOGLE_CLIENT_SECRET= 36 | NEXTAUTH_SECRET= 37 | NEXT_PUBLIC_CLOUDINARY_PRESET= 38 | NEXT_PUBLIC_CLOUDINARY_NAME= 39 | NEXTAUTH_URL= 40 | ``` 41 | - Fire up prisma 42 | ```bash 43 | pnpm dlx prisma db push # or npx prisma db push 44 | ``` 45 | - Run the app 46 | ```bash 47 | pnpm run dev # or npm run dev 48 | ``` 49 | 50 | ### License 51 | [MIT License](LICENSE) 52 | 53 | ### Contributing 54 | - Fork the repository 55 | - Create a new branch 56 | - Make your changes 57 | - Commit your changes 58 | - Push your changes to the main branch 59 | - Open a pull request 60 | 61 | ### Ending Note 62 | - This project was made for the [hashnode](https://hashnode.com) x [planetscale](https://planetscale.com) hackathon. 63 | - If you have any questions, suggestions or bug reports please open an issue. 64 | - Leave a star if you like the project. 65 | - If you like this project, please consider [supporting](https://www.buymeacoffee.com/asheeshh) me. -------------------------------------------------------------------------------- /components/Features.tsx: -------------------------------------------------------------------------------- 1 | import { FiArrowRight } from "react-icons/fi"; 2 | import { useRouter } from "next/router"; 3 | import { features } from "../lib/constants"; 4 | 5 | type FeatureProps = { 6 | showButton?: boolean; 7 | }; 8 | 9 | type Feature = { 10 | title: string; 11 | description: string; 12 | icon: any; 13 | }; 14 | 15 | export default function Features(props: FeatureProps) { 16 | const router = useRouter(); 17 | 18 | return ( 19 |
20 |

21 | Features 22 |

23 |
24 | {features.map((feature) => ( 25 | <> 26 | {feature.map((f: Feature) => ( 27 |
31 |

32 | {f.icon()} {f.title} 33 |

34 |

35 | {f.description} 36 |

37 |
38 | ))} 39 | 40 | ))} 41 |
42 | {props.showButton && ( 43 | 49 | )} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 |

5 | made with {`<3`} by{" "} 6 | 12 | ashish 13 | 14 |

15 |

16 | for{" "} 17 | 23 | hashnode 24 | {" "} 25 | x{" "} 26 | 32 | planetscale 33 | {" "} 34 | hackathon 35 |

36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FiPlus, 3 | FiX, 4 | FiSave, 5 | FiChevronDown, 6 | FiCopy, 7 | FiCheck, 8 | } from "react-icons/fi"; 9 | import { useState } from "react"; 10 | import { isURL } from "../lib/isURL"; 11 | import toast from "react-hot-toast"; 12 | import { getIcon } from "../lib/getIcon"; 13 | import { memo } from "react"; 14 | import type { FormProps, DerivedLink, Slug } from "../lib/types"; 15 | import { sponsorLinks } from "../lib/constants"; 16 | 17 | export default memo(function PonsorForm({ ...props }: FormProps) { 18 | const [hasMadeChanges, setHasMadeChanges] = useState(false); 19 | const [copied, setCopied] = useState(false); 20 | const [addedLinkTypes, setAddedLinkTypes] = useState( 21 | props?.links?.map((link) => link.title) || [] 22 | ); 23 | const [addedLinks, setAddedLinks] = useState( 24 | props?.links?.map((link) => { 25 | return { 26 | url: link.url, 27 | title: link.title, 28 | icon: getIcon(link.type as Slug), 29 | type: link.type, 30 | }; 31 | }) || [] 32 | ); 33 | const [currentLinkType, setCurrentLinkType] = useState(""); 34 | const [currentLink, setCurrentLink] = useState(""); 35 | 36 | return ( 37 |
38 |
39 |
40 |
41 | 44 | { 48 | e.preventDefault(); 49 | setHasMadeChanges(true); 50 | props.uploadImage(e); 51 | }} 52 | > 53 |
54 |
55 | 58 | { 63 | e.preventDefault(); 64 | if (e.target.value !== props?.widget?.heading) { 65 | setHasMadeChanges(true); 66 | } 67 | props.setName(e.target.value); 68 | }} 69 | > 70 |
71 |
72 | 75 | { 80 | e.preventDefault(); 81 | if (e.target.value !== props?.widget?.heading) { 82 | setHasMadeChanges(true); 83 | } 84 | props.setHeading(e.target.value); 85 | }} 86 | > 87 |
88 |
89 | 92 |
93 | 116 | 132 |
133 |
134 | { 139 | e.preventDefault(); 140 | setCurrentLink(e.target.value); 141 | }} 142 | > 143 | 184 |
185 |
186 |
187 | 188 |
189 | Show Embed Code{" "} 190 | 191 | {" "} 192 | make sure you have saved your widget! 193 | 194 |
195 | 196 |
197 |
198 |               {``}
204 |               
222 |             
223 |
224 |
225 |
226 | {addedLinks.map((link) => ( 227 |
231 | 237 | {link?.icon()} 238 | {link?.title} 239 | 240 | 256 |
257 | ))} 258 |
259 |
260 |
261 | ); 262 | }); 263 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | import { signOut } from "next-auth/react"; 5 | import { useRouter } from "next/router"; 6 | import { motion } from "framer-motion"; 7 | import { FiHome, FiLogOut, FiSliders } from "react-icons/fi"; 8 | import { memo } from "react"; 9 | import type { HeaderProps } from "../lib/types"; 10 | 11 | export default memo(function Header(props: HeaderProps) { 12 | const router = useRouter(); 13 | const ref = useRef(); 14 | const [drawerOpen, setDrawerOpen] = useState(false); 15 | const currentLocation = router.pathname; 16 | 17 | useEffect(() => { 18 | const handleClickOutside = (event: any) => { 19 | if (ref.current && !ref.current?.contains(event.target)) { 20 | setDrawerOpen(false); 21 | } 22 | }; 23 | document.addEventListener("mousedown", handleClickOutside); 24 | return () => { 25 | document.removeEventListener("mousedown", handleClickOutside); 26 | }; 27 | }, [drawerOpen]); 28 | 29 | return ( 30 |
34 |

router.push("/")}> 35 | ponsor 36 |

37 |
{ 40 | setDrawerOpen(!drawerOpen); 41 | }} 42 | > 43 |
44 | {props?.name} 49 |

{props?.name}

50 |
51 | {drawerOpen && ( 52 | 62 | 69 | 78 | 79 | )} 80 |
81 |
82 | ); 83 | }); 84 | -------------------------------------------------------------------------------- /components/Preview.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | 3 | import { memo } from "react"; 4 | 5 | export default memo(function Preview(props: any) { 6 | return ( 7 |
8 |
9 |
10 | avatar 15 |

16 | Sponsor {props.name} 17 |

18 |

{props.heading}

19 |
20 |
21 | {props?.links?.map((link: any) => ( 22 | 36 | ))} 37 |
38 |
39 |

40 | made with {`<3`} using{" "} 41 | 47 | ponsor 48 | 49 |

50 |
51 |
52 |
53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /components/TriggerButton.tsx: -------------------------------------------------------------------------------- 1 | import { FiHeart } from "react-icons/fi"; 2 | 3 | export default function TriggerButton() { 4 | return ( 5 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /components/Widget.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | import { getIcon } from "../lib/getIcon"; 3 | import type { WidgetProps } from "../lib/types"; 4 | 5 | export default function WidgetComponent(props: WidgetProps) { 6 | return ( 7 |
8 |
9 |
10 | avatar 15 |

16 | Sponsor {props?.widget?.name} 17 |

18 |

19 | {props?.widget?.heading} 20 |

21 |
22 |
23 | {props?.links?.map((link: any) => ( 24 | 39 | ))} 40 |
41 |
42 |

43 | made with {`<3`} using{" "} 44 | 50 | ponsor 51 | 52 |

53 |
54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FiPenTool, 3 | FiSave, 4 | FiHeart, 5 | FiSliders, 6 | FiSmile, 7 | FiCopy, 8 | } from "react-icons/fi"; 9 | import { 10 | SiPatreon, 11 | SiGithubsponsors, 12 | SiKofi, 13 | SiBuymeacoffee, 14 | SiOpencollective, 15 | SiLiberapay, 16 | SiPaypal, 17 | SiGumroad, 18 | } from "react-icons/si"; 19 | 20 | export const features = [ 21 | [ 22 | { 23 | icon: FiPenTool, 24 | title: "Clean Widgets", 25 | description: "easy to make clean and beautiful widgets.", 26 | }, 27 | { 28 | icon: FiSave, 29 | title: "Auth Support", 30 | description: "login with github or google to never lose your widget.", 31 | }, 32 | { 33 | icon: FiSliders, 34 | title: "Customizable", 35 | description: "add upto 7 different sponsor methods to your widget.", 36 | }, 37 | ], 38 | [ 39 | { 40 | icon: FiCopy, 41 | title: "Embed Anywhere", 42 | description: "embed your widget on any site using script tags.", 43 | }, 44 | { 45 | icon: FiSmile, 46 | title: "Easy to Use", 47 | description: "creating the widget is as easy as filling a form.", 48 | }, 49 | { 50 | icon: FiHeart, 51 | title: "Free Forever", 52 | description: "ponsor is a completely free to use tool.", 53 | }, 54 | ], 55 | ]; 56 | 57 | export const sponsorLinks = [ 58 | { 59 | name: "GitHub Sponsor", 60 | icon: SiGithubsponsors, 61 | }, 62 | { 63 | name: "Buy me a coffee", 64 | icon: SiBuymeacoffee, 65 | }, 66 | { 67 | name: "Kofi", 68 | icon: SiKofi, 69 | }, 70 | { 71 | name: "Patreon", 72 | icon: SiPatreon, 73 | }, 74 | { 75 | name: "Open Collective", 76 | icon: SiOpencollective, 77 | }, 78 | { 79 | name: "Liberapay", 80 | icon: SiLiberapay, 81 | }, 82 | { 83 | name: "PayPal", 84 | icon: SiPaypal, 85 | }, 86 | { 87 | name: "Gumroad", 88 | icon: SiGumroad, 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /lib/getIcon.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SiPatreon, 3 | SiGithubsponsors, 4 | SiKofi, 5 | SiBuymeacoffee, 6 | SiOpencollective, 7 | SiLiberapay, 8 | SiPaypal, 9 | SiGumroad 10 | } from "react-icons/si"; 11 | import type { Slug } from "./types"; 12 | 13 | const slugToIcon = { 14 | patreon: SiPatreon, 15 | githubsponsor: SiGithubsponsors, 16 | kofi: SiKofi, 17 | buymeacoffee: SiBuymeacoffee, 18 | opencollective: SiOpencollective, 19 | liberapay: SiLiberapay, 20 | paypal: SiPaypal, 21 | gumroad: SiGumroad 22 | }; 23 | 24 | export function getIcon(slug: Slug) { 25 | return slugToIcon[slug]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/isURL.ts: -------------------------------------------------------------------------------- 1 | export function isURL(url: string): boolean { 2 | return /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test( 3 | url 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Widget, Link } from "@prisma/client"; 2 | import type { Session } from "next-auth"; 3 | 4 | export type Slug = 5 | | "patreon" 6 | | "githubsponsor" 7 | | "kofi" 8 | | "buymeacoffee" 9 | | "opencollective" 10 | | "liberapay" 11 | | "paypal" 12 | | "gumroad"; 13 | 14 | export type FormData = { 15 | name?: string; 16 | image?: string; 17 | rawImage?: any; 18 | previewImage?: any; 19 | heading?: string; 20 | links: any[]; 21 | }; 22 | 23 | export type DashboardProps = { 24 | session?: Session; 25 | widget?: Widget; 26 | links?: Link[]; 27 | }; 28 | 29 | export type DerivedLink = { 30 | url: string; 31 | title: string; 32 | type: string; 33 | icon: any; 34 | }; 35 | 36 | export type FormProps = { 37 | widget?: Widget; 38 | links?: DerivedLink[]; 39 | uploadImage: (e: any) => void; 40 | addLink: (link: DerivedLink) => void; 41 | removeLink: (linkName: string) => void; 42 | setHeading: (head: string) => void; 43 | setName: (name: string) => void; 44 | handleSave: () => void; 45 | }; 46 | 47 | export type HeaderProps = { 48 | name: string | any; 49 | image: string | any; 50 | }; 51 | 52 | export type WidgetProps = { 53 | widget?: Widget; 54 | links?: Link[]; 55 | }; 56 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spm", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "npm run build-script && npm run build-app", 8 | "build-app": "next build", 9 | "build-script": "uglifyjs ./scripts/embed.js --output ./public/scripts/embed.min.js", 10 | "start": "next start", 11 | "lint": "next lint" 12 | }, 13 | "dependencies": { 14 | "@next-auth/prisma-adapter": "^1.0.3", 15 | "@prisma/client": "^4.0.0", 16 | "axios": "^0.27.2", 17 | "framer-motion": "^6.5.1", 18 | "next": "12.2.0", 19 | "next-auth": "^4.9.0", 20 | "nprogress": "^0.2.0", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-hook-form": "^7.33.1", 24 | "react-hot-toast": "^2.2.0", 25 | "react-icons": "^4.4.0", 26 | "uglify-js": "^3.16.3" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "18.0.3", 30 | "@types/nprogress": "^0.2.0", 31 | "@types/react": "18.0.15", 32 | "@types/react-dom": "18.0.6", 33 | "autoprefixer": "^10.4.7", 34 | "eslint": "8.19.0", 35 | "eslint-config-next": "12.2.0", 36 | "postcss": "^8.4.14", 37 | "prisma": "^4.0.0", 38 | "tailwindcss": "^3.1.4", 39 | "typescript": "4.7.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import { Toaster } from "react-hot-toast"; 5 | import NProgress from "nprogress"; 6 | import { Router } from "next/router"; 7 | import "../styles/nprogress.css"; 8 | import Head from "next/head"; 9 | import Script from "next/script"; 10 | 11 | function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { 12 | NProgress.configure({ showSpinner: false }); 13 | Router.events.on("routeChangeStart", () => NProgress.start()); 14 | Router.events.on("routeChangeComplete", () => NProgress.done()); 15 | Router.events.on("routeChangeError", () => NProgress.done()); 16 | return ( 17 | 18 | 19 | ponsor 20 | 21 | 25 | 26 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default MyApp; 46 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import GoogleProvider from "next-auth/providers/google"; 4 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 5 | import { PrismaClient } from "@prisma/client"; 6 | 7 | const prisma = new PrismaClient(); 8 | 9 | export default NextAuth({ 10 | adapter: PrismaAdapter(prisma), 11 | providers: [ 12 | GithubProvider({ 13 | clientId: process.env.GITHUB_CLIENT_ID, 14 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 15 | }), 16 | GoogleProvider({ 17 | clientId: process.env.GOOGLE_CLIENT_ID, 18 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 19 | }), 20 | ], 21 | callbacks: { 22 | async session({ session, token, user }) { 23 | session = { 24 | ...session, 25 | user: { 26 | id: user.id, 27 | ...session.user, 28 | }, 29 | }; 30 | return session; 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /pages/api/widget.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { getSession } from "next-auth/react"; 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | import type { NextApiRequest, NextApiResponse } from "next"; 7 | 8 | export type Link = { 9 | url: string; 10 | title: string; 11 | type: string; 12 | }; 13 | 14 | export default async function handler( 15 | req: NextApiRequest, 16 | res: NextApiResponse 17 | ) { 18 | const session = await getSession({ req }); 19 | const { name, heading, avatar, links } = req.body; 20 | if (!session) { 21 | return res.status(401).json({ message: "Not logged in" }); 22 | } 23 | try { 24 | await prisma.user.update({ 25 | where: { 26 | id: session.user.id, 27 | }, 28 | data: { 29 | widget: { 30 | upsert: { 31 | create: { 32 | name, 33 | heading, 34 | avatar, 35 | links: { 36 | create: links, 37 | }, 38 | }, 39 | update: { 40 | name, 41 | heading, 42 | avatar, 43 | links: { 44 | deleteMany: {}, 45 | create: links, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }); 52 | res.status(200).json({ message: "Widget created" }); 53 | } catch (error) { 54 | console.log(error); 55 | res.status(500).json({ message: "Something went wrong" }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useSession, getSession } from "next-auth/react"; 2 | import type { Session } from "next-auth"; 3 | import { GetServerSideProps } from "next"; 4 | import Header from "../components/Header"; 5 | import Preview from "../components/Preview"; 6 | import PonsorForm from "../components/Form"; 7 | import { useState } from "react"; 8 | import axios from "axios"; 9 | import { PrismaClient } from "@prisma/client"; 10 | import { getIcon } from "../lib/getIcon"; 11 | import toast from "react-hot-toast"; 12 | import Footer from "../components/Footer"; 13 | import type { FormData, DashboardProps, Slug } from "../lib/types"; 14 | 15 | const prisma = new PrismaClient(); 16 | 17 | export default function Dashboard(props: DashboardProps) { 18 | const { data: session, status } = useSession(); 19 | const [formData, setFormData] = useState({ 20 | name: props?.widget?.name 21 | ? props?.widget?.name 22 | : (session?.user?.name as string), 23 | image: props?.widget?.avatar 24 | ? props?.widget?.avatar 25 | : (session?.user?.image as string), 26 | previewImage: props?.widget?.avatar 27 | ? props?.widget?.avatar 28 | : (session?.user?.image as string), 29 | rawImage: null, 30 | heading: props?.widget?.heading 31 | ? props?.widget?.heading 32 | : "loved the project? help me create more!", 33 | links: props?.links 34 | ? props?.links.map((link) => { 35 | return { 36 | url: link.url, 37 | title: link.title, 38 | icon: getIcon(link.type as Slug), 39 | type: link.type, 40 | }; 41 | }) 42 | : [], 43 | }); 44 | 45 | let imageURL: string | null = null; 46 | 47 | const handleSave = async () => { 48 | if (formData.rawImage) { 49 | imageURL = await uploadImage(); 50 | } 51 | 52 | let toastId = toast.loading("Saving..."); 53 | 54 | const res = await axios.post("/api/widget", { 55 | name: formData.name, 56 | heading: formData.heading, 57 | avatar: imageURL !== null ? imageURL : formData.image, 58 | links: formData.links.map((link) => { 59 | return { 60 | url: link.url, 61 | title: link.title, 62 | type: link.type, 63 | }; 64 | }), 65 | }); 66 | 67 | if (res.status === 200) { 68 | toast.success("Saved!", { 69 | id: toastId, 70 | }); 71 | 72 | setTimeout(() => { 73 | window.location.reload(); 74 | }, 1000); 75 | } else { 76 | toast.error("Error saving! Try Again!", { 77 | id: toastId, 78 | }); 79 | } 80 | }; 81 | 82 | const uploadImage = async () => { 83 | const fileData = new FormData(); 84 | fileData.append("file", formData.rawImage); 85 | fileData.append( 86 | "upload_preset", 87 | process.env.NEXT_PUBLIC_CLOUDINARY_PRESET as string 88 | ); 89 | try { 90 | return axios 91 | .post( 92 | `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_NAME}/image/upload`, 93 | fileData 94 | ) 95 | .then((res) => { 96 | return res.data.secure_url; 97 | }); 98 | } catch (err) { 99 | console.log(err); 100 | return null; 101 | } 102 | }; 103 | 104 | return ( 105 |
106 | {session?.user ? ( 107 |
108 |
109 |
117 |
118 |
119 | 130 | 131 | { 134 | return { 135 | url: link.url, 136 | title: link.title, 137 | type: link.type, 138 | icon: getIcon(link.type as Slug), 139 | }; 140 | })} 141 | uploadImage={(e: any) => { 142 | e.preventDefault(); 143 | setFormData({ 144 | ...formData, 145 | previewImage: URL.createObjectURL(e.target.files[0]), 146 | rawImage: e.target.files[0], 147 | }); 148 | }} 149 | addLink={(link: any) => 150 | setFormData({ ...formData, links: [...formData.links, link] }) 151 | } 152 | removeLink={(linkName: string) => { 153 | setFormData({ 154 | ...formData, 155 | links: formData.links.filter( 156 | (link: any) => link.title !== linkName // here 157 | ), 158 | }); 159 | }} 160 | setHeading={(head: string) => { 161 | setFormData({ ...formData, heading: head }); 162 | }} 163 | setName={(name: string) => { 164 | setFormData({ ...formData, name: name }); 165 | }} 166 | handleSave={handleSave} 167 | /> 168 |
169 |
170 | ) : ( 171 |

not signed in

172 | )} 173 |
174 |
175 | ); 176 | } 177 | 178 | export const getServerSideProps: GetServerSideProps<{ 179 | session: Session | null; 180 | }> = async (context) => { 181 | const sess = await getSession(context); 182 | 183 | if (!sess) { 184 | return { 185 | redirect: { 186 | destination: "/", 187 | permanent: false, 188 | }, 189 | }; 190 | } 191 | 192 | const widget = await prisma.widget.findUnique({ 193 | where: { 194 | userId: sess.user.id, 195 | }, 196 | }); 197 | 198 | if (widget) { 199 | const links = await prisma.link.findMany({ 200 | where: { 201 | widgetId: widget.id, 202 | }, 203 | }); 204 | return { 205 | props: { 206 | session: sess, 207 | widget: widget, 208 | links: links, 209 | }, 210 | }; 211 | } 212 | 213 | return { 214 | props: { 215 | session: sess, 216 | }, 217 | }; 218 | }; 219 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useSession, signIn, signOut } from "next-auth/react"; 3 | import type { Session } from "next-auth"; 4 | import { useRouter } from "next/router"; 5 | import GitHub from "next-auth/providers/github"; 6 | import Header from "../components/Header"; 7 | import Script from "next/script"; 8 | import { FiArrowRight, FiArrowDown } from "react-icons/fi"; 9 | import { SiGoogle, SiGithub } from "react-icons/si"; 10 | import Features from "../components/Features"; 11 | import Footer from "../components/Footer"; 12 | 13 | const Home: NextPage = () => { 14 | const { data: session } = useSession(); 15 | const router = useRouter(); 16 | if (session?.user) { 17 | return ( 18 |
19 |
20 |
21 |
22 |
23 |

24 | ponsor 25 |

26 |

27 | use widgets in your websites to let people find a way to support 28 | your work! 29 |

30 | 36 |

37 | 38 |

39 |
40 |
41 | 46 |
47 | 48 |