├── .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 |
58 |
59 | );
60 | };
61 |
62 | export default function Feature({ isFlipped }: FeaturePropsI) {
63 | return (
64 |
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 |
19 |
20 |
21 |
22 | {data?.session && (
23 |
27 | Dashboard
28 | )}
29 |
30 | {!data?.session && (
31 |
35 | Login
36 | )}
37 |
38 |
39 |
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 |
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 |
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 |
11 |
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 | router.push('/auth/login')}>
18 | Click Here to sign in
19 |
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 | router.push('/auth/login')}>
18 | Back to login
19 |
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 | reset()} className="mt-4">
19 | Try Again
20 |
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 |
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 |
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 |
27 | Copy Link
28 |
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 |
45 |
46 |
47 |
48 |
49 | Can edit
50 | Can view
51 |
52 |
53 |
54 |
55 |
56 |
57 | IN
58 |
59 |
60 |
Isabella Nguyen
61 |
b@example.com
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Can edit
70 | Can view
71 |
72 |
73 |
74 |
75 |
76 |
77 | SD
78 |
79 |
80 |
Sofia Davis
81 |
p@example.com
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Can edit
90 | Can view
91 |
92 |
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 |
18 |
19 |
20 | setOpen(!isOpen)} />
21 |
22 |
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 |
44 |
45 |
46 |
47 | {routes.map((item) => (
48 |
49 | ))}
50 |
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 |
52 |
53 |
57 | SC
58 |
59 | {selectedTeam.label}
60 |
61 |
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 |
36 |
37 |
38 | Toggle theme
39 |
40 |
41 |
42 | setTheme("light")}>
43 | Light
44 |
45 | setTheme("dark")}>
46 | Dark
47 |
48 | setTheme("system")}>
49 | System
50 |
51 |
52 |
53 |
54 |
55 |
56 | {avatar_url ? (
57 |
58 | ) : (
59 |
60 |
61 |
62 | )}
63 |
64 |
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 |
91 | Log out
92 |
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 | reset()} className="mt-4">
19 | Try Again
20 |
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 |
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 |
32 | Go to Portal
33 |
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 |
17 | {items.map((item) => (
18 |
27 | {item.title}
28 |
29 | ))}
30 |
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 |
83 | Go to Billing
84 |
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 | handleSubscription(plan?.price_id)}>
81 | Get Started
82 |
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 |
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 |
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 |
27 | Go to Subscription
28 |
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 |
40 | Delete
41 |
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 |
107 |
108 |
109 |
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/_PageSections/TodosEditForm.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 | import { zodResolver } from '@hookform/resolvers/zod';
5 |
6 | import { todoFormSchema, todoFormValues } from '@/lib/types/validations';
7 | import { useForm } from 'react-hook-form';
8 | import { Button } from '@/components/ui/Button';
9 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/Form';
10 | import { Input } from '@/components/ui/Input';
11 | import { Textarea } from '@/components/ui/Textarea';
12 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
13 | import { Icons } from '@/components/Icons';
14 | import { UpdateTodo } from '@/lib/API/Database/todos/mutations';
15 | import { toast } from 'react-toastify';
16 | import { TodoT } from '@/lib/types/todos';
17 |
18 | interface EditFormProps {
19 | todo: TodoT;
20 | }
21 |
22 | export default function TodosEditForm({ todo }: EditFormProps) {
23 | const router = useRouter();
24 |
25 | const { title, description, id } = todo;
26 |
27 | const form = useForm({
28 | resolver: zodResolver(todoFormSchema),
29 | defaultValues: {
30 | title,
31 | description
32 | }
33 | });
34 |
35 | const {
36 | register,
37 | reset,
38 | setError,
39 | formState: { isSubmitting, isSubmitted }
40 | } = form;
41 |
42 | const onSubmit = async (values: todoFormValues) => {
43 | const title = values.title;
44 | const description = values.description;
45 | const props = { id, title, description };
46 |
47 | const { error } = await UpdateTodo(props);
48 |
49 | if (error) {
50 | setError('title', {
51 | type: '"root.serverError',
52 | message: error.message
53 | });
54 | return;
55 | }
56 |
57 | reset({ title: '', description: '' });
58 | toast.success('Todo Updated');
59 | router.refresh();
60 | router.push('/dashboard/todos/my-todos');
61 | };
62 |
63 | return (
64 |
65 |
66 |
67 | Update Todo
68 | Update Todo with a new Title or Description
69 |
70 |
71 |
72 |
73 |
74 | (
78 |
79 |
80 | Title
81 |
82 |
87 |
88 |
89 | )}
90 | />
91 | (
95 |
96 | Description
97 |
98 |
102 |
103 |
104 |
105 | )}
106 | />
107 |
108 | {isSubmitting && }Submit
109 |
110 |
111 |
112 |
113 |
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/_PageSections/TodosHeader.tsx:
--------------------------------------------------------------------------------
1 | const TodosHeader = () => {
2 | return (
3 |
4 |
Todos
5 |
Create or Manage Todos
6 |
7 | );
8 | };
9 |
10 | export default TodosHeader;
11 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/_PageSections/TodosList.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
2 | import { TodoT } from '@/lib/types/todos';
3 |
4 | const TodoCard = ({ title, description, author }: TodoT) => {
5 | return (
6 |
7 |
8 | {title}
9 | {description}
10 |
11 |
12 | By: {author ? author : 'anonymous'}
13 |
14 |
15 | );
16 | };
17 |
18 | const TodosList = ({ todos }) => {
19 | return (
20 |
21 | {todos.map((todo) => (
22 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default TodosList;
34 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/_PageSections/TodosNav.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Link from 'next/link';
3 | import { NavItem } from '@/lib/types/types';
4 | import { usePathname } from 'next/navigation';
5 |
6 | interface TodosNavProps {
7 | items: NavItem[];
8 | }
9 |
10 | export function TodosNav({ items }: TodosNavProps) {
11 | const pathname = usePathname();
12 |
13 | return (
14 |
15 | {items.map((item) => (
16 |
25 | {item.title}
26 |
27 | ))}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/create/page.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 |
15 | export default function TodosCreateForm() {
16 | const form = useForm({
17 | resolver: zodResolver(todoFormSchema),
18 | defaultValues: {
19 | title: '',
20 | description: ''
21 | }
22 | });
23 |
24 | const {
25 | reset,
26 | register,
27 | setError,
28 | formState: { isSubmitting }
29 | } = form;
30 |
31 | const onSubmit = async (values: todoFormValues) => {
32 | const title = values.title;
33 | const description = values.description;
34 |
35 | const props = { title, description };
36 | const { error } = await CreateTodo(props);
37 |
38 | if (error) {
39 | setError('title', {
40 | type: '"root.serverError',
41 | message: error.message
42 | });
43 | return;
44 | }
45 |
46 | reset({ title: '', description: '' });
47 | toast.success('Todo Submitted');
48 | };
49 |
50 | return (
51 |
52 |
53 |
54 | New Todo
55 | Create a Todo with Title and Description
56 |
57 |
58 |
59 |
60 |
61 | (
65 |
66 | Title
67 |
68 |
69 |
70 |
71 | )}
72 | />
73 | (
77 |
78 | Description
79 |
80 |
81 |
82 |
83 |
84 | )}
85 | />
86 |
87 | {isSubmitting && }Submit
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/edit/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useState, useEffect } from 'react';
4 | import { useParams, useRouter } from 'next/navigation';
5 | import { zodResolver } from '@hookform/resolvers/zod';
6 |
7 | import { todoFormSchema, todoFormValues } from '@/lib/types/validations';
8 | import { useForm } from 'react-hook-form';
9 | import { Button } from '@/components/ui/Button';
10 | import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/Form';
11 | import { Input } from '@/components/ui/Input';
12 | import { Textarea } from '@/components/ui/Textarea';
13 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
14 | import { Icons } from '@/components/Icons';
15 | import { UpdateTodo } from '@/lib/API/Database/todos/mutations';
16 | import { toast } from 'react-toastify';
17 | import { TodoT } from '@/lib/types/todos';
18 | import { GetTodoById } from '@/lib/API/Database/todos/queries';
19 |
20 | export default function TodosEditForm() {
21 | const router = useRouter();
22 | const params = useParams();
23 | const todo_id = params.id as string;
24 |
25 | const GetTodo = async () => {
26 | const res = await GetTodoById(todo_id);
27 |
28 | const { title, description } = res?.data?.[0];
29 | reset({ title, description });
30 | };
31 |
32 | useEffect(() => {
33 | GetTodo();
34 | }, []);
35 |
36 | const form = useForm({
37 | resolver: zodResolver(todoFormSchema),
38 | defaultValues: {
39 | title: '',
40 | description: ''
41 | }
42 | });
43 |
44 | const {
45 | register,
46 | reset,
47 | setError,
48 | formState: { isSubmitting, isSubmitted }
49 | } = form;
50 |
51 | const onSubmit = async (values: todoFormValues) => {
52 | const title = values.title;
53 | const description = values.description;
54 |
55 | const props = { id: todo_id, title, description };
56 | const { error } = await UpdateTodo(props);
57 |
58 | if (error) {
59 | setError('title', {
60 | type: '"root.serverError',
61 | message: error.message
62 | });
63 | return;
64 | }
65 |
66 | reset({ title: '', description: '' });
67 | toast.success('Todo Updated');
68 | router.refresh();
69 | router.push('/dashboard/todos/my-todos');
70 | };
71 |
72 | return (
73 |
74 |
75 |
76 | Update Todo
77 | Update Todo with a new Title or Description
78 |
79 |
80 |
81 |
82 |
83 | (
87 |
88 |
89 | Title
90 |
91 |
92 |
93 |
94 | )}
95 | />
96 | (
100 |
101 | Description
102 |
103 |
104 |
105 |
106 |
107 | )}
108 | />
109 |
110 | {isSubmitting && }Submit
111 |
112 |
113 |
114 |
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Separator } from '@/components/ui/Separator';
2 | import { TodosNav } from './_PageSections/TodosNav';
3 | import TodosHeader from './_PageSections/TodosHeader';
4 | import configuration from '@/lib/config/dashboard';
5 | import { LayoutProps } from '@/lib/types/types';
6 |
7 | export default function Layout({ children }: LayoutProps) {
8 | const {
9 | subroutes: { todos }
10 | } = configuration;
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
{children}
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/list-todos/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
4 | import { TodoT } from '@/lib/types/todos';
5 | import { GetAllTodos } from '@/lib/API/Database/todos/queries';
6 | import useSWR from 'swr';
7 | import { toast } from 'react-toastify';
8 | import config from '@/lib/config/api';
9 |
10 | const TodoCard = ({ title, description, author }: TodoT) => {
11 | return (
12 |
13 |
14 | {title}
15 | {description}
16 |
17 |
18 | By: {author ? author : 'anonymous'}
19 |
20 |
21 | );
22 | };
23 |
24 | const TodosList = () => {
25 | const { data, error } = useSWR(config.swrKeys.getAllTodos, GetAllTodos);
26 | if (error) toast.error('Something Went Wrong, please try again');
27 |
28 | return (
29 |
30 | {data?.data?.map((todo) => (
31 |
37 | ))}
38 |
39 | );
40 | };
41 |
42 | export default TodosList;
43 |
--------------------------------------------------------------------------------
/src/app/dashboard/todos/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/todos/my-todos/page.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 { TodoT } from '@/lib/types/todos';
10 | import { GetTodosByUserId } from '@/lib/API/Database/todos/queries';
11 | import useSWR, { useSWRConfig } from 'swr';
12 | import config from '@/lib/config/api';
13 |
14 | interface TodoCardProps extends TodoT {
15 | deleteTodo: (id: string) => Promise;
16 | }
17 |
18 | const TodoCard = ({ id, title, description, deleteTodo }: TodoCardProps) => {
19 | return (
20 |
21 |
22 | {title}
23 | {description}
24 |
25 |
26 |
30 | Edit
31 |
32 | deleteTodo(id)} variant="destructive">
33 | Delete
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default function MyTodos() {
41 | const { data, error } = useSWR(config.swrKeys.getMyTodos, GetTodosByUserId);
42 | const { mutate } = useSWRConfig();
43 |
44 | if (error) toast.error('Something Went Wrong, please try again');
45 |
46 | const deleteTodo = async (id: string) => {
47 | const { error } = await DeleteTodo(id);
48 |
49 | if (error) {
50 | toast.error('Something Went Wrong, please try again');
51 | return;
52 | }
53 | toast.success('Todo Deleted');
54 |
55 | mutate('GetTodos');
56 | };
57 |
58 | return (
59 |
60 | {data?.data?.map((todo) => (
61 |
68 | ))}
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
4 | console.log('Global Error', error);
5 |
6 | return (
7 |
8 |
9 | Something went wrong!
10 | {error.message}
11 | reset()}>Try again
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css';
2 | import { InterFont } from '@/styles/fonts';
3 | import { ThemeProvider } from '@/styles/ThemeProvider';
4 | import { ToastContainer } from 'react-toastify';
5 | import 'react-toastify/dist/ReactToastify.min.css';
6 | import NextTopLoader from 'nextjs-toploader';
7 | import config from '@/lib/config/site';
8 |
9 | const RootLayout = ({ children }) => {
10 | return (
11 |
12 |
13 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default RootLayout;
29 |
--------------------------------------------------------------------------------
/src/app/loading.tsx:
--------------------------------------------------------------------------------
1 | const Loading = () => {
2 | return Loading...
;
3 | };
4 |
5 | export default Loading;
6 |
--------------------------------------------------------------------------------
/src/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | const NotFound = () => {
2 | return NotFound
;
3 | };
4 |
5 | export default NotFound;
6 |
--------------------------------------------------------------------------------
/src/components/ErrorText.tsx:
--------------------------------------------------------------------------------
1 | interface ErrorTextProps {
2 | errorMessage: string;
3 | }
4 |
5 | export const ErrorText = ({ errorMessage }: ErrorTextProps) => {
6 | return (
7 |
8 | {errorMessage && (
9 |
{errorMessage}
10 | )}
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import config from '@/lib/config/marketing';
4 | import Link from 'next/link';
5 | import { SocialIcons } from './Icons';
6 | import configuration from '@/lib/config/site';
7 | import { Input } from './ui/Input';
8 | import { Button } from './ui/Button';
9 |
10 | export default function Footer() {
11 | const { footer_nav } = config;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {footer_nav.about.title}
22 |
23 |
24 | {footer_nav.about.routes.map((item) => (
25 |
26 |
30 | {item.title}
31 |
32 |
33 | ))}
34 |
35 |
36 |
37 |
38 | {footer_nav.resources.title}
39 |
40 |
41 | {footer_nav.resources.routes.map((item) => (
42 |
43 |
47 | {item.title}
48 |
49 |
50 | ))}
51 |
52 |
53 |
54 |
55 |
56 |
57 | {footer_nav.legal.title}
58 |
59 |
60 | {footer_nav.legal.routes.map((item) => (
61 |
62 |
66 | {item.title}
67 |
68 |
69 | ))}
70 |
71 |
72 |
73 |
74 |
75 |
76 | Subscribe to our newsletter
77 |
78 |
79 | The latest news, articles, and resources, sent to your inbox weekly.
80 |
81 |
82 |
88 |
89 |
90 | Subscribe
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | © 2023 Your Company, Inc. All rights reserved.
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/Form.tsx:
--------------------------------------------------------------------------------
1 | 'client-only';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { Slot } from '@radix-ui/react-slot';
6 | import {
7 | Controller,
8 | ControllerProps,
9 | FieldPath,
10 | FieldValues,
11 | FormProvider,
12 | useFormContext
13 | } from 'react-hook-form';
14 |
15 | import { cn } from '@/lib/utils/helpers';
16 | import { Label } from './ui/Label';
17 |
18 | const Form = FormProvider;
19 |
20 | type FormFieldContextValue<
21 | TFieldValues extends FieldValues = FieldValues,
22 | TName extends FieldPath = FieldPath
23 | > = {
24 | name: TName;
25 | };
26 |
27 | const FormFieldContext = React.createContext({} as FormFieldContextValue);
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error('useFormField should be used within ');
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext({} as FormItemContextValue);
70 |
71 | const FormItem = React.forwardRef>(
72 | ({ className, ...props }, ref) => {
73 | const id = React.useId();
74 |
75 | return (
76 |
77 |
78 |
79 | );
80 | }
81 | );
82 | FormItem.displayName = 'FormItem';
83 |
84 | const FormLabel = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => {
88 | const { error, formItemId } = useFormField();
89 |
90 | return (
91 |
97 | );
98 | });
99 | FormLabel.displayName = 'FormLabel';
100 |
101 | const FormControl = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ ...props }, ref) => {
105 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
106 |
107 | return (
108 |
115 | );
116 | });
117 | FormControl.displayName = 'FormControl';
118 |
119 | const FormDescription = React.forwardRef<
120 | HTMLParagraphElement,
121 | React.HTMLAttributes
122 | >(({ className, ...props }, ref) => {
123 | const { formDescriptionId } = useFormField();
124 |
125 | return (
126 |
132 | );
133 | });
134 | FormDescription.displayName = 'FormDescription';
135 |
136 | const FormMessage = React.forwardRef<
137 | HTMLParagraphElement,
138 | React.HTMLAttributes
139 | >(({ className, children, ...props }, ref) => {
140 | const { error, formMessageId } = useFormField();
141 | const body = error ? String(error?.message) : children;
142 |
143 | if (!body) {
144 | return null;
145 | }
146 |
147 | return (
148 |
154 | {body}
155 |
156 | );
157 | });
158 | FormMessage.displayName = 'FormMessage';
159 |
160 | export {
161 | useFormField,
162 | Form,
163 | FormItem,
164 | FormLabel,
165 | FormControl,
166 | FormDescription,
167 | FormMessage,
168 | FormField
169 | };
170 |
--------------------------------------------------------------------------------
/src/components/Icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Check,
3 | Laptop,
4 | Home,
5 | Settings,
6 | AlignLeftIcon,
7 | Mail,
8 | Command,
9 | X,
10 | Loader2,
11 | EyeIcon,
12 | EyeOffIcon,
13 | CircleDollarSign,
14 | Users,
15 | ScreenShare,
16 | User,
17 | Lock,
18 | CheckCircle2,
19 | Sun,
20 | Moon,
21 | Twitter,
22 | Github,
23 | Linkedin,
24 | Menu
25 | } from 'lucide-react';
26 |
27 | const Google = ({ ...props }) => (
28 |
29 |
33 |
34 | );
35 |
36 | export const Icons = {
37 | SidebarToggle: AlignLeftIcon,
38 | Laptop,
39 | Settings,
40 | Home,
41 | User,
42 | Mail,
43 | Command,
44 | Close: X,
45 | Check,
46 | Spinner: Loader2,
47 | CircleDollarSign,
48 | Users,
49 | ScreenShare,
50 | EyeIcon,
51 | EyeOffIcon,
52 | Lock,
53 | Google,
54 | CheckCircle2,
55 | Sun,
56 | Moon,
57 | Menu
58 | };
59 |
60 | export const SocialIcons = {
61 | Twitter,
62 | Google,
63 | Linkedin,
64 | Github
65 | };
66 |
67 | export const MarketingIcons = {};
68 |
--------------------------------------------------------------------------------
/src/components/MainLogo.tsx:
--------------------------------------------------------------------------------
1 | import siteConfig from '@/lib/config/site';
2 | import Link from 'next/link';
3 | import { Icons } from '@/components/Icons';
4 |
5 | export const MainLogoText = () => {
6 | return (
7 |
8 |
9 | {siteConfig.alt_name}
10 |
11 | );
12 | };
13 |
14 | export const MainLogoIcon = () => {
15 | return (
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/MobileNav.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | import { NavItem } from '@/lib/types/types';
4 |
5 | import { Icons } from '@/components/Icons';
6 |
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger
12 | } from '@/components/ui/DropdownMenu';
13 |
14 | export interface NavProps {
15 | items?: NavItem[];
16 | }
17 |
18 | const MobileNavItem = ({ title, link }: NavItem) => (
19 |
20 |
21 | {title}
22 |
23 |
24 | );
25 |
26 | export const MobileNav = ({ items }: NavProps) => {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | {items.map((item) => (
35 |
36 |
37 |
38 | ))}
39 |
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/ThemeDropdown.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuTrigger
8 | } from '@/components/ui/DropdownMenu';
9 | import { Button } from '@/components/ui/Button';
10 | import { Icons } from '@/components/Icons';
11 | import { useTheme } from 'next-themes';
12 |
13 | export const ThemeDropDownMenu = () => {
14 | const { setTheme } = useTheme();
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
25 |
29 | Toggle theme
30 |
31 |
32 |
33 | setTheme('light')}>Light
34 | setTheme('dark')}>Dark
35 | setTheme('system')}>System
36 |
37 |
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/readme.md:
--------------------------------------------------------------------------------
1 | Basic building blocks of Pages are placed here.
2 |
3 | Basic components can be placed in /ui.
4 |
5 | More complex components that are shared across multiple pages can be placed in /components
6 |
7 | complex components that are only used by a single page, should just be placed in that specific pages /\_PageSections directory.
8 |
--------------------------------------------------------------------------------
/src/components/ui/Avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils/helpers';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
17 | ));
18 | Avatar.displayName = AvatarPrimitive.Root.displayName;
19 |
20 | const AvatarImage = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31 |
32 | const AvatarFallback = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46 |
47 | export { Avatar, AvatarImage, AvatarFallback };
48 |
--------------------------------------------------------------------------------
/src/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { VariantProps, cva } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils/helpers';
5 |
6 | const buttonVariants = cva(
7 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
13 | outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
14 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | ghost: 'hover:bg-accent hover:text-accent-foreground',
16 | link: 'underline-offset-4 hover:underline text-primary'
17 | },
18 | size: {
19 | default: 'h-10 py-2 px-4',
20 | sm: 'h-9 px-3 rounded-md',
21 | lg: 'h-11 px-8 rounded-md',
22 | icon: 'mr-2 h-4 w-4'
23 | }
24 | },
25 | defaultVariants: {
26 | variant: 'default',
27 | size: 'default'
28 | }
29 | }
30 | );
31 |
32 | export interface ButtonProps
33 | extends React.ButtonHTMLAttributes,
34 | VariantProps {}
35 |
36 | const Button = React.forwardRef(
37 | ({ className, variant, size, ...props }, ref) => {
38 | return (
39 |
40 | );
41 | }
42 | );
43 | Button.displayName = 'Button';
44 |
45 | export { Button, buttonVariants };
46 |
--------------------------------------------------------------------------------
/src/components/ui/Card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils/helpers';
4 |
5 | const Card = React.forwardRef>(
6 | ({ className, ...props }, ref) => (
7 |
12 | )
13 | );
14 | Card.displayName = 'Card';
15 |
16 | const CardHeader = React.forwardRef>(
17 | ({ className, ...props }, ref) => (
18 |
19 | )
20 | );
21 | CardHeader.displayName = 'CardHeader';
22 |
23 | const CardTitle = React.forwardRef>(
24 | ({ className, ...props }, ref) => (
25 |
30 | )
31 | );
32 | CardTitle.displayName = 'CardTitle';
33 |
34 | const CardDescription = React.forwardRef<
35 | HTMLParagraphElement,
36 | React.HTMLAttributes
37 | >(({ className, ...props }, ref) => (
38 |
39 | ));
40 | CardDescription.displayName = 'CardDescription';
41 |
42 | const CardContent = React.forwardRef>(
43 | ({ className, ...props }, ref) => (
44 |
45 | )
46 | );
47 | CardContent.displayName = 'CardContent';
48 |
49 | const CardFooter = React.forwardRef>(
50 | ({ className, ...props }, ref) => (
51 |
52 | )
53 | );
54 | CardFooter.displayName = 'CardFooter';
55 |
56 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
57 |
--------------------------------------------------------------------------------
/src/components/ui/Command.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { DialogProps } from '@radix-ui/react-dialog';
5 | import { Command as CommandPrimitive } from 'cmdk';
6 | import { Search } from 'lucide-react';
7 |
8 | import { cn } from '@/lib/utils/helpers';
9 | import { Dialog, DialogContent } from './Dialog';
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ));
24 | Command.displayName = CommandPrimitive.displayName;
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ));
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName;
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ));
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName;
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
77 | ));
78 |
79 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
80 |
81 | const CommandGroup = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
93 | ));
94 |
95 | CommandGroup.displayName = CommandPrimitive.Group.displayName;
96 |
97 | const CommandSeparator = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ));
107 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
108 |
109 | const CommandItem = React.forwardRef<
110 | React.ElementRef,
111 | React.ComponentPropsWithoutRef
112 | >(({ className, ...props }, ref) => (
113 |
121 | ));
122 |
123 | CommandItem.displayName = CommandPrimitive.Item.displayName;
124 |
125 | const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => {
126 | return (
127 |
131 | );
132 | };
133 | CommandShortcut.displayName = 'CommandShortcut';
134 |
135 | export {
136 | Command,
137 | CommandDialog,
138 | CommandInput,
139 | CommandList,
140 | CommandEmpty,
141 | CommandGroup,
142 | CommandItem,
143 | CommandShortcut,
144 | CommandSeparator
145 | };
146 |
--------------------------------------------------------------------------------
/src/components/ui/Dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as DialogPrimitive from '@radix-ui/react-dialog';
5 | import { X } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils/helpers';
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => (
14 |
15 | );
16 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
17 |
18 | const DialogOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
32 |
33 | const DialogContent = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, children, ...props }, ref) => (
37 |
38 |
39 |
47 | {children}
48 |
49 |
50 | Close
51 |
52 |
53 |
54 | ));
55 | DialogContent.displayName = DialogPrimitive.Content.displayName;
56 |
57 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
58 |
59 | );
60 | DialogHeader.displayName = 'DialogHeader';
61 |
62 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
63 |
67 | );
68 | DialogFooter.displayName = 'DialogFooter';
69 |
70 | const DialogTitle = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, ...props }, ref) => (
74 |
79 | ));
80 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
81 |
82 | const DialogDescription = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
91 | ));
92 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
93 |
94 | export {
95 | Dialog,
96 | DialogTrigger,
97 | DialogContent,
98 | DialogHeader,
99 | DialogFooter,
100 | DialogTitle,
101 | DialogDescription
102 | };
103 |
--------------------------------------------------------------------------------
/src/components/ui/Input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils/helpers';
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
19 | );
20 | }
21 | );
22 | Input.displayName = 'Input';
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/src/components/ui/Label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils/helpers';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef & VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
18 | ));
19 | Label.displayName = LabelPrimitive.Root.displayName;
20 |
21 | export { Label };
22 |
--------------------------------------------------------------------------------
/src/components/ui/Popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 |
6 | import { cn } from '@/lib/utils/helpers';
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/src/components/ui/Select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SelectPrimitive from '@radix-ui/react-select';
5 | import { Check, ChevronDown } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils/helpers';
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ));
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = 'popper', ...props }, ref) => (
39 |
40 |
51 |
58 | {children}
59 |
60 |
61 |
62 | ));
63 | SelectContent.displayName = SelectPrimitive.Content.displayName;
64 |
65 | const SelectLabel = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
74 | ));
75 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
76 |
77 | const SelectItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef
80 | >(({ className, children, ...props }, ref) => (
81 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {children}
96 |
97 | ));
98 | SelectItem.displayName = SelectPrimitive.Item.displayName;
99 |
100 | const SelectSeparator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ));
110 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
111 |
112 | export {
113 | Select,
114 | SelectGroup,
115 | SelectValue,
116 | SelectTrigger,
117 | SelectContent,
118 | SelectLabel,
119 | SelectItem,
120 | SelectSeparator
121 | };
122 |
--------------------------------------------------------------------------------
/src/components/ui/Separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
5 |
6 | import { cn } from '@/lib/utils/helpers';
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
12 |
23 | ));
24 | Separator.displayName = SeparatorPrimitive.Root.displayName;
25 |
26 | export { Separator };
27 |
--------------------------------------------------------------------------------
/src/components/ui/Switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SwitchPrimitives from '@radix-ui/react-switch';
5 |
6 | import { cn } from '@/lib/utils/helpers';
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/components/ui/Table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils/helpers';
4 |
5 | const Table = React.forwardRef>(
6 | ({ className, ...props }, ref) => (
7 |
10 | )
11 | );
12 | Table.displayName = 'Table';
13 |
14 | const TableHeader = React.forwardRef<
15 | HTMLTableSectionElement,
16 | React.HTMLAttributes
17 | >(({ className, ...props }, ref) => (
18 |
19 | ));
20 | TableHeader.displayName = 'TableHeader';
21 |
22 | const TableBody = React.forwardRef<
23 | HTMLTableSectionElement,
24 | React.HTMLAttributes
25 | >(({ className, ...props }, ref) => (
26 |
27 | ));
28 | TableBody.displayName = 'TableBody';
29 |
30 | const TableFooter = React.forwardRef<
31 | HTMLTableSectionElement,
32 | React.HTMLAttributes
33 | >(({ className, ...props }, ref) => (
34 |
39 | ));
40 | TableFooter.displayName = 'TableFooter';
41 |
42 | const TableRow = React.forwardRef>(
43 | ({ className, ...props }, ref) => (
44 |
52 | )
53 | );
54 | TableRow.displayName = 'TableRow';
55 |
56 | const TableHead = React.forwardRef<
57 | HTMLTableCellElement,
58 | React.ThHTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
68 | ));
69 | TableHead.displayName = 'TableHead';
70 |
71 | const TableCell = React.forwardRef<
72 | HTMLTableCellElement,
73 | React.TdHTMLAttributes
74 | >(({ className, ...props }, ref) => (
75 |
80 | ));
81 | TableCell.displayName = 'TableCell';
82 |
83 | const TableCaption = React.forwardRef<
84 | HTMLTableCaptionElement,
85 | React.HTMLAttributes
86 | >(({ className, ...props }, ref) => (
87 |
88 | ));
89 | TableCaption.displayName = 'TableCaption';
90 |
91 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
92 |
--------------------------------------------------------------------------------
/src/components/ui/Tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TabsPrimitive from '@radix-ui/react-tabs';
5 |
6 | import { cn } from '@/lib/utils/helpers';
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/src/components/ui/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils/helpers';
4 |
5 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(
8 | ({ className, ...props }, ref) => {
9 | return (
10 |
18 | );
19 | }
20 | );
21 | Textarea.displayName = 'Textarea';
22 |
23 | export { Textarea };
24 |
--------------------------------------------------------------------------------
/src/lib/API/Database/profile/mutations.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
4 | import { PostgrestSingleResponse } from '@supabase/supabase-js';
5 |
6 | interface ProfileUpdatePropsI {
7 | id: string;
8 | display_name: string;
9 | }
10 |
11 | export const SupabaseProfileUpdate = async ({
12 | id,
13 | display_name
14 | }: ProfileUpdatePropsI): Promise> => {
15 | const res = await supabase().from('profiles').upsert({ id, display_name });
16 |
17 | return res;
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/API/Database/profile/queries.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
3 | import { ProfileT } from '@/lib/types/supabase';
4 | import { PostgrestSingleResponse } from '@supabase/supabase-js';
5 | import { SupabaseDBError } from '@/lib/utils/error';
6 |
7 | export const GetProfileByUserId = async (
8 | id: string
9 | ): Promise> => {
10 | const res: PostgrestSingleResponse = await supabase()
11 | .from('profiles')
12 | .select()
13 | .eq('id', id);
14 |
15 | if (res.error) SupabaseDBError(res.error);
16 |
17 | return res;
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/API/Database/subcription/queries.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
3 | import { SubscriptionT } from '@/lib/types/supabase';
4 | import { PostgrestSingleResponse } from '@supabase/supabase-js';
5 | import { SupabaseDBError } from '@/lib/utils/error';
6 |
7 | export const GetSubscriptionById = async (
8 | id: string
9 | ): Promise> => {
10 | const res: PostgrestSingleResponse = await supabase()
11 | .from('subscriptions')
12 | .select()
13 | .eq('id', id);
14 |
15 | if (res.error) SupabaseDBError(res.error);
16 |
17 | return res;
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/API/Database/todos/mutations.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { SupabaseUser } from '@/lib/API/Services/supabase/user';
4 | import { GetProfileByUserId } from '@/lib/API/Database/profile/queries';
5 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
6 | import { PostgrestSingleResponse } from '@supabase/supabase-js';
7 | import { revalidatePath } from 'next/cache';
8 |
9 | interface CreateTodoPropsI {
10 | title: string;
11 | description: string;
12 | }
13 |
14 | interface UpdateTodoPropsI {
15 | id: string;
16 | title: string;
17 | description: string;
18 | }
19 |
20 | export const CreateTodo = async ({
21 | title,
22 | description
23 | }: CreateTodoPropsI): Promise> => {
24 | const user = await SupabaseUser();
25 | const profile = await GetProfileByUserId(user?.id);
26 | const user_id = user?.id;
27 | const author = profile?.data?.[0]?.display_name || '';
28 |
29 | const res = await supabase().from('todos').insert({ title, description, user_id, author });
30 |
31 | return res;
32 | };
33 |
34 | export const UpdateTodo = async ({
35 | id,
36 | title,
37 | description
38 | }: UpdateTodoPropsI): Promise> => {
39 | const res = await supabase().from('todos').update({ title, description }).eq('id', id);
40 | return res;
41 | };
42 |
43 | export const DeleteTodo = async (todo_id: string): Promise> => {
44 | const res = await supabase().from('todos').delete().eq('id', todo_id);
45 | revalidatePath('/');
46 | return res;
47 | };
48 |
--------------------------------------------------------------------------------
/src/lib/API/Database/todos/queries.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
4 | import { PostgrestSingleResponse } from '@supabase/supabase-js';
5 | import { TodosT } from '@/lib/types/supabase';
6 | import { SupabaseDBError } from '@/lib/utils/error';
7 | import { SupabaseUser } from '@/lib/API/Services/supabase/user';
8 |
9 | export const GetTodosByUserId = async (): Promise> => {
10 | const user = await SupabaseUser();
11 | const user_id = user.id;
12 |
13 | const res: PostgrestSingleResponse = await supabase()
14 | .from('todos')
15 | .select()
16 | .eq('user_id', user_id);
17 |
18 | if (res.error) SupabaseDBError(res.error);
19 | return res;
20 | };
21 |
22 | export const GetTodoById = async (todo_id: string): Promise> => {
23 | const res: PostgrestSingleResponse = await supabase()
24 | .from('todos')
25 | .select()
26 | .eq('id', todo_id);
27 |
28 | if (res.error) SupabaseDBError(res.error);
29 | return res;
30 | };
31 |
32 | export const GetAllTodos = async (): Promise> => {
33 | const res: PostgrestSingleResponse = await supabase().from('todos').select();
34 | if (res.error) SupabaseDBError(res.error);
35 |
36 | return res;
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/API/Services/init/stripe.ts:
--------------------------------------------------------------------------------
1 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2 |
3 | export default stripe;
4 |
--------------------------------------------------------------------------------
/src/lib/API/Services/init/supabase.ts:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 |
3 | import { cookies } from 'next/headers';
4 | import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
5 | import { Database } from '../../../../../supabase/types';
6 |
7 | //https://github.com/vercel/next.js/issues/45371
8 |
9 | //import type { Database } from '@/lib/database.types'
10 |
11 | export const SupabaseServerClient = () => {
12 | const cookieStore = cookies();
13 | return createServerActionClient({ cookies: () => cookieStore });
14 | };
15 |
--------------------------------------------------------------------------------
/src/lib/API/Services/stripe/customer.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import stripe from '@/lib/API/Services/init/stripe';
4 | import Stripe from 'stripe';
5 | import { CustomerPropsT } from '@/lib/types/stripe';
6 | import { StripeError } from '@/lib/utils/error';
7 |
8 | export const RetrieveSubscription = async (
9 | subscription_id: string
10 | ): Promise => {
11 | let subscription: Stripe.Subscription;
12 |
13 | try {
14 | subscription = await stripe.subscriptions.retrieve(subscription_id as string);
15 | } catch (err) {
16 | StripeError(err);
17 | }
18 |
19 | return subscription;
20 | };
21 |
22 | export const UpdateStripeCustomerEmail = async ({
23 | customer,
24 | email
25 | }: CustomerPropsT): Promise => {
26 | let customerRes: Stripe.Customer;
27 |
28 | try {
29 | customerRes = await stripe.customers.update(customer, {
30 | email
31 | });
32 | } catch (err) {
33 | StripeError(err);
34 | }
35 |
36 | return customerRes;
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/API/Services/stripe/session.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import stripe from '@/lib/API/Services/init/stripe';
4 | import config from '@/lib/config/auth';
5 | import { PortalSessionT, CreatePortalSessionPropsT } from '@/lib/types/stripe';
6 | import Stripe from 'stripe';
7 | import { StripeError } from '@/lib/utils/error';
8 | import { GetProfileByUserId } from '../../Database/profile/queries';
9 | import { SupabaseUser } from '../supabase/user';
10 | import configuration from '@/lib/config/site';
11 |
12 | interface createCheckoutProps {
13 | price: string;
14 | }
15 |
16 | export const createCheckoutSession = async ({ price }: createCheckoutProps) => {
17 | const { redirects } = config;
18 | const { toBilling, toSubscription } = redirects;
19 |
20 | const user = await SupabaseUser();
21 | const user_id = user.id;
22 | const customer_email = user.email;
23 | const origin = configuration.url;
24 |
25 | let session: Stripe.Checkout.Session;
26 |
27 | try {
28 | session = await stripe.checkout.sessions.create({
29 | line_items: [
30 | {
31 | price,
32 | quantity: 1
33 | }
34 | ],
35 | mode: 'subscription',
36 | success_url: `${origin}${toBilling}`,
37 | cancel_url: `${origin}${toSubscription}`,
38 | metadata: {
39 | user_id
40 | },
41 | customer_email,
42 | subscription_data: {
43 | trial_period_days: 14
44 | }
45 | });
46 | } catch (err) {
47 | StripeError(err);
48 | }
49 |
50 | return session;
51 | };
52 |
53 | export const createPortalSession = async (): Promise => {
54 | let portalSession: PortalSessionT;
55 |
56 | const user = await SupabaseUser();
57 | const profile = await GetProfileByUserId(user?.id);
58 | const customer = profile?.data?.[0]?.stripe_customer_id;
59 | const origin = configuration.url;
60 |
61 | try {
62 | portalSession = await stripe.billingPortal.sessions.create({
63 | customer,
64 | return_url: `${origin}${config.redirects.toSubscription}`
65 | });
66 | } catch (err) {
67 | StripeError(err);
68 | }
69 |
70 | return portalSession;
71 | };
72 |
--------------------------------------------------------------------------------
/src/lib/API/Services/stripe/webhook.ts:
--------------------------------------------------------------------------------
1 | import 'server-only';
2 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
3 | import Stripe from 'stripe';
4 | import { RetrieveSubscription } from './customer';
5 | import { StripeEvent } from '@/lib/types/stripe';
6 |
7 | const WebhookEvents = {
8 | subscription_updated: 'customer.subscription.updated',
9 | checkout_session_completed: 'checkout.session.completed'
10 | };
11 |
12 | export const WebhookEventHandler = async (event: StripeEvent) => {
13 | // Handle the event
14 | switch (event.type) {
15 | case WebhookEvents.checkout_session_completed: {
16 | const session = event.data.object;
17 |
18 | const user_db_id = session.metadata.user_id;
19 |
20 | const subscription: Stripe.Subscription = await RetrieveSubscription(session.subscription);
21 |
22 | const stripe_customer_id = subscription.customer as string;
23 | const statusSub = subscription.status as string;
24 |
25 | const dataSub = {
26 | id: subscription.id,
27 | price_id: subscription.items.data[0].price.id,
28 | status: statusSub,
29 | created_at: new Date(Date.now()).toString(),
30 | period_starts_at: new Date(subscription.current_period_start * 1000).toString(),
31 | period_ends_at: new Date(subscription.current_period_end * 1000).toString()
32 | };
33 |
34 | const subscriptionRes = await supabase().from('subscriptions').insert(dataSub);
35 | if (subscriptionRes?.error) throw subscriptionRes.error;
36 |
37 | const dataUser = {
38 | stripe_customer_id,
39 | subscription_id: subscription.id
40 | };
41 |
42 | const profileRes = await supabase().from('profiles').update(dataUser).eq('id', user_db_id);
43 | if (profileRes?.error) throw profileRes.error;
44 |
45 | console.log('Stripe Customer Successfully Created');
46 | break;
47 | }
48 | case WebhookEvents.subscription_updated: {
49 | // Incorrect infered type, need to override.
50 | const subscription = event.data.object as unknown as Stripe.Subscription;
51 |
52 | const dataSub = {
53 | id: subscription.id,
54 | price_id: subscription.items.data[0].price.id,
55 | status: subscription.status,
56 | created_at: new Date(Date.now()).toString(),
57 | period_starts_at: new Date(subscription.current_period_start * 1000).toString(),
58 | period_ends_at: new Date(subscription.current_period_end * 1000).toString()
59 | };
60 |
61 | const { error } = await supabase()
62 | .from('subscriptions')
63 | .update(dataSub)
64 | .eq('id', subscription.id);
65 |
66 | if (error) throw error;
67 |
68 | break;
69 | }
70 | default:
71 | // Unexpected event type
72 | console.log(`Unhandled event type ${event.type}.`);
73 | }
74 | };
75 |
76 | /*
77 |
78 | Webhook triggers can be triggered by stripe CLI to similate webhook events. Copy and paste into terminal.
79 |
80 | stripe.exe trigger checkout.session.completed --add checkout_session:metadata.user_id={REPLACE WITH A SUPABASE USER ID}
81 |
82 | stripe.exe trigger customer.subscription.updated
83 |
84 | stripe.exe trigger invoice.paid
85 |
86 | ngrok setup can also be used to directly trigger events from the app. See ngrok stripe webhook guide.
87 | */
88 |
--------------------------------------------------------------------------------
/src/lib/API/Services/supabase/auth.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
3 | import config from '@/lib/config/auth';
4 | import { SupabaseAuthError } from '@/lib/utils/error';
5 |
6 | export const SupabaseSignUp = async (email: string, password: string) => {
7 | const res = await supabase().auth.signUp({
8 | email,
9 | password
10 | });
11 | return res;
12 | };
13 |
14 | export const SupabaseSignIn = async (email: string, password: string) => {
15 | const res = await supabase().auth.signInWithPassword({
16 | email,
17 | password
18 | });
19 | return res;
20 | };
21 |
22 | export const SupabaseSignOut = async () => {
23 | const res = await supabase().auth.signOut();
24 | if (res.error) SupabaseAuthError(res.error);
25 | return res;
26 | };
27 |
28 | export const SupabaseSignInWithGoogle = async () => {
29 | const res = await supabase().auth.signInWithOAuth({
30 | provider: 'google'
31 | });
32 | return res;
33 | };
34 |
35 | export const SupabaseSignInWithMagicLink = async (email: string) => {
36 | const res = await supabase().auth.signInWithOtp({
37 | email: `${email}`,
38 | options: {
39 | emailRedirectTo: `${process.env.NEXT_PUBLIC_DOMAIN}${config.redirects.callback}`
40 | }
41 | });
42 | return res;
43 | };
44 |
45 | export const SupabaseUpdateEmail = async (email: string) => {
46 | const res = await supabase().auth.updateUser({ email });
47 | return res;
48 | };
49 |
50 | export const SupabaseUpdatePassword = async (password: string) => {
51 | const res = await supabase().auth.updateUser({ password });
52 | return res;
53 | };
54 |
55 | export const SupabaseResetPasswordEmail = async (email: string) => {
56 | const redirectTo = `${process.env.NEXT_PUBLIC_DOMAIN}${config.redirects.toProfile}`;
57 | const res = await supabase().auth.resetPasswordForEmail(email, {
58 | redirectTo
59 | });
60 | return res;
61 | };
62 |
--------------------------------------------------------------------------------
/src/lib/API/Services/supabase/user.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { SupabaseServerClient as supabase } from '@/lib/API/Services/init/supabase';
4 | import { SupabaseAuthError } from '@/lib/utils/error';
5 |
6 | export const SupabaseSession = async () => {
7 | const res = await supabase().auth.getSession();
8 | if (res.error) SupabaseAuthError(res.error);
9 | return res;
10 | };
11 |
12 | export const SupabaseUser = async () => {
13 | const res = await supabase().auth.getSession();
14 | if (res.error) SupabaseAuthError(res.error);
15 | return res?.data?.session?.user;
16 | };
17 |
--------------------------------------------------------------------------------
/src/lib/config/api.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | errorMessageGeneral: 'Something Went Wrong',
3 | swrKeys: {
4 | getMyTodos: 'GetMyTodos',
5 | getAllTodos: 'GetAllTodos'
6 | }
7 | };
8 |
9 | export default config;
10 |
--------------------------------------------------------------------------------
/src/lib/config/auth.ts:
--------------------------------------------------------------------------------
1 | const config = {
2 | routes: {
3 | login: {
4 | link: '/auth/login'
5 | },
6 | signup: {
7 | link: '/auth/signup'
8 | },
9 | forgotPassword: {
10 | link: '/auth/forgot-password'
11 | },
12 | magiclink: {
13 | link: '/auth/magic-link'
14 | }
15 | },
16 | redirects: {
17 | toDashboard: '/dashboard/main',
18 | toSubscription: '/dashboard/settings/subscription',
19 | toBilling: '/dashboard/settings/billing',
20 | requireAuth: '/auth/auth-required',
21 | authConfirm: '/auth/auth-confirm',
22 | callback: '/api/auth-callback',
23 | toProfile: '/dashboard/settings/profile',
24 | requireSub: '/dashboard/settings/subscription-required',
25 | toAddSub: '/dashboard/settings/add-subscription'
26 | }
27 | };
28 |
29 | export default config;
30 |
--------------------------------------------------------------------------------
/src/lib/config/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { Icons } from '@/components/Icons';
2 | import { IntervalE } from '../types/enums';
3 |
4 | const configuration = {
5 | routes: [
6 | { title: 'Overview', link: '/dashboard/main', icon: Icons.Home },
7 | { title: 'Todos', link: '/dashboard/todos/create', icon: Icons.Laptop },
8 | { title: 'Settings', link: '/dashboard/settings/profile', icon: Icons.Settings }
9 | ],
10 | subroutes: {
11 | todos: [
12 | { title: 'Create', link: '/dashboard/todos/create' },
13 | { title: 'My Todos', link: '/dashboard/todos/my-todos' },
14 | { title: 'All Todos', link: '/dashboard/todos/list-todos' }
15 | ],
16 | settings: [
17 | { title: 'Profile', link: '/dashboard/settings/profile' },
18 | { title: 'Billing', link: '/dashboard/settings/billing' },
19 | { title: 'Subscription', link: '/dashboard/settings/subscription' }
20 | ]
21 | },
22 | products: [
23 | {
24 | name: 'Basic',
25 | description: 'Best for hobby or individual Projects',
26 | features: ['Unlimited Posts', '10 Users', '1000 API requests', 'Email Support'],
27 | plans: [
28 | {
29 | name: 'Basic Monthly',
30 | interval: IntervalE.MONTHLY,
31 | price: '10',
32 | price_id: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC_MONTHLY,
33 | isPopular: true
34 | },
35 | {
36 | name: 'Basic Annual',
37 | interval: IntervalE.YEARLY,
38 | price: '100',
39 | price_id: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_BASIC_YEARLY,
40 | isPopular: false
41 | }
42 | ]
43 | },
44 | {
45 | name: 'Pro',
46 | description: 'Best for Teams or organizations',
47 | features: [
48 | 'Unlimited Posts',
49 | 'Unlimited Users',
50 | 'Unlimited API Requests',
51 | 'Priority Support'
52 | ],
53 | plans: [
54 | {
55 | name: 'Pro Monthly',
56 | interval: IntervalE.MONTHLY,
57 | price: '20',
58 | price_id: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM_MONTHLY,
59 | isPopular: false
60 | },
61 | {
62 | name: 'Pro Annual',
63 | interval: IntervalE.YEARLY,
64 | price: '200',
65 | price_id: process.env.NEXT_PUBLIC_STRIPE_PRICE_ID_PREMIUM_YEARLY,
66 | isPopular: false
67 | }
68 | ]
69 | }
70 | ]
71 | };
72 |
73 | export default configuration;
74 |
--------------------------------------------------------------------------------
/src/lib/config/marketing.ts:
--------------------------------------------------------------------------------
1 | import { Metadata } from 'next';
2 | import siteConfig from './site';
3 |
4 | const config = {
5 | routes: [
6 | { title: 'Pricing', link: '/pricing' },
7 | { title: 'FAQ', link: '/faq' }
8 | ],
9 | footer_nav: {
10 | about: {
11 | title: 'About',
12 | routes: [
13 | { title: 'Pricing', link: '/pricing' },
14 | { title: 'FAQs', link: '/faq' }
15 | ]
16 | },
17 | resources: {
18 | title: 'Resources',
19 | routes: [
20 | { title: 'Blog', link: '/' },
21 | { title: 'Docs', link: '/' }
22 | ]
23 | },
24 | legal: {
25 | title: 'Legal',
26 | routes: [
27 | { title: 'Privacy Policy', link: '/' },
28 | { title: 'Terms and Conditions', link: '/' }
29 | ]
30 | }
31 | },
32 | metadate: {
33 | title: {
34 | default: siteConfig.name,
35 | template: `%s | ${siteConfig.name}`
36 | },
37 | description: siteConfig.description,
38 | keywords: ['Next.js', 'React', 'Tailwind CSS', 'Server Components', 'Radix UI'],
39 | authors: [
40 | {
41 | name: ''
42 | }
43 | ],
44 | creator: '',
45 | themeColor: [
46 | { media: '(prefers-color-scheme: light)', color: 'white' },
47 | { media: '(prefers-color-scheme: dark)', color: 'black' }
48 | ],
49 | openGraph: {
50 | type: 'website',
51 | locale: 'en_US',
52 | url: siteConfig.url,
53 | title: siteConfig.name,
54 | description: siteConfig.description,
55 | siteName: siteConfig.name
56 | },
57 | twitter: {
58 | card: 'summary_large_image',
59 | title: siteConfig.name,
60 | description: siteConfig.description,
61 | images: [`${siteConfig.url}/og.jpg`],
62 | creator: ''
63 | },
64 | icons: {
65 | icon: '/favicon.ico',
66 | shortcut: '/favicon-16x16.png',
67 | apple: '/apple-touch-icon.png'
68 | },
69 | manifest: `${siteConfig.url}/site.webmanifest`
70 | },
71 | copy: {}
72 | };
73 |
74 | export default config;
75 |
--------------------------------------------------------------------------------
/src/lib/config/site.ts:
--------------------------------------------------------------------------------
1 | const siteConfig = {
2 | name: 'Saas Starter Kit',
3 | alt_name: 'My SAAS',
4 | description: 'An open source Saas boilerplate with Nextjs and Supabase.',
5 | url: process.env.NEXT_PUBLIC_DOMAIN,
6 | ogImage: '',
7 | loading_bar_color: '#ADD8E6',
8 | links: {
9 | twitter: 'https://twitter.com',
10 | github: 'https://github.com',
11 | linkedin: 'https://linkedin.com'
12 | }
13 | };
14 |
15 | export default siteConfig;
16 |
--------------------------------------------------------------------------------
/src/lib/types/enums.ts:
--------------------------------------------------------------------------------
1 | export enum IntervalE {
2 | MONTHLY = 'Monthly',
3 | YEARLY = 'Yearly'
4 | }
5 |
--------------------------------------------------------------------------------
/src/lib/types/readme.md:
--------------------------------------------------------------------------------
1 | This directory contains types, emums and form validations.
2 |
--------------------------------------------------------------------------------
/src/lib/types/stripe.ts:
--------------------------------------------------------------------------------
1 | /* Stripe Billing Portal Types */
2 | export type PortalSessionT = {
3 | id: string;
4 | object: string;
5 | configuration: string;
6 | created: number;
7 | customer: string;
8 | flow: null;
9 | livemode: boolean;
10 | locale: null;
11 | on_behalf_of: null;
12 | return_url: string;
13 | url: string;
14 | };
15 |
16 | export type PortalSessionReqPropsT = {
17 | customer: string;
18 | };
19 |
20 | export type CreatePortalSessionPropsT = {
21 | customer: string;
22 | origin: string;
23 | };
24 |
25 | /* Stripe Checkout session types */
26 |
27 | export type CreateCheckoutSessionPropsT = {
28 | price: string;
29 | };
30 |
31 | export type CheckoutSessionReqPropsT = {
32 | price: string;
33 | };
34 |
35 | // imported Stripe Event from 'stripe' type has outdated keys/values
36 | export type StripeEvent = {
37 | type: string;
38 | data: {
39 | object: {
40 | id: string;
41 | metadata: {
42 | user_id: string;
43 | };
44 | subscription: string;
45 | status: string;
46 | };
47 | previous_attributes: object | null;
48 | };
49 | };
50 |
51 | export type CustomerPropsT = {
52 | customer: string;
53 | email: string;
54 | };
55 |
56 | export type CustomerReqPropsT = {
57 | customer: string;
58 | email: string;
59 | };
60 |
--------------------------------------------------------------------------------
/src/lib/types/supabase.ts:
--------------------------------------------------------------------------------
1 | import { Session, User } from '@supabase/supabase-js';
2 | import { AuthError } from '@supabase/supabase-js';
3 |
4 | export type ProfileT = {
5 | display_name?: string | null;
6 | id: string;
7 | stripe_customer_id?: string | null;
8 | subscription_id?: string | null;
9 | };
10 |
11 | export type SubscriptionT = {
12 | created_at: string | Date | null;
13 | id: string;
14 | period_ends_at: string | null;
15 | period_starts_at: string | null;
16 | price_id: string;
17 | status: string;
18 | };
19 |
20 | export type TodosT = {
21 | author: string | null;
22 | description: string | null;
23 | id: string;
24 | title: string | null;
25 | user_id: string;
26 | };
27 |
28 | export type TSupabaseUserSession =
29 | | {
30 | user: User;
31 | session: Session;
32 | }
33 | | {
34 | user: null;
35 | session: null;
36 | };
37 |
38 | export interface SupabaseAuthErrorProps {
39 | error: AuthError;
40 | data: TSupabaseUserSession;
41 | email: string;
42 | }
43 |
44 | export interface SupbaseAuthError {
45 | isError: boolean;
46 | }
47 |
--------------------------------------------------------------------------------
/src/lib/types/todos.ts:
--------------------------------------------------------------------------------
1 | export type TodoT = {
2 | id?: string;
3 | title: string;
4 | description: string;
5 | author?: string;
6 | user_id?: string;
7 | };
8 |
--------------------------------------------------------------------------------
/src/lib/types/types.ts:
--------------------------------------------------------------------------------
1 | import { LucideIcon } from 'lucide-react';
2 | import { IntervalE } from './enums';
3 | import { AuthError, PostgrestError } from '@supabase/supabase-js';
4 |
5 | export type NavItem = {
6 | title: string;
7 | link: string;
8 | };
9 |
10 | export type NavItemSidebar = {
11 | title: string;
12 | link: string;
13 | icon: LucideIcon;
14 | };
15 |
16 | export interface LayoutProps {
17 | children: React.ReactNode;
18 | }
19 |
20 | export interface PlanI {
21 | name: string;
22 | interval?: IntervalE;
23 | price?: string;
24 | price_id?: string;
25 | isPopular?: boolean;
26 | }
27 |
28 | export interface ProductI {
29 | name: string;
30 | description: string;
31 | features: string[];
32 | plans: PlanI[];
33 | }
34 |
35 | export type ServerError = AuthError | PostgrestError | null;
36 |
--------------------------------------------------------------------------------
/src/lib/types/validations.ts:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as z from 'zod';
4 |
5 | export const authFormSchema = z.object({
6 | email: z
7 | .string({
8 | required_error: 'Please select an email to display.'
9 | })
10 | .email(),
11 | password: z
12 | .string()
13 | .min(8, {
14 | message: 'Password must be at least 8 characters.'
15 | })
16 | .max(30, {
17 | message: 'Password must not be longer than 30 characters.'
18 | })
19 | });
20 |
21 | export const todoFormSchema = z.object({
22 | title: z
23 | .string({
24 | required_error: 'Please enter a Title.'
25 | })
26 | .max(30, {
27 | message: 'Title must not be longer than 30 characters.'
28 | }),
29 | description: z.string().min(8, {
30 | message: 'Description Must be at least 8 characters'
31 | })
32 | });
33 |
34 | export const DisplayNameFormSchema = z.object({
35 | display_name: z
36 | .string()
37 | .min(2, {
38 | message: 'Display Name must be at least 2 characters.'
39 | })
40 | .max(30, {
41 | message: 'Display Name must not be longer than 30 characters.'
42 | })
43 | });
44 |
45 | export const EmailFormSchema = z.object({
46 | email: z.string().email()
47 | });
48 |
49 | export const UpdatePasswordFormSchema = z.object({
50 | password: z
51 | .string()
52 | .min(8, {
53 | message: 'Password must be at least 8 characters.'
54 | })
55 | .max(30, {
56 | message: 'Password must not be longer than 30 characters.'
57 | })
58 | });
59 |
60 | export type DisplayNameFormValues = z.infer;
61 | export type EmailFormValues = z.infer;
62 | export type UpdatePasswordFormValues = z.infer;
63 | export type todoFormValues = z.infer;
64 | export type authFormValues = z.infer;
65 |
--------------------------------------------------------------------------------
/src/lib/utils/error.ts:
--------------------------------------------------------------------------------
1 | import { AuthError, PostgrestError } from '@supabase/supabase-js';
2 | import { AxiosError } from 'axios';
3 | import Stripe from 'stripe';
4 |
5 | export const handleError = (error: any) => {
6 | //general or custom error handler
7 | throw error;
8 | };
9 |
10 | export const StripeError = (err: Stripe.errors.StripeError) => {
11 | if (err) {
12 | console.log(err);
13 | throw err;
14 | }
15 | };
16 |
17 | export const SupabaseAuthError = (err: AuthError) => {
18 | if (err) {
19 | console.log(err);
20 | throw err;
21 | }
22 | };
23 |
24 | export const SupabaseDBError = (err: PostgrestError) => {
25 | if (err) {
26 | console.log(err);
27 | throw err;
28 | }
29 | };
30 |
31 | export const AxiosHandleError = (err: AxiosError) => {
32 | if (err) {
33 | console.log(err);
34 | throw err;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/lib/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | // place helper utility functions here.
2 |
3 | import { ClassValue, clsx } from 'clsx';
4 | import { twMerge } from 'tailwind-merge';
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/utils/hooks.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | // @see https://usehooks.com/useLockBodyScroll.
4 | export function useLockBody() {
5 | React.useLayoutEffect((): (() => void) => {
6 | const originalStyle: string = window.getComputedStyle(document.body).overflow;
7 | document.body.style.overflow = 'hidden';
8 | return () => (document.body.style.overflow = originalStyle);
9 | }, []);
10 | }
11 |
--------------------------------------------------------------------------------
/src/styles/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 | import { type ThemeProviderProps } from 'next-themes/dist/types';
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children} ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Inter } from 'next/font/google';
2 |
3 | export const InterFont = Inter({
4 | subsets: ['latin'],
5 | display: 'swap',
6 | variable: '--font-inter'
7 | });
8 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 222.2 47.4% 11.2%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 222.2 84% 4.9%;
26 | --radius: 0.3rem;
27 | }
28 |
29 | .dark {
30 | --background: 222.2 84% 4.9%;
31 | --foreground: 210 40% 98%;
32 | --card: 222.2 84% 4.9%;
33 | --card-foreground: 210 40% 98%;
34 | --popover: 222.2 84% 4.9%;
35 | --popover-foreground: 210 40% 98%;
36 | --primary: 210 40% 98%;
37 | --primary-foreground: 222.2 47.4% 11.2%;
38 | --secondary: 217.2 32.6% 17.5%;
39 | --secondary-foreground: 210 40% 98%;
40 | --muted: 217.2 32.6% 17.5%;
41 | --muted-foreground: 215 20.2% 65.1%;
42 | --accent: 217.2 32.6% 17.5%;
43 | --accent-foreground: 210 40% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 210 40% 98%;
46 | --border: 217.2 32.6% 17.5%;
47 | --input: 217.2 32.6% 17.5%;
48 | --ring: 212.7 26.8% 83.9;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the
2 | # working directory name when running `supabase init`.
3 | project_id = "Nextjs_supabase"
4 |
5 | [api]
6 | enabled = true
7 | # Port to use for the API URL.
8 | port = 54321
9 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
10 | # endpoints. public and storage are always included.
11 | schemas = ["public", "storage", "graphql_public"]
12 | # Extra schemas to add to the search_path of every request. public is always included.
13 | extra_search_path = ["public", "extensions"]
14 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
15 | # for accidental or malicious requests.
16 | max_rows = 1000
17 |
18 | [db]
19 | # Port to use for the local database URL.
20 | port = 54322
21 | # Port used by db diff command to initialise the shadow database.
22 | shadow_port = 54320
23 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
24 | # server_version;` on the remote database to check.
25 | major_version = 15
26 |
27 | [db.pooler]
28 | enabled = false
29 | # Port to use for the local connection pooler.
30 | port = 54329
31 | # Specifies when a server connection can be reused by other clients.
32 | # Configure one of the supported pooler modes: `transaction`, `session`.
33 | pool_mode = "transaction"
34 | # How many server connections to allow per user/database pair.
35 | default_pool_size = 20
36 | # Maximum number of client connections allowed.
37 | max_client_conn = 100
38 |
39 | [realtime]
40 | enabled = true
41 | # Bind realtime via either IPv4 or IPv6. (default: IPv6)
42 | # ip_version = "IPv6"
43 |
44 | [studio]
45 | enabled = true
46 | # Port to use for Supabase Studio.
47 | port = 54323
48 | # External URL of the API server that frontend connects to.
49 | api_url = "http://localhost"
50 |
51 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
52 | # are monitored, and you can view the emails that would have been sent from the web interface.
53 | [inbucket]
54 | enabled = true
55 | # Port to use for the email testing server web interface.
56 | port = 54324
57 | # Uncomment to expose additional ports for testing user applications that send emails.
58 | # smtp_port = 54325
59 | # pop3_port = 54326
60 |
61 | [storage]
62 | enabled = true
63 | # The maximum file size allowed (e.g. "5MB", "500KB").
64 | file_size_limit = "50MiB"
65 |
66 | [auth]
67 | enabled = true
68 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
69 | # in emails.
70 | site_url = "http://localhost:3000"
71 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
72 | additional_redirect_urls = ["https://localhost:3000"]
73 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
74 | jwt_expiry = 3600
75 | # If disabled, the refresh token will never expire.
76 | enable_refresh_token_rotation = true
77 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
78 | # Requires enable_refresh_token_rotation = true.
79 | refresh_token_reuse_interval = 10
80 | # Allow/disallow new user signups to your project.
81 | enable_signup = true
82 |
83 | [auth.email]
84 | # Allow/disallow new user signups via email to your project.
85 | enable_signup = true
86 | # If enabled, a user will be required to confirm any email change on both the old, and new email
87 | # addresses. If disabled, only the new email is required to confirm.
88 | double_confirm_changes = true
89 | # If enabled, users need to confirm their email address before signing in.
90 | enable_confirmations = false
91 |
92 | # Uncomment to customize email template
93 | # [auth.email.template.invite]
94 | # subject = "You have been invited"
95 | # content_path = "./supabase/templates/invite.html"
96 |
97 | [auth.sms]
98 | # Allow/disallow new user signups via SMS to your project.
99 | enable_signup = true
100 | # If enabled, users need to confirm their phone number before signing in.
101 | enable_confirmations = false
102 |
103 | # Use pre-defined map of phone number to OTP for testing.
104 | [auth.sms.test_otp]
105 | # 4152127777 = "123456"
106 |
107 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
108 | [auth.sms.twilio]
109 | enabled = false
110 | account_sid = ""
111 | message_service_sid = ""
112 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
113 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
114 |
115 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
116 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
117 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
118 | [auth.external.apple]
119 | enabled = false
120 | client_id = ""
121 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
122 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
123 | # Overrides the default auth redirectUrl.
124 | redirect_uri = ""
125 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
126 | # or any other third-party OIDC providers.
127 | url = ""
128 |
129 | [analytics]
130 | enabled = false
131 | port = 54327
132 | vector_port = 54328
133 | # Configure one of the supported backends: `postgres`, `bigquery`.
134 | backend = "postgres"
135 |
--------------------------------------------------------------------------------
/supabase/functions/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true
4 | }
5 |
--------------------------------------------------------------------------------
/supabase/migrations/20230927195226_remote_schema.sql:
--------------------------------------------------------------------------------
1 |
2 | SET statement_timeout = 0;
3 | SET lock_timeout = 0;
4 | SET idle_in_transaction_session_timeout = 0;
5 | SET client_encoding = 'UTF8';
6 | SET standard_conforming_strings = on;
7 | SELECT pg_catalog.set_config('search_path', '', false);
8 | SET check_function_bodies = false;
9 | SET xmloption = content;
10 | SET client_min_messages = warning;
11 | SET row_security = off;
12 |
13 | CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
14 |
15 | CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
16 |
17 | CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
18 |
19 | CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
20 |
21 | CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
22 |
23 | CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
24 |
25 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
26 |
27 | CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger"
28 | LANGUAGE "plpgsql" SECURITY DEFINER
29 | SET "search_path" TO 'public'
30 | AS $$
31 | begin
32 | insert into public.profiles (id)
33 | values (new.id);
34 | return new;
35 | end;
36 | $$;
37 |
38 | ALTER FUNCTION "public"."handle_new_user"() OWNER TO "postgres";
39 |
40 | SET default_tablespace = '';
41 |
42 | SET default_table_access_method = "heap";
43 |
44 | CREATE TABLE IF NOT EXISTS "public"."profiles" (
45 | "id" "uuid" NOT NULL,
46 | "stripe_customer_id" character varying,
47 | "display_name" character varying,
48 | "subscription_id" "text"
49 | );
50 |
51 | ALTER TABLE "public"."profiles" OWNER TO "postgres";
52 |
53 | CREATE TABLE IF NOT EXISTS "public"."subscriptions" (
54 | "id" "text" NOT NULL,
55 | "price_id" "text" NOT NULL,
56 | "status" "text" NOT NULL,
57 | "created_at" timestamp without time zone,
58 | "period_starts_at" timestamp without time zone,
59 | "period_ends_at" timestamp without time zone
60 | );
61 |
62 | ALTER TABLE "public"."subscriptions" OWNER TO "postgres";
63 |
64 | CREATE TABLE IF NOT EXISTS "public"."todos" (
65 | "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
66 | "title" character varying,
67 | "description" "text",
68 | "user_id" "uuid" NOT NULL,
69 | "author" character varying
70 | );
71 |
72 | ALTER TABLE "public"."todos" OWNER TO "postgres";
73 |
74 | ALTER TABLE ONLY "public"."profiles"
75 | ADD CONSTRAINT "profiles_pkey" PRIMARY KEY ("id");
76 |
77 | ALTER TABLE ONLY "public"."profiles"
78 | ADD CONSTRAINT "profiles_stripe_customer_id_key" UNIQUE ("stripe_customer_id");
79 |
80 | ALTER TABLE ONLY "public"."subscriptions"
81 | ADD CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id");
82 |
83 | ALTER TABLE ONLY "public"."todos"
84 | ADD CONSTRAINT "todos_pkey" PRIMARY KEY ("id");
85 |
86 | ALTER TABLE ONLY "public"."profiles"
87 | ADD CONSTRAINT "profiles_id_fkey" FOREIGN KEY ("id") REFERENCES "auth"."users"("id") ON DELETE CASCADE;
88 |
89 | ALTER TABLE ONLY "public"."profiles"
90 | ADD CONSTRAINT "profiles_subscription_id_fkey" FOREIGN KEY ("subscription_id") REFERENCES "public"."subscriptions"("id") ON DELETE SET NULL;
91 |
92 | ALTER TABLE ONLY "public"."todos"
93 | ADD CONSTRAINT "todos_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."profiles"("id") ON DELETE CASCADE;
94 |
95 | CREATE POLICY "Enable delete for users based on user_id" ON "public"."todos" FOR DELETE USING (("auth"."uid"() = "user_id"));
96 |
97 | CREATE POLICY "Enable insert for authenticated users only" ON "public"."todos" FOR INSERT TO "authenticated" WITH CHECK (true);
98 |
99 | CREATE POLICY "Enable read access for all users" ON "public"."todos" FOR SELECT USING (true);
100 |
101 | CREATE POLICY "Enable update for users based on email" ON "public"."todos" FOR UPDATE USING (("auth"."uid"() = "user_id")) WITH CHECK (("auth"."uid"() = "user_id"));
102 |
103 | ALTER TABLE "public"."todos" ENABLE ROW LEVEL SECURITY;
104 |
105 | GRANT USAGE ON SCHEMA "public" TO "postgres";
106 | GRANT USAGE ON SCHEMA "public" TO "anon";
107 | GRANT USAGE ON SCHEMA "public" TO "authenticated";
108 | GRANT USAGE ON SCHEMA "public" TO "service_role";
109 |
110 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "anon";
111 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "authenticated";
112 | GRANT ALL ON FUNCTION "public"."handle_new_user"() TO "service_role";
113 |
114 | GRANT ALL ON TABLE "public"."profiles" TO "anon";
115 | GRANT ALL ON TABLE "public"."profiles" TO "authenticated";
116 | GRANT ALL ON TABLE "public"."profiles" TO "service_role";
117 |
118 | GRANT ALL ON TABLE "public"."subscriptions" TO "anon";
119 | GRANT ALL ON TABLE "public"."subscriptions" TO "authenticated";
120 | GRANT ALL ON TABLE "public"."subscriptions" TO "service_role";
121 |
122 | GRANT ALL ON TABLE "public"."todos" TO "anon";
123 | GRANT ALL ON TABLE "public"."todos" TO "authenticated";
124 | GRANT ALL ON TABLE "public"."todos" TO "service_role";
125 |
126 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres";
127 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon";
128 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated";
129 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role";
130 |
131 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres";
132 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon";
133 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated";
134 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role";
135 |
136 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres";
137 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon";
138 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated";
139 | ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role";
140 |
141 | RESET ALL;
142 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Saas-Starter-Kit/Saas-Kit-supabase/dd3c49cb3697edb90a5b59c8fe04d8af216bfcea/supabase/seed.sql
--------------------------------------------------------------------------------
/supabase/types.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json | undefined }
7 | | Json[]
8 |
9 | export interface Database {
10 | public: {
11 | Tables: {
12 | profiles: {
13 | Row: {
14 | display_name: string | null
15 | id: string
16 | stripe_customer_id: string | null
17 | subscription_id: string | null
18 | }
19 | Insert: {
20 | display_name?: string | null
21 | id: string
22 | stripe_customer_id?: string | null
23 | subscription_id?: string | null
24 | }
25 | Update: {
26 | display_name?: string | null
27 | id?: string
28 | stripe_customer_id?: string | null
29 | subscription_id?: string | null
30 | }
31 | Relationships: [
32 | {
33 | foreignKeyName: "profiles_id_fkey"
34 | columns: ["id"]
35 | referencedRelation: "users"
36 | referencedColumns: ["id"]
37 | },
38 | {
39 | foreignKeyName: "profiles_subscription_id_fkey"
40 | columns: ["subscription_id"]
41 | referencedRelation: "subscriptions"
42 | referencedColumns: ["id"]
43 | }
44 | ]
45 | }
46 | subscriptions: {
47 | Row: {
48 | created_at: string | null
49 | id: string
50 | period_ends_at: string | null
51 | period_starts_at: string | null
52 | price_id: string
53 | status: string
54 | }
55 | Insert: {
56 | created_at?: string | null
57 | id: string
58 | period_ends_at?: string | null
59 | period_starts_at?: string | null
60 | price_id: string
61 | status: string
62 | }
63 | Update: {
64 | created_at?: string | null
65 | id?: string
66 | period_ends_at?: string | null
67 | period_starts_at?: string | null
68 | price_id?: string
69 | status?: string
70 | }
71 | Relationships: []
72 | }
73 | todos: {
74 | Row: {
75 | author: string | null
76 | description: string | null
77 | id: string
78 | title: string | null
79 | user_id: string
80 | }
81 | Insert: {
82 | author?: string | null
83 | description?: string | null
84 | id?: string
85 | title?: string | null
86 | user_id: string
87 | }
88 | Update: {
89 | author?: string | null
90 | description?: string | null
91 | id?: string
92 | title?: string | null
93 | user_id?: string
94 | }
95 | Relationships: [
96 | {
97 | foreignKeyName: "todos_user_id_fkey"
98 | columns: ["user_id"]
99 | referencedRelation: "profiles"
100 | referencedColumns: ["id"]
101 | }
102 | ]
103 | }
104 | }
105 | Views: {
106 | [_ in never]: never
107 | }
108 | Functions: {
109 | [_ in never]: never
110 | }
111 | Enums: {
112 | [_ in never]: never
113 | }
114 | CompositeTypes: {
115 | [_ in never]: never
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | darkMode: 'class',
5 | content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
6 | theme: {
7 | extend: {
8 | colors: {
9 | border: 'hsl(var(--border))',
10 | input: 'hsl(var(--input))',
11 | ring: 'hsl(var(--ring))',
12 | background: 'hsl(var(--background))',
13 | foreground: 'hsl(var(--foreground))',
14 | primary: {
15 | DEFAULT: 'hsl(var(--primary))',
16 | foreground: 'hsl(var(--primary-foreground))'
17 | },
18 | secondary: {
19 | DEFAULT: 'hsl(var(--secondary))',
20 | foreground: 'hsl(var(--secondary-foreground))'
21 | },
22 | destructive: {
23 | DEFAULT: 'hsl(var(--destructive))',
24 | foreground: 'hsl(var(--destructive-foreground))'
25 | },
26 | muted: {
27 | DEFAULT: 'hsl(var(--muted))',
28 | foreground: 'hsl(var(--muted-foreground))'
29 | },
30 | accent: {
31 | DEFAULT: 'hsl(var(--accent))',
32 | foreground: 'hsl(var(--accent-foreground))'
33 | },
34 | popover: {
35 | DEFAULT: 'hsl(var(--popover))',
36 | foreground: 'hsl(var(--popover-foreground))'
37 | },
38 | card: {
39 | DEFAULT: 'hsl(var(--card))',
40 | foreground: 'hsl(var(--card-foreground))'
41 | }
42 | },
43 | borderRadius: {
44 | lg: `var(--radius)`,
45 | md: `calc(var(--radius) - 2px)`,
46 | sm: 'calc(var(--radius) - 4px)'
47 | },
48 | fontFamily: {
49 | sans: ['var(--font-inter)']
50 | },
51 | animation: {
52 | fadeIn: 'fadeIn 700ms ease-in-out'
53 | },
54 | keyframes: () => ({
55 | fadeIn: {
56 | '0%': { opacity: '0' },
57 | '100%': { opacity: '1' }
58 | }
59 | })
60 | }
61 | },
62 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
63 | };
64 |
65 | export default config;
66 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": false,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": [
27 | "next-env.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx",
30 | ".next/types/**/*.ts",
31 | "src/app/(auth)/protected"
32 | ],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------