├── .env.example ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico ├── manifest.json ├── robots.txt └── static │ ├── fonts │ └── Roboto-Regular.ttf │ └── images │ ├── next.svg │ └── vercel.svg ├── src ├── app │ ├── (marketing) │ │ ├── _PageSections │ │ │ ├── CTA.tsx │ │ │ ├── Feature.tsx │ │ │ ├── FeatureList.tsx │ │ │ ├── Header.tsx │ │ │ ├── Hero.tsx │ │ │ ├── LogoCloud.tsx │ │ │ └── NavBar.tsx │ │ ├── faq │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── pricing │ │ │ └── page.tsx │ ├── api │ │ ├── auth-callback │ │ │ └── route.ts │ │ ├── readme.md │ │ └── stripe │ │ │ └── webhook │ │ │ └── route.ts │ ├── auth │ │ ├── auth-required │ │ │ └── page.tsx │ │ ├── confirm │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── forgot-password │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── login │ │ │ └── page.tsx │ │ ├── magic-link │ │ │ └── page.tsx │ │ └── signup │ │ │ └── page.tsx │ ├── dashboard │ │ ├── _PageSections │ │ │ ├── DocShare.tsx │ │ │ ├── Header.tsx │ │ │ ├── RecentSales.tsx │ │ │ ├── SideBar.tsx │ │ │ ├── SidebarNav.tsx │ │ │ ├── TeamSwitcher.tsx │ │ │ ├── UserNav.tsx │ │ │ └── charts │ │ │ │ ├── Bar.tsx │ │ │ │ ├── Compose.tsx │ │ │ │ └── Pie.tsx │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── main │ │ │ ├── _PageSections │ │ │ │ ├── Dashboard.tsx │ │ │ │ └── SummaryCard.tsx │ │ │ └── page.tsx │ │ ├── settings │ │ │ ├── _PageSections │ │ │ │ ├── Billing.tsx │ │ │ │ ├── SettingsHeader.tsx │ │ │ │ ├── SettingsNav.tsx │ │ │ │ ├── Subscription.tsx │ │ │ │ ├── UpdateForms.tsx │ │ │ │ └── UpdateProfileCard.tsx │ │ │ ├── add-subscription │ │ │ │ └── page.tsx │ │ │ ├── billing │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── profile │ │ │ │ └── page.tsx │ │ │ ├── subscription-required │ │ │ │ └── page.tsx │ │ │ └── subscription │ │ │ │ └── page.tsx │ │ └── todos │ │ │ ├── _PageSections │ │ │ ├── MyTodos.tsx │ │ │ ├── TodosCreateForm.tsx │ │ │ ├── TodosEditForm.tsx │ │ │ ├── TodosHeader.tsx │ │ │ ├── TodosList.tsx │ │ │ └── TodosNav.tsx │ │ │ ├── create │ │ │ └── page.tsx │ │ │ ├── edit │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── list-todos │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ └── my-todos │ │ │ └── page.tsx │ ├── global-error.tsx │ ├── layout.tsx │ ├── loading.tsx │ └── not-found.tsx ├── components │ ├── ErrorText.tsx │ ├── Footer.tsx │ ├── Form.tsx │ ├── Icons.tsx │ ├── MainLogo.tsx │ ├── MobileNav.tsx │ ├── ThemeDropdown.tsx │ ├── readme.md │ └── ui │ │ ├── Avatar.tsx │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── Command.tsx │ │ ├── Dialog.tsx │ │ ├── DropdownMenu.tsx │ │ ├── Input.tsx │ │ ├── Label.tsx │ │ ├── Navigation.tsx │ │ ├── Popover.tsx │ │ ├── Select.tsx │ │ ├── Separator.tsx │ │ ├── Switch.tsx │ │ ├── Table.tsx │ │ ├── Tabs.tsx │ │ └── Textarea.tsx ├── lib │ ├── API │ │ ├── Database │ │ │ ├── profile │ │ │ │ ├── mutations.ts │ │ │ │ └── queries.ts │ │ │ ├── subcription │ │ │ │ └── queries.ts │ │ │ └── todos │ │ │ │ ├── mutations.ts │ │ │ │ └── queries.ts │ │ └── Services │ │ │ ├── init │ │ │ ├── stripe.ts │ │ │ └── supabase.ts │ │ │ ├── stripe │ │ │ ├── customer.ts │ │ │ ├── session.ts │ │ │ └── webhook.ts │ │ │ └── supabase │ │ │ ├── auth.ts │ │ │ └── user.ts │ ├── config │ │ ├── api.ts │ │ ├── auth.ts │ │ ├── dashboard.ts │ │ ├── marketing.ts │ │ └── site.ts │ ├── types │ │ ├── enums.ts │ │ ├── readme.md │ │ ├── stripe.ts │ │ ├── supabase.ts │ │ ├── todos.ts │ │ ├── types.ts │ │ └── validations.ts │ └── utils │ │ ├── error.ts │ │ ├── helpers.ts │ │ └── hooks.ts └── styles │ ├── ThemeProvider.tsx │ ├── fonts.ts │ └── globals.css ├── supabase ├── .gitignore ├── config.toml ├── functions │ └── .vscode │ │ └── settings.json ├── migrations │ └── 20230927195226_remote_schema.sql ├── seed.sql └── types.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # App 2 | NEXT_PUBLIC_DOMAIN='http://localhost:3000' 3 | NEXT_PUBLIC_ENVIRONMENT= 4 | 5 | # Supabase connection 6 | NEXT_PUBLIC_SUPABASE_URL=Refer to project Settings -> API to get your Project URL 7 | NEXT_PUBLIC_SUPABASE_ANON_KEY=Refer to Project Setting -> API to get your Project API Keys 8 | 9 | # Stripe env variable 10 | NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC_MONTHLY= 11 | NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC_YEARLY= 12 | NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM_MONTHLY= 13 | NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM_YEARLY= 14 | 15 | # Stripe Secrets, Do Not Prefix with NEXT_PUBLIC_ 16 | STRIPE_SECRET_KEY= 17 | 18 | # substitute with Stripe CLI or Stripe dashboard webhook secret for ngrok 19 | STRIPE_WEBHOOK_SECRET= 20 | 21 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.7.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | public -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "semiColons": true, 5 | "trailingComma": "none", 6 | "jsxSingleQuote": false 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to SAAS Starter Kit! 2 |

3 |

4 | 5 |

6 |
7 |
8 | 9 | > Saas Starter Kit is a modern SAAS boilerplate. Save weeks of development time having standard SAAS features implemented for you, and start building your core app right away. 10 | 11 | ## 🎛 Tech Stack 12 | 13 | Reactjs, Nextjs, Typescript, Tailwind, Shadcn, Stripe, Supabase 14 | 15 | ## 🧿 Saas Starterkit Pro 16 | 17 |

**Note: Saas Starterkit Pro uses Prisma instead of Supabase

18 | 19 | Saas Starterkit also comes in a Pro version. Get premium marketing pages, multi-tenancy, roles and permissions, team invites, enhanced subscriptions with Lemon Squeezy, and more check it out here: 20 |
21 |
22 | https://www.saasstarterkit.com/ 23 |
24 | 25 | ## ✨ Features 26 | 27 | - ✅ Admin Dashboard 28 | - ✅ Full Authentication, with Google Social Login 29 | - ✅ User Profile Management with Email and Username change 30 | - ✅ User Dashboard 31 | - ✅ Checkout Pages 32 | - ✅ Landing and Pricing Page template 33 | - ✅ Testing Setup with Playwright 34 | - ✅ CRUD operations 35 | - ✅ Stripe subscription payments 36 | - ✅ Lemon Squeezy MoR (Pro version) 37 | - ✅ Roles and permissions (Pro version) 38 | - ✅ Team Invites (Pro version) 39 | - ✅ Multi user apps and multi tenancy (Pro version) 40 | - ✅ Fully Featured Blog (Pro version) 41 | - ✅ Event Based Analytics (Pro version) 42 | 43 | ## 📜 Docs 44 | 45 | The Documentation is available here: 46 |
47 | https://www.saasstarterkit.com/docs 48 | 49 | If there are any questions or something is not covered in the docs, feel free to open a github issue on this repo. 50 | 51 | ## 💻 Demo 52 | 53 | The Demo can be found here: 54 |
55 | https://www.saasstarterkit.com/dashboard/test243/main 56 | 57 | Certain Features have to be disabled or cant be included in the demo. 58 | 59 | ## 🤝 Contributing 60 | 61 | Pull requests are welcome. 62 | 63 | Also If you like this project please ⭐️ the repo to show your support. 64 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | remotePatterns: [ 4 | { 5 | protocol: 'https', 6 | hostname: 'tailwindui.com' 7 | } 8 | ] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "e2e": "start-server-and-test dev http://localhost:3000 \"cypress open --e2e\"", 11 | "e2e:headless": "start-server-and-test dev http://localhost:3000 \"cypress run --e2e\"", 12 | "test": "jest", 13 | "stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe/webhook", 14 | "ngrok": "ngrok http 3000" 15 | }, 16 | "dependencies": { 17 | "@adobe/css-tools": "^4.3.3", 18 | "@hookform/resolvers": "^3.3.1", 19 | "@radix-ui/react-avatar": "^1.0.4", 20 | "@radix-ui/react-dialog": "^1.0.4", 21 | "@radix-ui/react-dropdown-menu": "^2.0.5", 22 | "@radix-ui/react-icons": "^1.3.0", 23 | "@radix-ui/react-label": "^2.0.2", 24 | "@radix-ui/react-navigation-menu": "^1.1.4", 25 | "@radix-ui/react-popover": "^1.0.6", 26 | "@radix-ui/react-select": "^1.2.2", 27 | "@radix-ui/react-separator": "^1.0.3", 28 | "@radix-ui/react-slot": "^1.0.2", 29 | "@radix-ui/react-switch": "^1.0.3", 30 | "@radix-ui/react-tabs": "^1.0.4", 31 | "@stripe/stripe-js": "^2.1.2", 32 | "@supabase/auth-helpers-nextjs": "^0.7.4", 33 | "@supabase/supabase-js": "^2.33.1", 34 | "@tailwindcss/typography": "^0.5.10", 35 | "@types/node": "20.5.7", 36 | "@types/react": "18.2.21", 37 | "@types/react-dom": "18.2.7", 38 | "autoprefixer": "10.4.15", 39 | "axios": "^1.6.7", 40 | "class-variance-authority": "^0.7.0", 41 | "client-only": "^0.0.1", 42 | "clsx": "^2.0.0", 43 | "cmdk": "^0.2.0", 44 | "eslint": "8.48.0", 45 | "eslint-config-next": "13.4.19", 46 | "lucide-react": "^0.279.0", 47 | "next": "^14.1.0", 48 | "next-themes": "^0.2.1", 49 | "nextjs-toploader": "^1.4.2", 50 | "postcss": "^8.4.33", 51 | "react": "18.2.0", 52 | "react-countup": "^6.4.2", 53 | "react-dom": "18.2.0", 54 | "react-hook-form": "^7.46.1", 55 | "react-toastify": "^9.1.3", 56 | "recharts": "^2.8.0", 57 | "server-only": "^0.0.1", 58 | "stripe": "^13.4.0", 59 | "swr": "^2.2.4", 60 | "tailwind-merge": "^1.14.0", 61 | "tailwindcss": "3.3.3", 62 | "tailwindcss-animate": "^1.0.7", 63 | "typescript": "5.2.2", 64 | "zod": "^3.22.4" 65 | }, 66 | "devDependencies": { 67 | "@testing-library/jest-dom": "^6.1.2", 68 | "@testing-library/react": "^14.0.0", 69 | "cypress": "^13.0.0", 70 | "encoding": "^0.1.13", 71 | "jest": "^29.6.4", 72 | "jest-environment-jsdom": "^29.6.4", 73 | "prettier": "^3.0.3", 74 | "prettier-plugin-tailwindcss": "^0.5.4", 75 | "supabase": "^1.99.5" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier-plugin-tailwindcss'] 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Saas-Starter-Kit/Saas-Kit-supabase/dd3c49cb3697edb90a5b59c8fe04d8af216bfcea/public/favicon.ico -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Saas-Starter-Kit/Saas-Kit-supabase/dd3c49cb3697edb90a5b59c8fe04d8af216bfcea/public/manifest.json -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Saas-Starter-Kit/Saas-Kit-supabase/dd3c49cb3697edb90a5b59c8fe04d8af216bfcea/public/robots.txt -------------------------------------------------------------------------------- /public/static/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Saas-Starter-Kit/Saas-Kit-supabase/dd3c49cb3697edb90a5b59c8fe04d8af216bfcea/public/static/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /public/static/images/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/images/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/CTA.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from '@/components/ui/Button'; 2 | import Link from 'next/link'; 3 | import { cn } from '@/lib/utils/helpers'; 4 | 5 | export default function CTA() { 6 | return ( 7 |
8 |
9 |
10 |

11 | Boost your productivity. 12 |
13 | Start using our app today. 14 |

15 |

16 | Incididunt sint fugiat pariatur cupidatat consectetur sit cillum anim id veniam aliqua 17 | proident excepteur commodo do ea. 18 |

19 |
20 | 21 | See Pricing 22 | 23 | 29 | Learn More 30 | 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/Feature.tsx: -------------------------------------------------------------------------------- 1 | import { Lock, CloudIcon } from 'lucide-react'; 2 | import Image from 'next/image'; 3 | 4 | const features = [ 5 | { 6 | name: 'Push to deploy.', 7 | description: 8 | 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis suscipit eaque, iste dolor cupiditate blanditiis ratione.', 9 | icon: CloudIcon 10 | }, 11 | { 12 | name: 'SSL certificates.', 13 | description: 14 | 'Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat commodo.', 15 | icon: Lock 16 | } 17 | ]; 18 | 19 | interface FeaturePropsI { 20 | isFlipped?: boolean; 21 | } 22 | 23 | const FeatureText = ({ isFlipped }: FeaturePropsI) => { 24 | return ( 25 |
26 |
27 |

Deploy faster

28 |

A better workflow

29 |

30 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores impedit perferendis 31 | suscipit eaque, iste dolor cupiditate blanditiis ratione. 32 |

33 |
34 | {features.map((feature) => ( 35 |
36 |
37 | 38 | {feature.name} 39 |
{' '} 40 |
{feature.description}
41 |
42 | ))} 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | const FeatureImage = () => { 50 | return ( 51 |
52 | Product screenshot 58 |
59 | ); 60 | }; 61 | 62 | export default function Feature({ isFlipped }: FeaturePropsI) { 63 | return ( 64 |
65 |
66 |
67 | 68 | 69 |
70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/FeatureList.tsx: -------------------------------------------------------------------------------- 1 | import { CloudCog, Camera, Clock2, Code2, DownloadCloudIcon, GitFork } from 'lucide-react'; 2 | 3 | interface FeatureCardI { 4 | heading: string; 5 | description: string; 6 | icon: React.ReactNode; 7 | } 8 | 9 | const FeatureCard = ({ heading, description, icon }: FeatureCardI) => { 10 | return ( 11 |
12 |
13 |
14 | {icon} 15 |

{heading}

16 |

{description}

17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default async function FeatureList() { 24 | return ( 25 |
26 |
27 |

Features

28 |

29 | This project is an experiment to see how a modern app, with features like auth, 30 | subscriptions, API routes, and static pages would work in Next.js 13 app dir. 31 |

32 |
33 |
34 | } 38 | /> 39 | } 43 | /> 44 | } 48 | /> 49 | } 53 | /> 54 | } 58 | /> 59 | } 63 | /> 64 |
65 |
66 |

67 | Taxonomy also includes a blog and a full-featured documentation site built using 68 | Contentlayer and MDX. 69 |

70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/Header.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from '@/components/ui/Button'; 2 | import Link from 'next/link'; 3 | import { cn } from '@/lib/utils/helpers'; 4 | import { Nav } from './NavBar'; 5 | import config from '@/lib/config/marketing'; 6 | import { MainLogoText } from '@/components/MainLogo'; 7 | import { ThemeDropDownMenu } from '../../../components/ThemeDropdown'; 8 | import { SupabaseSession } from '@/lib/API/Services/supabase/user'; 9 | 10 | export const Header = async () => { 11 | const { routes } = config; 12 | const { data } = await SupabaseSession(); 13 | 14 | return ( 15 |
16 |
17 | 18 |
40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/Hero.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils/helpers'; 2 | import Link from 'next/link'; 3 | import { buttonVariants } from '@/components/ui/Button'; 4 | import Image from 'next/image'; 5 | 6 | const HeroScreenshot = () => { 7 | return ( 8 |
9 | App screenshot 16 |
17 | ); 18 | }; 19 | 20 | export default function Hero() { 21 | return ( 22 |
23 |
24 |
25 |

26 | An example app built using Next.js 13 server components. 27 |

28 |

29 | I'm building a web app with Next.js 13 and open sourcing everything. Follow along 30 | as we figure this out together. 31 |

32 |
33 | 34 | Get Started 35 | 36 | 42 | Learn More 43 | 44 |
45 |
46 |
47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/LogoCloud.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Anchor, Bird, Carrot, Citrus, Factory } from 'lucide-react'; 2 | 3 | export default function LogoCloud() { 4 | return ( 5 |
6 |
7 |

8 | Trusted by the world’s most innovative teams 9 |

10 |
11 |
12 | 13 | Cube 14 |
15 |
16 | 17 | Anchor 18 |
19 |
20 | 21 | Bird 22 |
23 |
24 | 25 | Carrot 26 |
27 |
28 | 29 | Citrus 30 |
31 |
32 | 33 | Factory 34 |
35 |
36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/app/(marketing)/_PageSections/NavBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import * as React from 'react'; 3 | 4 | import { 5 | NavigationMenu, 6 | NavigationMenuItem, 7 | NavigationMenuLink, 8 | NavigationMenuList, 9 | navigationMenuTriggerStyle 10 | } from '@/components/ui/Navigation'; 11 | 12 | import Link from 'next/link'; 13 | 14 | import { MobileNav, NavProps } from '@/components/MobileNav'; 15 | 16 | export const Nav = ({ items }: NavProps) => { 17 | return ( 18 |
19 | 20 | 21 | {items.map((item) => ( 22 | 23 | 24 | 25 | {item.title} 26 | 27 | 28 | 29 | ))} 30 | 31 | 32 | 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/(marketing)/faq/page.tsx: -------------------------------------------------------------------------------- 1 | // simple FAQ list 2 | 3 | const faqs = [ 4 | { question: 'ssssssssssss', answer: 'eeeeeeeeeeeeeeee' }, 5 | { question: 'ssssssssssss', answer: 'eeeeeeeeeeeeeeee' } 6 | ]; 7 | interface FaqPropsI { 8 | question: string; 9 | answer: string; 10 | } 11 | 12 | const Faq = ({ question, answer }: FaqPropsI) => { 13 | return ( 14 |
15 |
16 | {question} 17 |
18 |
{answer}
19 |
20 | ); 21 | }; 22 | 23 | export default function FAQs() { 24 | return ( 25 |
26 | {/*
27 |
28 | {question} 29 |
30 |
{answer}
31 |
*/} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from './_PageSections/Header'; 2 | import { LayoutProps } from '@/lib/types/types'; 3 | import Footer from '@/components/Footer'; 4 | 5 | export default async function MarketingLayout({ children }: LayoutProps) { 6 | return ( 7 |
8 |
9 |
{children}
10 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Hero from './_PageSections/Hero'; 4 | import FeatureList from './_PageSections/FeatureList'; 5 | import Feature from './_PageSections/Feature'; 6 | import LogoCloud from './_PageSections/LogoCloud'; 7 | import CTA from './_PageSections/CTA'; 8 | // have links to FAQ 9 | 10 | // link to pricing in CTA 11 | 12 | export default function Landing() { 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/api/auth-callback/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 3 | import { cookies } from 'next/headers'; 4 | import { Database } from '../../../../supabase/types'; 5 | 6 | import type { NextRequest } from 'next/server'; 7 | import config from '@/lib/config/auth'; 8 | 9 | export async function GET(request: NextRequest) { 10 | const requestUrl = new URL(request.url); 11 | const code = requestUrl.searchParams.get('code'); 12 | const origin: string = request.nextUrl.origin; 13 | 14 | if (code) { 15 | const cookieStore = cookies(); 16 | const supabase = createRouteHandlerClient({ cookies: () => cookieStore }); 17 | 18 | const { error } = await supabase.auth.exchangeCodeForSession(code); 19 | if (error) throw error; 20 | } 21 | 22 | const reDirectUrl = `${origin}${config.redirects.toDashboard}`; 23 | 24 | // URL to redirect to after sign in process completes 25 | return NextResponse.redirect(reDirectUrl); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/api/readme.md: -------------------------------------------------------------------------------- 1 | unlike the older pages router. /api has no special meaning in app router. A route is instead created by naming a file route.ts. 2 | 3 | /api is only used for logically organizing /api requests using the special route handler file. 4 | -------------------------------------------------------------------------------- /src/app/api/stripe/webhook/route.ts: -------------------------------------------------------------------------------- 1 | import stripe from '@/lib/API/Services/init/stripe'; 2 | import { NextResponse } from 'next/server'; 3 | import { headers } from 'next/headers'; 4 | import { WebhookEventHandler } from '@/lib/API/Services/stripe/webhook'; 5 | import type { NextRequest } from 'next/server'; 6 | import { StripeEvent } from '@/lib/types/stripe'; 7 | 8 | export async function POST(req: NextRequest) { 9 | const body = await req.text(); 10 | const sig = headers().get('Stripe-Signature'); 11 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; 12 | 13 | if (!sig || !webhookSecret) return; 14 | const event: StripeEvent = stripe.webhooks.constructEvent(body, sig, webhookSecret); 15 | 16 | try { 17 | await WebhookEventHandler(event); 18 | return NextResponse.json({ received: true }, { status: 200 }); 19 | } catch (err) { 20 | return NextResponse.json({ error: err.message }, { status: 500 }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/auth/auth-required/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { Button } from '@/components/ui/Button'; 5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 6 | 7 | const AuthRequired = () => { 8 | const router = useRouter(); 9 | 10 | return ( 11 | 12 | 13 | Authentication Required 14 | Please Sign in to view this page. 15 | 16 | 17 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default AuthRequired; 26 | -------------------------------------------------------------------------------- /src/app/auth/confirm/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { Button } from '@/components/ui/Button'; 5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 6 | 7 | const AuthConfirm = () => { 8 | const router = useRouter(); 9 | 10 | return ( 11 | 12 | 13 | Request Submitted 14 | Please check your email 15 | 16 | 17 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default AuthConfirm; 26 | -------------------------------------------------------------------------------- /src/app/auth/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Card, CardHeader, CardDescription, CardContent, CardTitle } from '@/components/ui/Card'; 3 | import { Button } from '@/components/ui/Button'; 4 | import config from '@/lib/config/api'; 5 | 6 | export default function Error({ error, reset }: { error: Error; reset: () => void }) { 7 | console.log('Error', error); 8 | 9 | return ( 10 |
11 |
12 | 13 | 14 | {config.errorMessageGeneral} 15 | Click Below to Try Again 16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { SupabaseResetPasswordEmail } from '@/lib/API/Services/supabase/auth'; 4 | import { zodResolver } from '@hookform/resolvers/zod'; 5 | import { EmailFormSchema, EmailFormValues } from '@/lib/types/validations'; 6 | import { useForm } from 'react-hook-form'; 7 | import { Button } from '@/components/ui/Button'; 8 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/Form'; 9 | import { Input } from '@/components/ui/Input'; 10 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 11 | import { useRouter } from 'next/navigation'; 12 | import config from '@/lib/config/auth'; 13 | import { Icons } from '@/components/Icons'; 14 | 15 | export default function ForgotPassword() { 16 | const router = useRouter(); 17 | const form = useForm({ 18 | resolver: zodResolver(EmailFormSchema) 19 | }); 20 | 21 | const { 22 | setError, 23 | register, 24 | formState: { isSubmitting } 25 | } = form; 26 | 27 | const onSubmit = async (values: EmailFormValues) => { 28 | const { error } = await SupabaseResetPasswordEmail(values.email); 29 | 30 | if (error) { 31 | setError('email', { 32 | type: '"root.serverError', 33 | message: error.message 34 | }); 35 | return; 36 | } 37 | 38 | router.push(config.redirects.authConfirm); 39 | }; 40 | 41 | return ( 42 |
43 | 44 | 45 | Forgot Password 46 | 47 | Enter your email below to receive a link to reset your password. 48 | 49 | 50 | 51 | 52 |
53 | 54 | ( 58 | 59 | Email 60 | 61 | 62 | 63 | 64 | 65 | )} 66 | /> 67 | 71 | 72 | 73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { MainLogoText, MainLogoIcon } from '@/components/MainLogo'; 2 | import { Separator } from '@/components/ui/Separator'; 3 | import { LayoutProps } from '@/lib/types/types'; 4 | import { ThemeDropDownMenu } from '../../components/ThemeDropdown'; 5 | import { redirect } from 'next/navigation'; 6 | import config from '@/lib/config/auth'; 7 | import { SupabaseSession } from '@/lib/API/Services/supabase/user'; 8 | 9 | export default async function AuthLayout({ children }: LayoutProps) { 10 | const { data } = await SupabaseSession(); 11 | 12 | // Reverse Auth Guard 13 | if (data?.session) { 14 | redirect(config.redirects.toDashboard); 15 | } 16 | 17 | return ( 18 |
19 |
20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 |
{children}
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/auth/magic-link/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useForm } from 'react-hook-form'; 6 | import { zodResolver } from '@hookform/resolvers/zod'; 7 | import { 8 | EmailFormSchema, 9 | EmailFormValues, 10 | authFormSchema, 11 | authFormValues 12 | } from '@/lib/types/validations'; 13 | import { SupabaseSignInWithMagicLink } from '@/lib/API/Services/supabase/auth'; 14 | import { Button } from '@/components/ui/Button'; 15 | import { Input } from '@/components/ui/Input'; 16 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/Form'; 17 | import { 18 | Card, 19 | CardContent, 20 | CardDescription, 21 | CardFooter, 22 | CardHeader, 23 | CardTitle 24 | } from '@/components/ui/Card'; 25 | import { Icons } from '@/components/Icons'; 26 | import Link from 'next/link'; 27 | import config from '@/lib/config/auth'; 28 | 29 | export default function MagicLink() { 30 | const [errorMessage, setErrorMessage] = useState(''); 31 | 32 | const router = useRouter(); 33 | 34 | const form = useForm({ 35 | resolver: zodResolver(EmailFormSchema) 36 | }); 37 | 38 | const onSubmit = async (values: EmailFormValues) => { 39 | const { error } = await SupabaseSignInWithMagicLink(values.email); 40 | 41 | if (error) { 42 | setErrorMessage(error.message); 43 | return; 44 | } 45 | router.push(config.redirects.authConfirm); 46 | }; 47 | 48 | const { 49 | register, 50 | formState: { isSubmitting, errors } 51 | } = form; 52 | 53 | return ( 54 |
55 | 56 | 57 | Email Link to Login 58 | 59 | Enter your email below to receive a clickable link to login. 60 | 61 | {errors &&
{errorMessage}
} 62 |
63 | 64 | 65 |
66 | 67 | ( 71 | 72 | Email 73 | 74 | 75 | 76 | 77 | 78 | )} 79 | /> 80 | 85 | 86 | 87 |
88 | 89 |
90 |
91 | 92 | Back to login page 93 | 94 |
95 |
96 |
97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/DocShare.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Avatar, AvatarFallback } from '@/components/ui/Avatar'; 4 | import { Button } from '@/components/ui/Button'; 5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 6 | import { Input } from '@/components/ui/Input'; 7 | import { 8 | Select, 9 | SelectContent, 10 | SelectItem, 11 | SelectTrigger, 12 | SelectValue 13 | } from '@/components/ui/Select'; 14 | import { Separator } from '@/components/ui/Separator'; 15 | 16 | export function DocShare() { 17 | return ( 18 | 19 | 20 | Share this document 21 | Anyone with the link can view this document. 22 | 23 | 24 |
25 | 26 | 29 |
30 | 31 |
32 |

People with access

33 |
34 |
35 |
36 | 37 | OM 38 | 39 |
40 |

Olivia Martin

41 |

m@example.com

42 |
43 |
44 | 53 |
54 |
55 |
56 | 57 | IN 58 | 59 |
60 |

Isabella Nguyen

61 |

b@example.com

62 |
63 |
64 | 73 |
74 |
75 |
76 | 77 | SD 78 | 79 |
80 |

Sofia Davis

81 |

p@example.com

82 |
83 |
84 | 93 |
94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/Header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { UserNav } from './UserNav'; 5 | import TeamSwitcher from './TeamSwitcher'; 6 | import { usePathname } from 'next/navigation'; 7 | import configuration from '@/lib/config/dashboard'; 8 | import { MobileNav } from '@/components/MobileNav'; 9 | 10 | interface HeaderProps { 11 | display_name: string; 12 | email: string; 13 | avatar_url: string; 14 | } 15 | 16 | const Header = ({ display_name, email, avatar_url }: HeaderProps) => { 17 | const [headerText, setHeaderText] = useState(''); 18 | const pathname = usePathname().split('/'); 19 | const { routes } = configuration; 20 | 21 | useEffect(() => { 22 | if (pathname.includes('main')) { 23 | setHeaderText('Dashboard'); 24 | } else if (pathname.includes('todos')) { 25 | setHeaderText('Todos'); 26 | } else if (pathname.includes('settings')) { 27 | setHeaderText('Settings'); 28 | } else { 29 | setHeaderText('Dashboard'); 30 | } 31 | }, [pathname]); 32 | 33 | return ( 34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 | 43 |
{headerText}
44 |
45 | 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default Header; 53 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/RecentSales.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'; 2 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 3 | 4 | export function RecentSales() { 5 | return ( 6 | 7 | 8 | Recent Sales: 9 | Sales made within the last 30 days 10 | 11 | 12 |
13 |
14 | 15 | OM 16 | 17 |
18 |

Olivia Martin

19 |

olivia.martin@email.com

20 |
21 |
+$1,999.00
22 |
23 |
24 | 25 | JL 26 | 27 |
28 |

Jackson Lee

29 |

jackson.lee@email.com

30 |
31 |
+$39.00
32 |
33 |
34 | 35 | IN 36 | 37 |
38 |

Isabella Nguyen

39 |

isabella.nguyen@email.com

40 |
41 |
+$299.00
42 |
43 |
44 | 45 | WK 46 | 47 |
48 |

William Kim

49 |

will@email.com

50 |
51 |
+$99.00
52 |
53 |
54 | 55 | SD 56 | 57 |
58 |

Sofia Davis

59 |

sofia.davis@email.com

60 |
61 |
+$39.00
62 |
63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/SideBar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { Icons } from '@/components/Icons'; 5 | import { SideBarNav } from './SidebarNav'; 6 | import configuration from '@/lib/config/dashboard'; 7 | 8 | const Sidebar = () => { 9 | const [ isOpen, setOpen ] = useState(true); 10 | const { routes } = configuration; 11 | 12 | return ( 13 | 23 | ); 24 | }; 25 | 26 | export default Sidebar; 27 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/SidebarNav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { MainLogoIcon } from '@/components/MainLogo'; 5 | import { usePathname } from 'next/navigation'; 6 | import { NavItemSidebar } from '@/lib/types/types'; 7 | 8 | interface SideBarNavProps { 9 | isOpen: boolean; 10 | routes: NavItemSidebar[]; 11 | } 12 | 13 | interface SidebarNavItemProps { 14 | isOpen: boolean; 15 | item: NavItemSidebar; 16 | } 17 | 18 | const SidebarNavItem = ({ item, isOpen }: SidebarNavItemProps) => { 19 | const pathname = usePathname(); 20 | 21 | return ( 22 |
23 | 24 | 33 | 34 | {isOpen && {item.title}} 35 | 36 | 37 |
38 | ); 39 | }; 40 | 41 | export function SideBarNav({ isOpen, routes }: SideBarNavProps) { 42 | return ( 43 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/TeamSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons'; 5 | 6 | import { cn } from '@/lib/utils/helpers'; 7 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'; 8 | import { Button } from '@/components/ui/Button'; 9 | import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/Command'; 10 | import { Dialog } from '@/components/ui/Dialog'; 11 | 12 | import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'; 13 | 14 | const groups = [ 15 | { 16 | label: 'Teams', 17 | teams: [ 18 | { 19 | label: 'Team-1', 20 | value: 'team1' 21 | }, 22 | { 23 | label: 'Team-2', 24 | value: 'team2' 25 | } 26 | ] 27 | } 28 | ]; 29 | 30 | type Team = (typeof groups)[number]['teams'][number]; 31 | 32 | type PopoverTriggerProps = React.ComponentPropsWithoutRef; 33 | 34 | interface TeamSwitcherProps extends PopoverTriggerProps {} 35 | 36 | export default function TeamSwitcher({ className }: TeamSwitcherProps) { 37 | const [open, setOpen] = useState(false); 38 | const [showNewTeamDialog, setShowNewTeamDialog] = useState(false); 39 | const [selectedTeam, setSelectedTeam] = useState(groups[0].teams[0]); 40 | 41 | return ( 42 | 43 | 44 | 45 | 62 | 63 | 64 | 65 | 66 | {groups.map((group) => ( 67 | 68 | {group.teams.map((team) => ( 69 | { 72 | setSelectedTeam(team); 73 | setOpen(false); 74 | }} 75 | className="text-sm" 76 | > 77 | 78 | 83 | SC 84 | 85 | {team.label} 86 | 92 | 93 | ))} 94 | 95 | ))} 96 | 97 | 98 | 99 | 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/UserNav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/Avatar'; 4 | import { Button } from '@/components/ui/Button'; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuGroup, 9 | DropdownMenuItem, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | DropdownMenuShortcut, 13 | DropdownMenuTrigger 14 | } from '@/components/ui/DropdownMenu'; 15 | import Link from 'next/link'; 16 | import { useRouter } from 'next/navigation'; 17 | import { SupabaseSignOut } from '@/lib/API/Services/supabase/auth'; 18 | import { Icons } from '@/components/Icons'; 19 | import { useTheme } from "next-themes"; 20 | 21 | export function UserNav({ email, display_name, avatar_url }) { 22 | const router = useRouter(); 23 | const { setTheme } = useTheme() 24 | 25 | const signOut = async () => { 26 | await SupabaseSignOut(); 27 | router.refresh(); 28 | router.push('/'); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | 35 | 40 | 41 | 42 | setTheme("light")}> 43 | Light 44 | 45 | setTheme("dark")}> 46 | Dark 47 | 48 | setTheme("system")}> 49 | System 50 | 51 | 52 | 53 | 54 | 65 | 66 | 67 | 68 |
69 |

{display_name}

70 |

{email}

71 |
72 |
73 | 74 | 75 | 76 | Todos 77 | ⇧⌘P 78 | 79 | 80 | Billing 81 | ⌘B 82 | 83 | 84 | Settings 85 | ⌘S 86 | 87 | 88 | 89 | 90 | 93 | ⇧⌘Q 94 | 95 |
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/charts/Bar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 4 | import React from 'react'; 5 | import { 6 | BarChart, 7 | Bar, 8 | XAxis, 9 | YAxis, 10 | CartesianGrid, 11 | Tooltip, 12 | Legend, 13 | ResponsiveContainer 14 | } from 'recharts'; 15 | 16 | const data = [ 17 | { 18 | date: '2000-01', 19 | uv: 4000, 20 | pv: 2400, 21 | amt: 2400 22 | }, 23 | { 24 | date: '2000-02', 25 | uv: 3000, 26 | pv: 1398, 27 | amt: 2210 28 | }, 29 | { 30 | date: '2000-03', 31 | uv: 2000, 32 | pv: 9800, 33 | amt: 2290 34 | }, 35 | { 36 | date: '2000-04', 37 | uv: 2780, 38 | pv: 3908, 39 | amt: 2000 40 | }, 41 | { 42 | date: '2000-05', 43 | uv: 1890, 44 | pv: 4800, 45 | amt: 2181 46 | }, 47 | { 48 | date: '2000-06', 49 | uv: 2390, 50 | pv: 3800, 51 | amt: 2500 52 | }, 53 | { 54 | date: '2000-07', 55 | uv: 3490, 56 | pv: 4300, 57 | amt: 2100 58 | }, 59 | { 60 | date: '2000-08', 61 | uv: 4000, 62 | pv: 2400, 63 | amt: 2400 64 | }, 65 | { 66 | date: '2000-09', 67 | uv: 3000, 68 | pv: 1398, 69 | amt: 2210 70 | }, 71 | { 72 | date: '2000-10', 73 | uv: 2000, 74 | pv: 9800, 75 | amt: 2290 76 | }, 77 | { 78 | date: '2000-11', 79 | uv: 2780, 80 | pv: 3908, 81 | amt: 2000 82 | }, 83 | { 84 | date: '2000-12', 85 | uv: 1890, 86 | pv: 4800, 87 | amt: 2181 88 | } 89 | ]; 90 | 91 | const monthTickFormatter = (tick) => { 92 | let date = new Date(tick); 93 | let dateTick = date.getMonth() + 1; 94 | 95 | return dateTick.toString(); 96 | }; 97 | 98 | const renderQuarterTick = (tickProps) => { 99 | const { x, y, payload } = tickProps; 100 | const { value, offset } = payload; 101 | const date = new Date(value); 102 | const month = date.getMonth(); 103 | const quarterNo = Math.floor(month / 3) + 1; 104 | const isMidMonth = month % 3 === 1; 105 | 106 | if (month % 3 === 1) { 107 | return {`Q${quarterNo}`}; 108 | } 109 | 110 | const isLast = month === 11; 111 | 112 | if (month % 3 === 0 || isLast) { 113 | const pathX = Math.floor(isLast ? x + offset : x - offset) + 0.5; 114 | 115 | return ; 116 | } 117 | return null; 118 | }; 119 | 120 | const BarChartComp = () => { 121 | return ( 122 | 123 | Quarterly Revenue: 124 | 125 | 126 | 137 | 138 | 139 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | ); 159 | }; 160 | 161 | export default BarChartComp; 162 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/charts/Compose.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 4 | import { 5 | ComposedChart, 6 | Line, 7 | Area, 8 | Bar, 9 | XAxis, 10 | YAxis, 11 | CartesianGrid, 12 | Tooltip, 13 | Legend, 14 | Scatter, 15 | ResponsiveContainer 16 | } from 'recharts'; 17 | 18 | const data = [ 19 | { 20 | name: 'Jan', 21 | uv: 590, 22 | pv: 800, 23 | amt: 1400, 24 | cnt: 490 25 | }, 26 | { 27 | name: 'Feb', 28 | uv: 868, 29 | pv: 967, 30 | amt: 1506, 31 | cnt: 590 32 | }, 33 | { 34 | name: 'Mar', 35 | uv: 1397, 36 | pv: 1098, 37 | amt: 989, 38 | cnt: 350 39 | }, 40 | { 41 | name: 'April', 42 | uv: 1480, 43 | pv: 1200, 44 | amt: 1228, 45 | cnt: 480 46 | }, 47 | { 48 | name: 'May', 49 | uv: 1520, 50 | pv: 1108, 51 | amt: 1100, 52 | cnt: 460 53 | }, 54 | { 55 | name: 'June', 56 | uv: 1400, 57 | pv: 680, 58 | amt: 1700, 59 | cnt: 380 60 | } 61 | ]; 62 | 63 | const Compose = () => { 64 | return ( 65 | 66 | Current Sales Growth: 67 | 68 | 69 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | export default Compose; 97 | -------------------------------------------------------------------------------- /src/app/dashboard/_PageSections/charts/Pie.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 4 | import React from 'react'; 5 | import { PieChart, Pie, Sector, Cell, ResponsiveContainer } from 'recharts'; 6 | 7 | const data01 = [ 8 | { name: 'Group A', value: 400 }, 9 | { name: 'Group B', value: 300 }, 10 | { name: 'Group C', value: 300 }, 11 | { name: 'Group D', value: 200 } 12 | ]; 13 | const data02 = [ 14 | { name: 'A1', value: 100 }, 15 | { name: 'A2', value: 300 }, 16 | { name: 'B1', value: 100 }, 17 | { name: 'B2', value: 80 }, 18 | { name: 'B3', value: 40 }, 19 | { name: 'B4', value: 30 }, 20 | { name: 'B5', value: 50 }, 21 | { name: 'C1', value: 100 }, 22 | { name: 'C2', value: 200 }, 23 | { name: 'D1', value: 150 }, 24 | { name: 'D2', value: 50 } 25 | ]; 26 | 27 | const PieChartComp = () => { 28 | return ( 29 | 30 | Current Usage: 31 | 32 | 33 | 34 | 35 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default PieChartComp; 53 | -------------------------------------------------------------------------------- /src/app/dashboard/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { Card, CardHeader, CardDescription, CardContent, CardTitle } from '@/components/ui/Card'; 3 | import { Button } from '@/components/ui/Button'; 4 | import config from '@/lib/config/api'; 5 | 6 | export default function Error({ error, reset }: { error: Error; reset: () => void }) { 7 | console.log('Error', error); 8 | 9 | return ( 10 |
11 |
12 | 13 | 14 | {config.errorMessageGeneral} 15 | Click Below to Try Again 16 | 17 | 18 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import SideBar from './_PageSections/SideBar'; 2 | import Header from './_PageSections/Header'; 3 | import { SupabaseSession } from '@/lib/API/Services/supabase/user'; 4 | import { GetProfileByUserId } from '@/lib/API/Database/profile/queries'; 5 | import { redirect } from 'next/navigation'; 6 | import config from '@/lib/config/auth'; 7 | import { ProfileT } from '@/lib/types/supabase'; 8 | import { PostgrestSingleResponse } from '@supabase/supabase-js'; 9 | import { LayoutProps } from '@/lib/types/types'; 10 | 11 | export default async function DashboardLayout({ children }: LayoutProps) { 12 | const { data, error } = await SupabaseSession(); 13 | 14 | // Auth Guard 15 | if (error || !data?.session) { 16 | redirect(config.redirects.requireAuth); 17 | } 18 | 19 | let profile: PostgrestSingleResponse; 20 | if (data?.session?.user) { 21 | profile = await GetProfileByUserId(data.session.user.id); 22 | } 23 | 24 | const display_name = data[0]?.display_name; 25 | const email = data?.session?.user?.email; 26 | const avatar_url = data?.session?.user?.user_metadata?.avatar_url; 27 | 28 | return ( 29 |
30 | 31 |
32 |
33 |
{children}
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/app/dashboard/main/_PageSections/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import SummaryCard from './SummaryCard'; 2 | import { Icons } from '@/components/Icons'; 3 | import ComposeChart from '../../_PageSections/charts/Compose'; 4 | import BarChart from '../../_PageSections/charts/Bar'; 5 | import PieChart from '../../_PageSections/charts/Pie'; 6 | import { RecentSales } from '../../_PageSections/RecentSales'; 7 | import { DocShare } from '../../_PageSections/DocShare'; 8 | 9 | const Dashboard = () => { 10 | return ( 11 |
12 |
13 | } 16 | content_main={45596} 17 | content_secondary={'+6.1% from last month'} 18 | /> 19 | } 22 | content_main={10298} 23 | content_secondary={'+18.1% from last month'} 24 | /> 25 | } 28 | content_main={28353} 29 | content_secondary={'+10.1% from last month'} 30 | /> 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Dashboard; 52 | -------------------------------------------------------------------------------- /src/app/dashboard/main/_PageSections/SummaryCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; 4 | import CountUp from 'react-countup'; 5 | 6 | interface SummaryCardProps { 7 | card_title: string; 8 | icon: React.ReactNode; 9 | content_main: number; 10 | content_secondary: string; 11 | } 12 | 13 | const SummaryCard = ({ card_title, icon, content_main, content_secondary }: SummaryCardProps) => { 14 | return ( 15 | 16 | 17 | {card_title} 18 | {icon} 19 | 20 | 21 |
22 | + 23 | 24 |
25 |

{content_secondary}

26 |
27 |
28 | ); 29 | }; 30 | 31 | export default SummaryCard; 32 | -------------------------------------------------------------------------------- /src/app/dashboard/main/page.tsx: -------------------------------------------------------------------------------- 1 | import Dashboard from './_PageSections/Dashboard'; 2 | 3 | export default async function DashboardPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/_PageSections/Billing.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | 5 | import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'; 6 | import { Button } from '@/components/ui/Button'; 7 | 8 | import { createPortalSession } from '@/lib/API/Services/stripe/session'; 9 | 10 | const Billing = () => { 11 | const router = useRouter(); 12 | 13 | const handleSubscription = async () => { 14 | const res = await createPortalSession(); 15 | 16 | router.push(res.url); 17 | }; 18 | 19 | return ( 20 |
21 | 22 | 23 | Manage Subscription & Billing 24 | 25 | Click below to Manage Subscription and Billing, You will be redirected to the Stripe 26 | Customer Portal, where you will be able to update or cancel subsciptions, update payment 27 | methods and view past invoices. 28 | 29 | 30 | 31 | 34 | 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default Billing; 41 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/_PageSections/SettingsHeader.tsx: -------------------------------------------------------------------------------- 1 | const SettingsHeader = () => { 2 | return ( 3 |
4 |

Settings

5 |

6 | Manage your account settings, billing and subscription 7 |

8 |
9 | ); 10 | }; 11 | 12 | export default SettingsHeader; 13 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/_PageSections/SettingsNav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Link from 'next/link'; 3 | 4 | import { cn } from '@/lib/utils/helpers'; 5 | import { usePathname } from 'next/navigation'; 6 | import { NavItem } from '@/lib/types/types'; 7 | 8 | interface SettingsNavProps { 9 | items: NavItem[]; 10 | } 11 | 12 | export function SettingsNav({ items }: SettingsNavProps) { 13 | const pathname = usePathname(); 14 | 15 | return ( 16 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/_PageSections/Subscription.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | import { 7 | Card, 8 | CardHeader, 9 | CardTitle, 10 | CardContent, 11 | CardDescription, 12 | CardFooter 13 | } from '@/components/ui/Card'; 14 | import { Button } from '@/components/ui/Button'; 15 | import configuration from '@/lib/config/dashboard'; 16 | import { PlanI } from '@/lib/types/types'; 17 | import config from '@/lib/config/auth'; 18 | import { ErrorText } from '@/components/ErrorText'; 19 | interface SubscriptionExistsProps { 20 | price_id: string; 21 | status: string; 22 | period_ends: string; 23 | } 24 | 25 | const SubscriptionExists = ({ price_id, status, period_ends }: SubscriptionExistsProps) => { 26 | const { products } = configuration; 27 | const [errorMessage, setErrorMessage] = useState(''); 28 | const [currentPlan, setPlan] = useState({ name: '' }); 29 | 30 | const matchSubscription = () => { 31 | const match: PlanI = products 32 | .map((product) => product.plans.find((x: PlanI) => x.price_id === price_id)) 33 | .find((item) => !!item); 34 | 35 | if (!match) { 36 | setErrorMessage('Subscription Type Not Valid, Please Contact Support'); 37 | return; 38 | } 39 | 40 | setPlan(match); 41 | }; 42 | 43 | useEffect(() => { 44 | matchSubscription(); 45 | }, []); 46 | 47 | const router = useRouter(); 48 | 49 | const goToPortal = async () => { 50 | router.push(config.redirects.toBilling); 51 | }; 52 | 53 | return ( 54 |
55 | 56 | 57 | Subscription 58 | 59 | Click button below to go to the billing page to manage your Subscription and Billing 60 | 61 | 62 | 63 | 64 |

65 | Current Plan: {currentPlan?.name} 66 |

67 |
68 | Status: {status} 69 |
70 |
71 | Billing:{' '} 72 | 73 | ${currentPlan?.price}/{currentPlan?.interval} 74 | 75 |
76 |
77 | Billing Period Ends:{' '} 78 | {new Date(period_ends).toLocaleDateString()} 79 |
80 |
81 | 82 | 85 | 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default SubscriptionExists; 92 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/_PageSections/UpdateProfileCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardHeader, CardDescription, CardContent, CardTitle } from '@/components/ui/Card'; 4 | 5 | import { UpdateDisplayName, UpdateEmail, UpdatePassword } from './UpdateForms'; 6 | 7 | import { User } from '@supabase/supabase-js'; 8 | 9 | interface UpdateProfileCardProps { 10 | user: User; 11 | display_name: string; 12 | email: string; 13 | customer: string; 14 | } 15 | 16 | const UpdateProfileCard = ({ user, display_name, email, customer }: UpdateProfileCardProps) => { 17 | return ( 18 |
19 | 20 | 21 | Update Account 22 | Update Account display name, email and password 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default UpdateProfileCard; 35 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/add-subscription/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | import configuration from '@/lib/config/dashboard'; 5 | import { useRouter } from 'next/navigation'; 6 | import { 7 | Card, 8 | CardHeader, 9 | CardContent, 10 | CardTitle, 11 | CardFooter, 12 | CardDescription 13 | } from '@/components/ui/Card'; 14 | import { Button } from '@/components/ui/Button'; 15 | import { Icons } from '@/components/Icons'; 16 | import { Switch } from '@/components/ui/Switch'; 17 | import { createCheckoutSession } from '@/lib/API/Services/stripe/session'; 18 | import { ProductI } from '@/lib/types/types'; 19 | import { IntervalE } from '@/lib/types/enums'; 20 | 21 | interface PriceCardProps { 22 | product: ProductI; 23 | handleSubscription: (price: string) => Promise; 24 | timeInterval: IntervalE; 25 | } 26 | 27 | const PriceCard = ({ product, handleSubscription, timeInterval }: PriceCardProps) => { 28 | const [plan, setPlan] = useState({ price: '', price_id: '', isPopular: false }); 29 | const { name, description, features, plans } = product; 30 | 31 | const setProductPlan = () => { 32 | if (timeInterval === IntervalE.MONTHLY) { 33 | setPlan({ 34 | price: plans[0].price, 35 | price_id: plans[0].price_id, 36 | isPopular: plans[0].isPopular 37 | }); 38 | } else { 39 | setPlan({ 40 | price: plans[1].price, 41 | price_id: plans[1].price_id, 42 | isPopular: plans[1].isPopular 43 | }); 44 | } 45 | }; 46 | 47 | useEffect(() => { 48 | setProductPlan(); 49 | }, [timeInterval]); 50 | 51 | return ( 52 | 57 | {plan.isPopular && ( 58 |
59 | Popular 60 |
61 | )} 62 | 63 | {name} 64 | {description} 65 | 66 | 67 |
68 |

${plan?.price}

69 |
Billed {timeInterval}
70 |
71 |
    72 | {features.map((feature) => ( 73 |
  • 74 | {feature} 75 |
  • 76 | ))} 77 |
78 |
79 | 80 | 83 | 84 |
85 | ); 86 | }; 87 | 88 | const PricingDisplay = () => { 89 | const [timeInterval, setTimeInterval] = useState(IntervalE.MONTHLY); 90 | 91 | const { products } = configuration; 92 | 93 | const basic: ProductI = products[0]; 94 | const premium: ProductI = products[1]; 95 | 96 | const router = useRouter(); 97 | 98 | const handleSubscription = async (price: string) => { 99 | const res = await createCheckoutSession({ price }); 100 | 101 | router.push(res.url); 102 | }; 103 | 104 | const changeTimeInterval = () => { 105 | let intervalSwitch = timeInterval === IntervalE.MONTHLY ? IntervalE.YEARLY : IntervalE.MONTHLY; 106 | setTimeInterval(intervalSwitch); 107 | }; 108 | 109 | return ( 110 |
111 |

Add Subscription

112 |

113 | Add a Subscription by choosing a plan below 114 |

115 | 116 |
117 |
Monthly
118 | 119 |
Yearly
120 |
121 | 122 |
123 | 128 | 133 |
134 |
135 | ); 136 | }; 137 | 138 | export default PricingDisplay; 139 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import ManageSubscription from '../_PageSections/Billing'; 2 | import { SupabaseUser } from '@/lib/API/Services/supabase/user'; 3 | import { GetProfileByUserId } from '@/lib/API/Database/profile/queries'; 4 | import { redirect } from 'next/navigation'; 5 | import config from '@/lib/config/auth'; 6 | 7 | export default async function Billing() { 8 | const user = await SupabaseUser(); 9 | const profile = await GetProfileByUserId(user.id); 10 | const subscription = profile?.data?.[0]?.subscription_id; 11 | 12 | if (!subscription) redirect(config.redirects.requireSub); 13 | 14 | return ( 15 |
16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from '@/components/ui/Separator'; 2 | import { SettingsNav } from './_PageSections/SettingsNav'; 3 | import SettingsHeader from './_PageSections/SettingsHeader'; 4 | import configuration from '@/lib/config/dashboard'; 5 | import { LayoutProps } from '@/lib/types/types'; 6 | 7 | export default function SettingsLayout({ children }: LayoutProps) { 8 | const { 9 | subroutes: { settings } 10 | } = configuration; 11 | 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 |
{children}
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from '@/components/Icons'; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { SupabaseUser } from '@/lib/API/Services/supabase/user'; 2 | import { GetProfileByUserId } from '@/lib/API/Database/profile/queries'; 3 | 4 | import { Card, CardHeader, CardDescription, CardContent, CardTitle } from '@/components/ui/Card'; 5 | import { UpdateDisplayName, UpdateEmail, UpdatePassword } from '../_PageSections/UpdateForms'; 6 | 7 | export default async function ProfileForm() { 8 | const user = await SupabaseUser(); 9 | const profile = await GetProfileByUserId(user?.id); 10 | 11 | const display_name = profile?.data?.[0]?.display_name || ''; 12 | const customer = profile?.data?.[0]?.stripe_customer_id || ''; 13 | const email = user?.email; 14 | 15 | return ( 16 |
17 | 18 | 19 | Update Account 20 | Update Account display name, email and password 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/subscription-required/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | 5 | import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'; 6 | import { Button } from '@/components/ui/Button'; 7 | import config from '@/lib/config/auth'; 8 | 9 | const SubscriptionRequired = () => { 10 | const router = useRouter(); 11 | 12 | const redirectToSubscription = async () => { 13 | router.push(config.redirects.toSubscription); 14 | }; 15 | 16 | return ( 17 |
18 | 19 | 20 | No Subscription Found 21 | 22 | Click below to redirect to the Subscription Page to add a Subscription to your account. 23 | 24 | 25 | 26 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default SubscriptionRequired; 36 | -------------------------------------------------------------------------------- /src/app/dashboard/settings/subscription/page.tsx: -------------------------------------------------------------------------------- 1 | import { SupabaseUser } from '@/lib/API/Services/supabase/user'; 2 | import { GetProfileByUserId } from '@/lib/API/Database/profile/queries'; 3 | 4 | import { GetSubscriptionById } from '@/lib/API/Database/subcription/queries'; 5 | import SubscriptionDisplay from '../_PageSections/Subscription'; 6 | import { PostgrestSingleResponse } from '@supabase/supabase-js'; 7 | import { SubscriptionT } from '@/lib/types/supabase'; 8 | import { redirect } from 'next/navigation'; 9 | import config from '@/lib/config/auth'; 10 | 11 | export default async function Subscription() { 12 | const user = await SupabaseUser(); 13 | 14 | const profile = await GetProfileByUserId(user?.id); 15 | const subscription_id = profile?.data?.[0]?.subscription_id; 16 | 17 | if (!subscription_id) redirect(config.redirects.toAddSub); 18 | 19 | let subscription: PostgrestSingleResponse; 20 | if (profile?.data?.[0]?.subscription_id) { 21 | subscription = await GetSubscriptionById(profile?.data?.[0]?.subscription_id); 22 | } 23 | 24 | const price_id = subscription?.data[0]?.price_id; 25 | const status = subscription?.data[0]?.status; 26 | const period_ends = subscription?.data[0]?.period_ends_at; 27 | 28 | return ( 29 |
30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/dashboard/todos/_PageSections/MyTodos.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Card, CardDescription, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; 4 | import { DeleteTodo } from '@/lib/API/Database/todos/mutations'; 5 | import { Button, buttonVariants } from '@/components/ui/Button'; 6 | import Link from 'next/link'; 7 | import { cn } from '@/lib/utils/helpers'; 8 | import { toast } from 'react-toastify'; 9 | import { useRouter } from 'next/navigation'; 10 | import { TodoT } from '@/lib/types/todos'; 11 | 12 | const TodoCard = ({ id, title, description }: TodoT) => { 13 | const router = useRouter(); 14 | 15 | const Delete = async () => { 16 | const { error } = await DeleteTodo(id); 17 | 18 | if (error) { 19 | toast.error('Something Went Wrong, please try again'); 20 | return; 21 | } 22 | toast.success('Todo Deleted'); 23 | router.refresh(); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | {title} 30 | {description} 31 | 32 | 33 | 37 | Edit 38 | 39 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | const MyTodos = ({ todos }) => { 48 | return ( 49 |
50 | {todos.map((todo) => ( 51 | 52 | ))} 53 |
54 | ); 55 | }; 56 | 57 | export default MyTodos; 58 | -------------------------------------------------------------------------------- /src/app/dashboard/todos/_PageSections/TodosCreateForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { todoFormSchema, todoFormValues } from '@/lib/types/validations'; 5 | import { useForm } from 'react-hook-form'; 6 | import { Button } from '@/components/ui/Button'; 7 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/Form'; 8 | import { Input } from '@/components/ui/Input'; 9 | import { Textarea } from '@/components/ui/Textarea'; 10 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card'; 11 | import { Icons } from '@/components/Icons'; 12 | import { CreateTodo } from '@/lib/API/Database/todos/mutations'; 13 | import { toast } from 'react-toastify'; 14 | import { User } from '@supabase/supabase-js'; 15 | 16 | interface TodosCreateFormProps { 17 | user: User; 18 | author: string; 19 | } 20 | 21 | export default function TodosCreateForm({ user, author }: TodosCreateFormProps) { 22 | const form = useForm({ 23 | resolver: zodResolver(todoFormSchema), 24 | defaultValues: { 25 | title: '', 26 | description: '' 27 | } 28 | }); 29 | 30 | const { 31 | reset, 32 | register, 33 | setError, 34 | formState: { isSubmitting } 35 | } = form; 36 | 37 | const onSubmit = async (values: todoFormValues) => { 38 | const title = values.title; 39 | const description = values.description; 40 | 41 | const user_id = user?.id; 42 | const props = { title, description, user_id, author }; 43 | 44 | const { error } = await CreateTodo(props); 45 | 46 | if (error) { 47 | setError('title', { 48 | type: '"root.serverError', 49 | message: error.message 50 | }); 51 | return; 52 | } 53 | 54 | reset({ title: '', description: '' }); 55 | toast.success('Todo Submitted'); 56 | }; 57 | 58 | return ( 59 |
60 | 61 | 62 | New Todo 63 | Create a Todo with Title and Description 64 | 65 | 66 | 67 |
68 | 69 | ( 73 | 74 | 75 | Title 76 | 77 | 83 | 84 | 85 | )} 86 | /> 87 | ( 91 | 92 | Description 93 | 94 |