├── .eslintrc.json
├── .gitignore
├── README.md
├── app
├── api
│ ├── auth
│ │ └── [kindeAuth]
│ │ │ └── route.ts
│ └── webhook
│ │ └── stripe
│ │ └── route.ts
├── components
│ ├── DashboardNav.tsx
│ ├── Navbar.tsx
│ ├── Submitbuttons.tsx
│ ├── Themetoggle.tsx
│ ├── UserNav.tsx
│ └── theme-provider.tsx
├── dashboard
│ ├── billing
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── new
│ │ ├── [id]
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── page.tsx
│ └── settings
│ │ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── lib
│ ├── db.ts
│ └── stripe.ts
├── page.tsx
└── payment
│ ├── cancelled
│ └── page.tsx
│ └── success
│ └── page.tsx
├── components.json
├── components
└── ui
│ ├── avatar.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ └── textarea.tsx
├── lib
└── utils.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
└── schema.prisma
├── public
├── next.svg
└── vercel.svg
├── tailwind.config.ts
└── tsconfig.json
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 🚀 Build a SaaS Application using Next.js 14, Stripe, Kinde, Prisma, Supabase, and Tailwind! Learn step-by-step and elevate your development skills.
2 |
3 | - 🚀 Kinde Auth: https://dub.sh/xeU8r3v
4 |
5 |
6 | - 👨🏻💻 GitHub Repository: https://www.janmarshal.com/courses/build-a-next-js-14-blog-or-react-sanity-io-tailwind-css-shadcn-ui
7 | - 🌍 My Website: https://www.janmarshal.com
8 | - 📧 Business ONLY: jan@alenix.de
9 |
10 | Resources used:
11 | - Next.js: https://nextjs.org
12 | - Kinde: https://dub.sh/xeU8r3v
13 | - Tailwind.css: https://tailwindcss.com
14 | - Shadcn/UI: https://ui.shadcn.com
15 | - Stripe: https://stripe.com
16 | - Prisma: https://prisma.io
17 | - Supabase: https://supabase.com
18 |
19 | Features:
20 |
21 | - 🌐 nextjs App Router
22 | - 🔐 Kinde Authentication
23 | - 📧 Passwordless Auth
24 | - 🔑 OAuth (Google and GitHub)
25 | - 💿 supabase Database
26 | - 💨 prisma Orm
27 | - 🎨 Styling with tailwindcss and shadcn UI
28 | - ✅ Change the color scheme to your liking
29 | - 💵 stripe for subscription handling
30 | - 🪝 Implementation of Stripe Webhooks
31 | - 😶🌫️ Deployment to vercel
32 |
33 | - Pending States
34 | - Cache Revalidation
35 | - Stripe Customer Portal
36 | - Stripe Checkout page
37 | - Server side implementation
38 | - Add Notes, View Notes, Edit Notes, Delete Nodes
39 |
40 |
41 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
42 |
43 | ## Getting Started
44 |
45 | First, run the development server:
46 |
47 | ```bash
48 | npm run dev
49 | # or
50 | yarn dev
51 | # or
52 | pnpm dev
53 | # or
54 | bun dev
55 | ```
56 |
57 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
58 |
59 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
60 |
61 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
62 |
63 | ## Learn More
64 |
65 | To learn more about Next.js, take a look at the following resources:
66 |
67 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
68 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
69 |
70 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
71 |
72 | ## Deploy on Vercel
73 |
74 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
75 |
76 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
77 |
--------------------------------------------------------------------------------
/app/api/auth/[kindeAuth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";
2 |
3 | export const GET = handleAuth();
4 |
--------------------------------------------------------------------------------
/app/api/webhook/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { stripe } from "@/app/lib/stripe";
2 | import { headers } from "next/headers";
3 | import Stripe from "stripe";
4 | import prisma from "@/app/lib/db";
5 |
6 | export async function POST(req: Request) {
7 | const body = await req.text();
8 |
9 | const signature = headers().get("Stripe-Signature") as string;
10 |
11 | let event: Stripe.Event;
12 |
13 | try {
14 | event = stripe.webhooks.constructEvent(
15 | body,
16 | signature,
17 | process.env.STRIPE_WEBHOOK_SECRET as string
18 | );
19 | } catch (error: unknown) {
20 | return new Response("webhook error", { status: 400 });
21 | }
22 |
23 | const session = event.data.object as Stripe.Checkout.Session;
24 |
25 | if (event.type === "checkout.session.completed") {
26 | const subscription = await stripe.subscriptions.retrieve(
27 | session.subscription as string
28 | );
29 | const customerId = String(session.customer);
30 |
31 | const user = await prisma.user.findUnique({
32 | where: {
33 | stripeCustomerId: customerId,
34 | },
35 | });
36 |
37 | if (!user) throw new Error("User not found...");
38 |
39 | await prisma.subscription.create({
40 | data: {
41 | stripeSubscriptionId: subscription.id,
42 | userId: user.id,
43 | currentPeriodStart: subscription.current_period_start,
44 | currentPeriodEnd: subscription.current_period_end,
45 | status: subscription.status,
46 | planId: subscription.items.data[0].plan.id,
47 | invterval: String(subscription.items.data[0].plan.interval),
48 | },
49 | });
50 | }
51 |
52 | if (event.type === "invoice.payment_succeeded") {
53 | const subscription = await stripe.subscriptions.retrieve(
54 | session.subscription as string
55 | );
56 |
57 | await prisma.subscription.update({
58 | where: {
59 | stripeSubscriptionId: subscription.id,
60 | },
61 | data: {
62 | planId: subscription.items.data[0].price.id,
63 | currentPeriodStart: subscription.current_period_start,
64 | currentPeriodEnd: subscription.current_period_end,
65 | status: subscription.status,
66 | },
67 | });
68 | }
69 |
70 | return new Response(null, { status: 200 });
71 | }
72 |
--------------------------------------------------------------------------------
/app/components/DashboardNav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { CreditCard, Home, Settings } from "lucide-react";
5 | import Link from "next/link";
6 | import { usePathname } from "next/navigation";
7 | import { navItems } from "./UserNav";
8 |
9 | export function DashboardNav() {
10 | const pathname = usePathname();
11 | console.log(pathname);
12 | return (
13 |
14 | {navItems.map((item, index) => (
15 |
16 |
22 |
23 | {item.name}
24 |
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/app/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { ThemeToggle } from "./Themetoggle";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | RegisterLink,
6 | LoginLink,
7 | } from "@kinde-oss/kinde-auth-nextjs/components";
8 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
9 | import { UserNav } from "./UserNav";
10 |
11 | export async function Navbar() {
12 | const { isAuthenticated, getUser } = getKindeServerSession();
13 | const user = await getUser();
14 |
15 | return (
16 |
17 |
18 |
19 |
20 | MarshalSaas
21 |
22 |
23 |
24 |
25 |
26 |
27 | {(await isAuthenticated()) ? (
28 |
33 | ) : (
34 |
35 |
36 | Sign In
37 |
38 |
39 |
40 | Sign Up
41 |
42 |
43 | )}
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/app/components/Submitbuttons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Loader2, Trash } from "lucide-react";
5 | import { useFormStatus } from "react-dom";
6 |
7 | export function SubmitButton() {
8 | const { pending } = useFormStatus();
9 | return (
10 | <>
11 | {pending ? (
12 |
13 | Please Wait
14 |
15 | ) : (
16 |
17 | Save Now
18 |
19 | )}
20 | >
21 | );
22 | }
23 |
24 | export function StripeSubscriptionCreationButton() {
25 | const { pending } = useFormStatus();
26 |
27 | return (
28 | <>
29 | {pending ? (
30 |
31 | Please Wait
32 |
33 | ) : (
34 |
35 | Create Subscription
36 |
37 | )}
38 | >
39 | );
40 | }
41 |
42 | export function StripePortal() {
43 | const { pending } = useFormStatus();
44 |
45 | return (
46 | <>
47 | {pending ? (
48 |
49 | Please Wait
50 |
51 | ) : (
52 |
53 | View payment details
54 |
55 | )}
56 | >
57 | );
58 | }
59 |
60 | export function TrashDelete() {
61 | const { pending } = useFormStatus();
62 |
63 | return (
64 | <>
65 | {pending ? (
66 |
67 |
68 |
69 | ) : (
70 |
71 |
72 |
73 | )}
74 | >
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/app/components/Themetoggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | import { Button } from "@/components/ui/button";
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 |
15 | export function ThemeToggle() {
16 | const { setTheme } = useTheme();
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 | Light
30 |
31 | setTheme("dark")}>
32 | Dark
33 |
34 | setTheme("system")}>
35 | System
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/components/UserNav.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuGroup,
7 | DropdownMenuItem,
8 | DropdownMenuLabel,
9 | DropdownMenuSeparator,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 | import { CreditCard, DoorClosed, Home, Settings } from "lucide-react";
13 | import { LogoutLink } from "@kinde-oss/kinde-auth-nextjs/components";
14 |
15 | import Link from "next/link";
16 |
17 | export const navItems = [
18 | { name: "Home", href: "/dashboard", icon: Home },
19 | { name: "Settings", href: "/dashboard/settings", icon: Settings },
20 | { name: "Billing", href: "/dashboard/billing", icon: CreditCard },
21 | ];
22 |
23 | export function UserNav({
24 | name,
25 | email,
26 | image,
27 | }: {
28 | name: string;
29 | email: string;
30 | image: string;
31 | }) {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 | Jan
39 |
40 |
41 |
42 |
43 |
44 |
45 |
{name}
46 |
47 | {email}
48 |
49 |
50 |
51 |
52 |
53 | {navItems.map((item, index) => (
54 |
55 |
59 | {item.name}
60 |
61 |
62 |
63 |
64 |
65 | ))}
66 |
67 |
68 |
69 |
73 |
74 | Logout{" "}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/app/components/theme-provider.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 |
--------------------------------------------------------------------------------
/app/dashboard/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { CheckCircle2 } from "lucide-react";
10 | import prisma from "@/app/lib/db";
11 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
12 | import { getStripeSession, stripe } from "@/app/lib/stripe";
13 | import { redirect } from "next/navigation";
14 | import {
15 | StripePortal,
16 | StripeSubscriptionCreationButton,
17 | } from "@/app/components/Submitbuttons";
18 | import { unstable_noStore as noStore } from "next/cache";
19 |
20 | const featureItems = [
21 | { name: "Lorem Ipsum something" },
22 | { name: "Lorem Ipsum something" },
23 | { name: "Lorem Ipsum something" },
24 | { name: "Lorem Ipsum something" },
25 | { name: "Lorem Ipsum something" },
26 | ];
27 |
28 | async function getData(userId: string) {
29 | noStore();
30 | const data = await prisma.subscription.findUnique({
31 | where: {
32 | userId: userId,
33 | },
34 | select: {
35 | status: true,
36 | user: {
37 | select: {
38 | stripeCustomerId: true,
39 | },
40 | },
41 | },
42 | });
43 |
44 | return data;
45 | }
46 |
47 | export default async function BillingPage() {
48 | const { getUser } = getKindeServerSession();
49 | const user = await getUser();
50 | const data = await getData(user?.id as string);
51 |
52 | async function createSubscription() {
53 | "use server";
54 |
55 | const dbUser = await prisma.user.findUnique({
56 | where: {
57 | id: user?.id,
58 | },
59 | select: {
60 | stripeCustomerId: true,
61 | },
62 | });
63 |
64 | if (!dbUser?.stripeCustomerId) {
65 | throw new Error("Unable to get customer id");
66 | }
67 |
68 | const subscriptionUrl = await getStripeSession({
69 | customerId: dbUser.stripeCustomerId,
70 | domainUrl:
71 | process.env.NODE_ENV == "production"
72 | ? (process.env.PRODUCTION_URL as string)
73 | : "http://localhost:3000",
74 | priceId: process.env.STRIPE_PRICE_ID as string,
75 | });
76 |
77 | return redirect(subscriptionUrl);
78 | }
79 |
80 | async function createCustomerPortal() {
81 | "use server";
82 | const session = await stripe.billingPortal.sessions.create({
83 | customer: data?.user.stripeCustomerId as string,
84 | return_url:
85 | process.env.NODE_ENV === "production"
86 | ? (process.env.PRODUCTION_URL as string)
87 | : "http://localhost:3000/dashboard",
88 | });
89 |
90 | return redirect(session.url);
91 | }
92 |
93 | if (data?.status === "active") {
94 | return (
95 |
96 |
97 |
98 |
Subscription
99 |
100 | Settings reagding your subscription
101 |
102 |
103 |
104 |
105 |
106 |
107 | Edit Subscription
108 |
109 | Click on the button below, this will give you the opportunity to
110 | change your payment details and view your statement at the same
111 | time.
112 |
113 |
114 |
115 |
118 |
119 |
120 |
121 | );
122 | }
123 |
124 | return (
125 |
126 |
127 |
128 |
129 |
130 | Monthly
131 |
132 |
133 |
134 |
135 | $30 /mo
136 |
137 |
138 | Write as many notes as you want for $30 a Month
139 |
140 |
141 |
142 |
143 | {featureItems.map((item, index) => (
144 |
145 |
146 |
147 |
148 | {item.name}
149 |
150 | ))}
151 |
152 |
153 |
156 |
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/app/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { DashboardNav } from "../components/DashboardNav";
3 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
4 | import { redirect } from "next/navigation";
5 | import prisma from "../lib/db";
6 | import { stripe } from "../lib/stripe";
7 | import { unstable_noStore as noStore } from "next/cache";
8 |
9 | async function getData({
10 | email,
11 | id,
12 | firstName,
13 | lastName,
14 | profileImage,
15 | }: {
16 | email: string;
17 | id: string;
18 | firstName: string | undefined | null;
19 | lastName: string | undefined | null;
20 | profileImage: string | undefined | null;
21 | }) {
22 | noStore();
23 | const user = await prisma.user.findUnique({
24 | where: {
25 | id: id,
26 | },
27 | select: {
28 | id: true,
29 | stripeCustomerId: true,
30 | },
31 | });
32 |
33 | if (!user) {
34 | const name = `${firstName ?? ""} ${lastName ?? ""}`;
35 | await prisma.user.create({
36 | data: {
37 | id: id,
38 | email: email,
39 | name: name,
40 | },
41 | });
42 | }
43 |
44 | if (!user?.stripeCustomerId) {
45 | const data = await stripe.customers.create({
46 | email: email,
47 | });
48 |
49 | await prisma.user.update({
50 | where: {
51 | id: id,
52 | },
53 | data: {
54 | stripeCustomerId: data.id,
55 | },
56 | });
57 | }
58 | }
59 |
60 | export default async function DashboardLayout({
61 | children,
62 | }: {
63 | children: ReactNode;
64 | }) {
65 | const { getUser } = getKindeServerSession();
66 | const user = await getUser();
67 | if (!user) {
68 | return redirect("/");
69 | }
70 | await getData({
71 | email: user.email as string,
72 | firstName: user.given_name as string,
73 | id: user.id as string,
74 | lastName: user.family_name as string,
75 | profileImage: user.picture,
76 | });
77 |
78 | return (
79 |
80 |
81 |
84 |
{children}
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/app/dashboard/new/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SubmitButton } from "@/app/components/Submitbuttons";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { Input } from "@/components/ui/input";
12 | import { Label } from "@/components/ui/label";
13 | import { Textarea } from "@/components/ui/textarea";
14 | import Link from "next/link";
15 | import prisma from "@/app/lib/db";
16 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
17 | import { redirect } from "next/navigation";
18 | import { revalidatePath, unstable_noStore as noStore } from "next/cache";
19 |
20 | async function getData({ userId, noteId }: { userId: string; noteId: string }) {
21 | noStore();
22 | const data = await prisma.note.findUnique({
23 | where: {
24 | id: noteId,
25 | userId: userId,
26 | },
27 | select: {
28 | title: true,
29 | description: true,
30 | id: true,
31 | },
32 | });
33 |
34 | return data;
35 | }
36 |
37 | export default async function DynamicRoute({
38 | params,
39 | }: {
40 | params: { id: string };
41 | }) {
42 | const { getUser } = getKindeServerSession();
43 | const user = await getUser();
44 | const data = await getData({ userId: user?.id as string, noteId: params.id });
45 |
46 | async function postData(formData: FormData) {
47 | "use server";
48 |
49 | if (!user) throw new Error("you are not allowed");
50 |
51 | const title = formData.get("title") as string;
52 | const description = formData.get("description") as string;
53 |
54 | await prisma.note.update({
55 | where: {
56 | id: data?.id,
57 | userId: user.id,
58 | },
59 | data: {
60 | description: description,
61 | title: title,
62 | },
63 | });
64 |
65 | revalidatePath("/dashboard");
66 |
67 | return redirect("/dashboard");
68 | }
69 | return (
70 |
71 |
108 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/app/dashboard/new/page.tsx:
--------------------------------------------------------------------------------
1 | import { SubmitButton } from "@/app/components/Submitbuttons";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { Input } from "@/components/ui/input";
12 | import { Label } from "@/components/ui/label";
13 | import { Textarea } from "@/components/ui/textarea";
14 | import Link from "next/link";
15 | import prisma from "@/app/lib/db";
16 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
17 | import { redirect } from "next/navigation";
18 | import { unstable_noStore as noStore } from "next/cache";
19 |
20 | export default async function NewNoteRoute() {
21 | noStore();
22 | const { getUser } = getKindeServerSession();
23 | const user = await getUser();
24 |
25 | async function postData(formData: FormData) {
26 | "use server";
27 |
28 | if (!user) {
29 | throw new Error("Not authorized");
30 | }
31 |
32 | const title = formData.get("title") as string;
33 | const description = formData.get("description") as string;
34 |
35 | await prisma.note.create({
36 | data: {
37 | userId: user?.id,
38 | description: description,
39 | title: title,
40 | },
41 | });
42 |
43 | return redirect("/dashboard");
44 | }
45 |
46 | return (
47 |
48 |
49 |
50 | New Note
51 |
52 | Right here you can now create your new notes
53 |
54 |
55 |
56 |
57 | Title
58 |
64 |
65 |
66 |
67 | Description
68 |
73 |
74 |
75 |
76 |
77 |
78 | Cancel
79 |
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import Link from "next/link";
3 | import prisma from "../lib/db";
4 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
5 | import { Edit, File, Trash } from "lucide-react";
6 | import { Card } from "@/components/ui/card";
7 |
8 | import { TrashDelete } from "../components/Submitbuttons";
9 | import { revalidatePath, unstable_noStore as noStore } from "next/cache";
10 |
11 | async function getData(userId: string) {
12 | noStore();
13 | const data = await prisma.user.findUnique({
14 | where: {
15 | id: userId,
16 | },
17 | select: {
18 | Notes: {
19 | select: {
20 | title: true,
21 | id: true,
22 | description: true,
23 | createdAt: true,
24 | },
25 | orderBy: {
26 | createdAt: "desc",
27 | },
28 | },
29 |
30 | Subscription: {
31 | select: {
32 | status: true,
33 | },
34 | },
35 | },
36 | });
37 |
38 | return data;
39 | }
40 |
41 | export default async function DashboardPage() {
42 | const { getUser } = getKindeServerSession();
43 | const user = await getUser();
44 | const data = await getData(user?.id as string);
45 |
46 | async function deleteNote(formData: FormData) {
47 | "use server";
48 |
49 | const noteId = formData.get("noteId") as string;
50 |
51 | await prisma.note.delete({
52 | where: {
53 | id: noteId,
54 | },
55 | });
56 |
57 | revalidatePath("/dasboard");
58 | }
59 | return (
60 |
61 |
62 |
63 |
Your Notes
64 |
65 | Here you can see and create new notes
66 |
67 |
68 |
69 | {data?.Subscription?.status === "active" ? (
70 |
71 | Create a new Note
72 |
73 | ) : (
74 |
75 | Create a new Note
76 |
77 | )}
78 |
79 |
80 | {data?.Notes.length == 0 ? (
81 |
82 |
83 |
84 |
85 |
86 |
87 | You dont have any notes created
88 |
89 |
90 | You currently dont have any notes. please create some so that you
91 | can see them right here.
92 |
93 |
94 | {data?.Subscription?.status === "active" ? (
95 |
96 | Create a new Note
97 |
98 | ) : (
99 |
100 | Create a new Note
101 |
102 | )}
103 |
104 | ) : (
105 |
106 | {data?.Notes.map((item) => (
107 |
111 |
112 |
113 | {item.title}
114 |
115 |
116 | {new Intl.DateTimeFormat("en-US", {
117 | dateStyle: "full",
118 | }).format(new Date(item.createdAt))}
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | ))}
135 |
136 | )}
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/app/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardFooter,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { Input } from "@/components/ui/input";
10 | import { Label } from "@/components/ui/label";
11 | import {
12 | Select,
13 | SelectContent,
14 | SelectGroup,
15 | SelectItem,
16 | SelectLabel,
17 | SelectTrigger,
18 | SelectValue,
19 | } from "@/components/ui/select";
20 | import prisma from "@/app/lib/db";
21 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
22 |
23 | import { SubmitButton } from "@/app/components/Submitbuttons";
24 | import { revalidatePath, unstable_noStore as noStore } from "next/cache";
25 |
26 | async function getData(userId: string) {
27 | noStore();
28 | const data = await prisma.user.findUnique({
29 | where: {
30 | id: userId,
31 | },
32 | select: {
33 | name: true,
34 | email: true,
35 | colorScheme: true,
36 | },
37 | });
38 |
39 | return data;
40 | }
41 |
42 | export default async function SettingPage() {
43 | const { getUser } = getKindeServerSession();
44 | const user = await getUser();
45 | const data = await getData(user?.id as string);
46 |
47 | async function postData(formData: FormData) {
48 | "use server";
49 |
50 | const name = formData.get("name") as string;
51 | const colorScheme = formData.get("color") as string;
52 |
53 | await prisma.user.update({
54 | where: {
55 | id: user?.id,
56 | },
57 | data: {
58 | name: name ?? undefined,
59 | colorScheme: colorScheme ?? undefined,
60 | },
61 | });
62 |
63 | revalidatePath("/", "layout");
64 | }
65 |
66 | return (
67 |
68 |
69 |
70 |
Settings
71 |
Your Profile settings
72 |
73 |
74 |
75 |
76 |
77 |
78 | General Data
79 |
80 | Please provide general information about yourself. Please dont
81 | forget to save
82 |
83 |
84 |
85 |
86 |
87 | Your Name
88 |
95 |
96 |
97 | Your Email
98 |
106 |
107 |
108 |
109 | Color Scheme
110 |
111 |
112 |
113 |
114 |
115 |
116 | Color
117 | Green
118 | Blue
119 | Violet
120 | Yellow
121 | Orange
122 | Red
123 | Rose
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ski043/Marshal-Saas/981f51dd474b648bc9dc0f60e5a4c26f963377a7/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .theme-zinc {
6 | --background: 0 0% 100%;
7 | --foreground: 240 10% 3.9%;
8 | --card: 0 0% 100%;
9 | --card-foreground: 240 10% 3.9%;
10 | --popover: 0 0% 100%;
11 | --popover-foreground: 240 10% 3.9%;
12 | --primary: 240 5.9% 10%;
13 | --primary-foreground: 0 0% 98%;
14 | --secondary: 240 4.8% 95.9%;
15 | --secondary-foreground: 240 5.9% 10%;
16 | --muted: 240 4.8% 95.9%;
17 | --muted-foreground: 240 3.8% 46.1%;
18 | --accent: 240 4.8% 95.9%;
19 | --accent-foreground: 240 5.9% 10%;
20 | --destructive: 0 84.2% 60.2%;
21 | --destructive-foreground: 0 0% 98%;
22 | --border: 240 5.9% 90%;
23 | --input: 240 5.9% 90%;
24 | --ring: 240 5.9% 10%;
25 | --radius: 0.5rem;
26 | }
27 |
28 | .dark .theme-zinc {
29 | --background: 240 10% 3.9%;
30 | --foreground: 0 0% 98%;
31 | --card: 240 10% 3.9%;
32 | --card-foreground: 0 0% 98%;
33 | --popover: 240 10% 3.9%;
34 | --popover-foreground: 0 0% 98%;
35 | --primary: 0 0% 98%;
36 | --primary-foreground: 240 5.9% 10%;
37 | --secondary: 240 3.7% 15.9%;
38 | --secondary-foreground: 0 0% 98%;
39 | --muted: 240 3.7% 15.9%;
40 | --muted-foreground: 240 5% 64.9%;
41 | --accent: 240 3.7% 15.9%;
42 | --accent-foreground: 0 0% 98%;
43 | --destructive: 0 62.8% 30.6%;
44 | --destructive-foreground: 0 0% 98%;
45 | --border: 240 3.7% 15.9%;
46 | --input: 240 3.7% 15.9%;
47 | --ring: 240 4.9% 83.9%;
48 | }
49 |
50 | .theme-slate {
51 | --background: 0 0% 100%;
52 | --foreground: 222.2 84% 4.9%;
53 | --card: 0 0% 100%;
54 | --card-foreground: 222.2 84% 4.9%;
55 | --popover: 0 0% 100%;
56 | --popover-foreground: 222.2 84% 4.9%;
57 | --primary: 222.2 47.4% 11.2%;
58 | --primary-foreground: 210 40% 98%;
59 | --secondary: 210 40% 96.1%;
60 | --secondary-foreground: 222.2 47.4% 11.2%;
61 | --muted: 210 40% 96.1%;
62 | --muted-foreground: 215.4 16.3% 46.9%;
63 | --accent: 210 40% 96.1%;
64 | --accent-foreground: 222.2 47.4% 11.2%;
65 | --destructive: 0 84.2% 60.2%;
66 | --destructive-foreground: 210 40% 98%;
67 | --border: 214.3 31.8% 91.4%;
68 | --input: 214.3 31.8% 91.4%;
69 | --ring: 222.2 84% 4.9%;
70 | --radius: 0.5rem;
71 | }
72 |
73 | .dark .theme-slate {
74 | --background: 222.2 84% 4.9%;
75 | --foreground: 210 40% 98%;
76 | --card: 222.2 84% 4.9%;
77 | --card-foreground: 210 40% 98%;
78 | --popover: 222.2 84% 4.9%;
79 | --popover-foreground: 210 40% 98%;
80 | --primary: 210 40% 98%;
81 | --primary-foreground: 222.2 47.4% 11.2%;
82 | --secondary: 217.2 32.6% 17.5%;
83 | --secondary-foreground: 210 40% 98%;
84 | --muted: 217.2 32.6% 17.5%;
85 | --muted-foreground: 215 20.2% 65.1%;
86 | --accent: 217.2 32.6% 17.5%;
87 | --accent-foreground: 210 40% 98%;
88 | --destructive: 0 62.8% 30.6%;
89 | --destructive-foreground: 210 40% 98%;
90 | --border: 217.2 32.6% 17.5%;
91 | --input: 217.2 32.6% 17.5%;
92 | --ring: 212.7 26.8% 83.9;
93 | }
94 |
95 | .theme-stone {
96 | --background: 0 0% 100%;
97 | --foreground: 20 14.3% 4.1%;
98 | --card: 0 0% 100%;
99 | --card-foreground: 20 14.3% 4.1%;
100 | --popover: 0 0% 100%;
101 | --popover-foreground: 20 14.3% 4.1%;
102 | --primary: 24 9.8% 10%;
103 | --primary-foreground: 60 9.1% 97.8%;
104 | --secondary: 60 4.8% 95.9%;
105 | --secondary-foreground: 24 9.8% 10%;
106 | --muted: 60 4.8% 95.9%;
107 | --muted-foreground: 25 5.3% 44.7%;
108 | --accent: 60 4.8% 95.9%;
109 | --accent-foreground: 24 9.8% 10%;
110 | --destructive: 0 84.2% 60.2%;
111 | --destructive-foreground: 60 9.1% 97.8%;
112 | --border: 20 5.9% 90%;
113 | --input: 20 5.9% 90%;
114 | --ring: 20 14.3% 4.1%;
115 | --radius: 0.5rem;
116 | }
117 |
118 | .dark .theme-stone {
119 | --background: 20 14.3% 4.1%;
120 | --foreground: 60 9.1% 97.8%;
121 | --card: 20 14.3% 4.1%;
122 | --card-foreground: 60 9.1% 97.8%;
123 | --popover: 20 14.3% 4.1%;
124 | --popover-foreground: 60 9.1% 97.8%;
125 | --primary: 60 9.1% 97.8%;
126 | --primary-foreground: 24 9.8% 10%;
127 | --secondary: 12 6.5% 15.1%;
128 | --secondary-foreground: 60 9.1% 97.8%;
129 | --muted: 12 6.5% 15.1%;
130 | --muted-foreground: 24 5.4% 63.9%;
131 | --accent: 12 6.5% 15.1%;
132 | --accent-foreground: 60 9.1% 97.8%;
133 | --destructive: 0 62.8% 30.6%;
134 | --destructive-foreground: 60 9.1% 97.8%;
135 | --border: 12 6.5% 15.1%;
136 | --input: 12 6.5% 15.1%;
137 | --ring: 24 5.7% 82.9%;
138 | }
139 |
140 | .theme-gray {
141 | --background: 0 0% 100%;
142 | --foreground: 224 71.4% 4.1%;
143 | --card: 0 0% 100%;
144 | --card-foreground: 224 71.4% 4.1%;
145 | --popover: 0 0% 100%;
146 | --popover-foreground: 224 71.4% 4.1%;
147 | --primary: 220.9 39.3% 11%;
148 | --primary-foreground: 210 20% 98%;
149 | --secondary: 220 14.3% 95.9%;
150 | --secondary-foreground: 220.9 39.3% 11%;
151 | --muted: 220 14.3% 95.9%;
152 | --muted-foreground: 220 8.9% 46.1%;
153 | --accent: 220 14.3% 95.9%;
154 | --accent-foreground: 220.9 39.3% 11%;
155 | --destructive: 0 84.2% 60.2%;
156 | --destructive-foreground: 210 20% 98%;
157 | --border: 220 13% 91%;
158 | --input: 220 13% 91%;
159 | --ring: 224 71.4% 4.1%;
160 | --radius: 0.5rem;
161 | }
162 |
163 | .dark .theme-gray {
164 | --background: 224 71.4% 4.1%;
165 | --foreground: 210 20% 98%;
166 | --card: 224 71.4% 4.1%;
167 | --card-foreground: 210 20% 98%;
168 | --popover: 224 71.4% 4.1%;
169 | --popover-foreground: 210 20% 98%;
170 | --primary: 210 20% 98%;
171 | --primary-foreground: 220.9 39.3% 11%;
172 | --secondary: 215 27.9% 16.9%;
173 | --secondary-foreground: 210 20% 98%;
174 | --muted: 215 27.9% 16.9%;
175 | --muted-foreground: 217.9 10.6% 64.9%;
176 | --accent: 215 27.9% 16.9%;
177 | --accent-foreground: 210 20% 98%;
178 | --destructive: 0 62.8% 30.6%;
179 | --destructive-foreground: 210 20% 98%;
180 | --border: 215 27.9% 16.9%;
181 | --input: 215 27.9% 16.9%;
182 | --ring: 216 12.2% 83.9%;
183 | }
184 |
185 | .theme-neutral {
186 | --background: 0 0% 100%;
187 | --foreground: 0 0% 3.9%;
188 | --card: 0 0% 100%;
189 | --card-foreground: 0 0% 3.9%;
190 | --popover: 0 0% 100%;
191 | --popover-foreground: 0 0% 3.9%;
192 | --primary: 0 0% 9%;
193 | --primary-foreground: 0 0% 98%;
194 | --secondary: 0 0% 96.1%;
195 | --secondary-foreground: 0 0% 9%;
196 | --muted: 0 0% 96.1%;
197 | --muted-foreground: 0 0% 45.1%;
198 | --accent: 0 0% 96.1%;
199 | --accent-foreground: 0 0% 9%;
200 | --destructive: 0 84.2% 60.2%;
201 | --destructive-foreground: 0 0% 98%;
202 | --border: 0 0% 89.8%;
203 | --input: 0 0% 89.8%;
204 | --ring: 0 0% 3.9%;
205 | --radius: 0.5rem;
206 | }
207 |
208 | .dark .theme-neutral {
209 | --background: 0 0% 3.9%;
210 | --foreground: 0 0% 98%;
211 | --card: 0 0% 3.9%;
212 | --card-foreground: 0 0% 98%;
213 | --popover: 0 0% 3.9%;
214 | --popover-foreground: 0 0% 98%;
215 | --primary: 0 0% 98%;
216 | --primary-foreground: 0 0% 9%;
217 | --secondary: 0 0% 14.9%;
218 | --secondary-foreground: 0 0% 98%;
219 | --muted: 0 0% 14.9%;
220 | --muted-foreground: 0 0% 63.9%;
221 | --accent: 0 0% 14.9%;
222 | --accent-foreground: 0 0% 98%;
223 | --destructive: 0 62.8% 30.6%;
224 | --destructive-foreground: 0 0% 98%;
225 | --border: 0 0% 14.9%;
226 | --input: 0 0% 14.9%;
227 | --ring: 0 0% 83.1%;
228 | }
229 |
230 | .theme-red {
231 | --background: 0 0% 100%;
232 | --foreground: 0 0% 3.9%;
233 | --card: 0 0% 100%;
234 | --card-foreground: 0 0% 3.9%;
235 | --popover: 0 0% 100%;
236 | --popover-foreground: 0 0% 3.9%;
237 | --primary: 0 72.2% 50.6%;
238 | --primary-foreground: 0 85.7% 97.3%;
239 | --secondary: 0 0% 96.1%;
240 | --secondary-foreground: 0 0% 9%;
241 | --muted: 0 0% 96.1%;
242 | --muted-foreground: 0 0% 45.1%;
243 | --accent: 0 0% 96.1%;
244 | --accent-foreground: 0 0% 9%;
245 | --destructive: 0 84.2% 60.2%;
246 | --destructive-foreground: 0 0% 98%;
247 | --border: 0 0% 89.8%;
248 | --input: 0 0% 89.8%;
249 | --ring: 0 72.2% 50.6%;
250 | --radius: 0.5rem;
251 | }
252 |
253 | .dark .theme-red {
254 | --background: 0 0% 3.9%;
255 | --foreground: 0 0% 98%;
256 | --card: 0 0% 3.9%;
257 | --card-foreground: 0 0% 98%;
258 | --popover: 0 0% 3.9%;
259 | --popover-foreground: 0 0% 98%;
260 | --primary: 0 72.2% 50.6%;
261 | --primary-foreground: 0 85.7% 97.3%;
262 | --secondary: 0 0% 14.9%;
263 | --secondary-foreground: 0 0% 98%;
264 | --muted: 0 0% 14.9%;
265 | --muted-foreground: 0 0% 63.9%;
266 | --accent: 0 0% 14.9%;
267 | --accent-foreground: 0 0% 98%;
268 | --destructive: 0 62.8% 30.6%;
269 | --destructive-foreground: 0 0% 98%;
270 | --border: 0 0% 14.9%;
271 | --input: 0 0% 14.9%;
272 | --ring: 0 72.2% 50.6%;
273 | }
274 |
275 | .theme-rose {
276 | --background: 0 0% 100%;
277 | --foreground: 240 10% 3.9%;
278 | --card: 0 0% 100%;
279 | --card-foreground: 240 10% 3.9%;
280 | --popover: 0 0% 100%;
281 | --popover-foreground: 240 10% 3.9%;
282 | --primary: 346.8 77.2% 49.8%;
283 | --primary-foreground: 355.7 100% 97.3%;
284 | --secondary: 240 4.8% 95.9%;
285 | --secondary-foreground: 240 5.9% 10%;
286 | --muted: 240 4.8% 95.9%;
287 | --muted-foreground: 240 3.8% 46.1%;
288 | --accent: 240 4.8% 95.9%;
289 | --accent-foreground: 240 5.9% 10%;
290 | --destructive: 0 84.2% 60.2%;
291 | --destructive-foreground: 0 0% 98%;
292 | --border: 240 5.9% 90%;
293 | --input: 240 5.9% 90%;
294 | --ring: 346.8 77.2% 49.8%;
295 | --radius: 0.5rem;
296 | }
297 |
298 | .dark .theme-rose {
299 | --background: 20 14.3% 4.1%;
300 | --foreground: 0 0% 95%;
301 | --card: 24 9.8% 10%;
302 | --card-foreground: 0 0% 95%;
303 | --popover: 0 0% 9%;
304 | --popover-foreground: 0 0% 95%;
305 | --primary: 346.8 77.2% 49.8%;
306 | --primary-foreground: 355.7 100% 97.3%;
307 | --secondary: 240 3.7% 15.9%;
308 | --secondary-foreground: 0 0% 98%;
309 | --muted: 0 0% 15%;
310 | --muted-foreground: 240 5% 64.9%;
311 | --accent: 12 6.5% 15.1%;
312 | --accent-foreground: 0 0% 98%;
313 | --destructive: 0 62.8% 30.6%;
314 | --destructive-foreground: 0 85.7% 97.3%;
315 | --border: 240 3.7% 15.9%;
316 | --input: 240 3.7% 15.9%;
317 | --ring: 346.8 77.2% 49.8%;
318 | }
319 |
320 | .theme-orange {
321 | --background: 0 0% 100%;
322 | --foreground: 20 14.3% 4.1%;
323 | --card: 0 0% 100%;
324 | --card-foreground: 20 14.3% 4.1%;
325 | --popover: 0 0% 100%;
326 | --popover-foreground: 20 14.3% 4.1%;
327 | --primary: 24.6 95% 53.1%;
328 | --primary-foreground: 60 9.1% 97.8%;
329 | --secondary: 60 4.8% 95.9%;
330 | --secondary-foreground: 24 9.8% 10%;
331 | --muted: 60 4.8% 95.9%;
332 | --muted-foreground: 25 5.3% 44.7%;
333 | --accent: 60 4.8% 95.9%;
334 | --accent-foreground: 24 9.8% 10%;
335 | --destructive: 0 84.2% 60.2%;
336 | --destructive-foreground: 60 9.1% 97.8%;
337 | --border: 20 5.9% 90%;
338 | --input: 20 5.9% 90%;
339 | --ring: 24.6 95% 53.1%;
340 | --radius: 0.5rem;
341 | }
342 |
343 | .dark .theme-orange {
344 | --background: 20 14.3% 4.1%;
345 | --foreground: 60 9.1% 97.8%;
346 | --card: 20 14.3% 4.1%;
347 | --card-foreground: 60 9.1% 97.8%;
348 | --popover: 20 14.3% 4.1%;
349 | --popover-foreground: 60 9.1% 97.8%;
350 | --primary: 20.5 90.2% 48.2%;
351 | --primary-foreground: 60 9.1% 97.8%;
352 | --secondary: 12 6.5% 15.1%;
353 | --secondary-foreground: 60 9.1% 97.8%;
354 | --muted: 12 6.5% 15.1%;
355 | --muted-foreground: 24 5.4% 63.9%;
356 | --accent: 12 6.5% 15.1%;
357 | --accent-foreground: 60 9.1% 97.8%;
358 | --destructive: 0 72.2% 50.6%;
359 | --destructive-foreground: 60 9.1% 97.8%;
360 | --border: 12 6.5% 15.1%;
361 | --input: 12 6.5% 15.1%;
362 | --ring: 20.5 90.2% 48.2%;
363 | }
364 |
365 | .theme-green {
366 | --background: 0 0% 100%;
367 | --foreground: 240 10% 3.9%;
368 | --card: 0 0% 100%;
369 | --card-foreground: 240 10% 3.9%;
370 | --popover: 0 0% 100%;
371 | --popover-foreground: 240 10% 3.9%;
372 | --primary: 142.1 76.2% 36.3%;
373 | --primary-foreground: 355.7 100% 97.3%;
374 | --secondary: 240 4.8% 95.9%;
375 | --secondary-foreground: 240 5.9% 10%;
376 | --muted: 240 4.8% 95.9%;
377 | --muted-foreground: 240 3.8% 46.1%;
378 | --accent: 240 4.8% 95.9%;
379 | --accent-foreground: 240 5.9% 10%;
380 | --destructive: 0 84.2% 60.2%;
381 | --destructive-foreground: 0 0% 98%;
382 | --border: 240 5.9% 90%;
383 | --input: 240 5.9% 90%;
384 | --ring: 142.1 76.2% 36.3%;
385 | --radius: 0.5rem;
386 | }
387 |
388 | .dark .theme-green {
389 | --background: 20 14.3% 4.1%;
390 | --foreground: 0 0% 95%;
391 | --card: 24 9.8% 10%;
392 | --card-foreground: 0 0% 95%;
393 | --popover: 0 0% 9%;
394 | --popover-foreground: 0 0% 95%;
395 | --primary: 142.1 70.6% 45.3%;
396 | --primary-foreground: 144.9 80.4% 10%;
397 | --secondary: 240 3.7% 15.9%;
398 | --secondary-foreground: 0 0% 98%;
399 | --muted: 0 0% 15%;
400 | --muted-foreground: 240 5% 64.9%;
401 | --accent: 12 6.5% 15.1%;
402 | --accent-foreground: 0 0% 98%;
403 | --destructive: 0 62.8% 30.6%;
404 | --destructive-foreground: 0 85.7% 97.3%;
405 | --border: 240 3.7% 15.9%;
406 | --input: 240 3.7% 15.9%;
407 | --ring: 142.4 71.8% 29.2%;
408 | }
409 |
410 | .theme-blue {
411 | --background: 0 0% 100%;
412 | --foreground: 222.2 84% 4.9%;
413 | --card: 0 0% 100%;
414 | --card-foreground: 222.2 84% 4.9%;
415 | --popover: 0 0% 100%;
416 | --popover-foreground: 222.2 84% 4.9%;
417 | --primary: 221.2 83.2% 53.3%;
418 | --primary-foreground: 210 40% 98%;
419 | --secondary: 210 40% 96.1%;
420 | --secondary-foreground: 222.2 47.4% 11.2%;
421 | --muted: 210 40% 96.1%;
422 | --muted-foreground: 215.4 16.3% 46.9%;
423 | --accent: 210 40% 96.1%;
424 | --accent-foreground: 222.2 47.4% 11.2%;
425 | --destructive: 0 84.2% 60.2%;
426 | --destructive-foreground: 210 40% 98%;
427 | --border: 214.3 31.8% 91.4%;
428 | --input: 214.3 31.8% 91.4%;
429 | --ring: 221.2 83.2% 53.3%;
430 | --radius: 0.5rem;
431 | }
432 |
433 | .dark .theme-blue {
434 | --background: 222.2 84% 4.9%;
435 | --foreground: 210 40% 98%;
436 | --card: 222.2 84% 4.9%;
437 | --card-foreground: 210 40% 98%;
438 | --popover: 222.2 84% 4.9%;
439 | --popover-foreground: 210 40% 98%;
440 | --primary: 217.2 91.2% 59.8%;
441 | --primary-foreground: 222.2 47.4% 11.2%;
442 | --secondary: 217.2 32.6% 17.5%;
443 | --secondary-foreground: 210 40% 98%;
444 | --muted: 217.2 32.6% 17.5%;
445 | --muted-foreground: 215 20.2% 65.1%;
446 | --accent: 217.2 32.6% 17.5%;
447 | --accent-foreground: 210 40% 98%;
448 | --destructive: 0 62.8% 30.6%;
449 | --destructive-foreground: 210 40% 98%;
450 | --border: 217.2 32.6% 17.5%;
451 | --input: 217.2 32.6% 17.5%;
452 | --ring: 224.3 76.3% 48%;
453 | }
454 |
455 | .theme-yellow {
456 | --background: 0 0% 100%;
457 | --foreground: 20 14.3% 4.1%;
458 | --card: 0 0% 100%;
459 | --card-foreground: 20 14.3% 4.1%;
460 | --popover: 0 0% 100%;
461 | --popover-foreground: 20 14.3% 4.1%;
462 | --primary: 47.9 95.8% 53.1%;
463 | --primary-foreground: 26 83.3% 14.1%;
464 | --secondary: 60 4.8% 95.9%;
465 | --secondary-foreground: 24 9.8% 10%;
466 | --muted: 60 4.8% 95.9%;
467 | --muted-foreground: 25 5.3% 44.7%;
468 | --accent: 60 4.8% 95.9%;
469 | --accent-foreground: 24 9.8% 10%;
470 | --destructive: 0 84.2% 60.2%;
471 | --destructive-foreground: 60 9.1% 97.8%;
472 | --border: 20 5.9% 90%;
473 | --input: 20 5.9% 90%;
474 | --ring: 20 14.3% 4.1%;
475 | --radius: 0.5rem;
476 | }
477 |
478 | .dark .theme-yellow {
479 | --background: 20 14.3% 4.1%;
480 | --foreground: 60 9.1% 97.8%;
481 | --card: 20 14.3% 4.1%;
482 | --card-foreground: 60 9.1% 97.8%;
483 | --popover: 20 14.3% 4.1%;
484 | --popover-foreground: 60 9.1% 97.8%;
485 | --primary: 47.9 95.8% 53.1%;
486 | --primary-foreground: 26 83.3% 14.1%;
487 | --secondary: 12 6.5% 15.1%;
488 | --secondary-foreground: 60 9.1% 97.8%;
489 | --muted: 12 6.5% 15.1%;
490 | --muted-foreground: 24 5.4% 63.9%;
491 | --accent: 12 6.5% 15.1%;
492 | --accent-foreground: 60 9.1% 97.8%;
493 | --destructive: 0 62.8% 30.6%;
494 | --destructive-foreground: 60 9.1% 97.8%;
495 | --border: 12 6.5% 15.1%;
496 | --input: 12 6.5% 15.1%;
497 | --ring: 35.5 91.7% 32.9%;
498 | }
499 |
500 | .theme-violet {
501 | --background: 0 0% 100%;
502 | --foreground: 224 71.4% 4.1%;
503 | --card: 0 0% 100%;
504 | --card-foreground: 224 71.4% 4.1%;
505 | --popover: 0 0% 100%;
506 | --popover-foreground: 224 71.4% 4.1%;
507 | --primary: 262.1 83.3% 57.8%;
508 | --primary-foreground: 210 20% 98%;
509 | --secondary: 220 14.3% 95.9%;
510 | --secondary-foreground: 220.9 39.3% 11%;
511 | --muted: 220 14.3% 95.9%;
512 | --muted-foreground: 220 8.9% 46.1%;
513 | --accent: 220 14.3% 95.9%;
514 | --accent-foreground: 220.9 39.3% 11%;
515 | --destructive: 0 84.2% 60.2%;
516 | --destructive-foreground: 210 20% 98%;
517 | --border: 220 13% 91%;
518 | --input: 220 13% 91%;
519 | --ring: 262.1 83.3% 57.8%;
520 | --radius: 0.5rem;
521 | }
522 |
523 | .dark .theme-violet {
524 | --background: 224 71.4% 4.1%;
525 | --foreground: 210 20% 98%;
526 | --card: 224 71.4% 4.1%;
527 | --card-foreground: 210 20% 98%;
528 | --popover: 224 71.4% 4.1%;
529 | --popover-foreground: 210 20% 98%;
530 | --primary: 263.4 70% 50.4%;
531 | --primary-foreground: 210 20% 98%;
532 | --secondary: 215 27.9% 16.9%;
533 | --secondary-foreground: 210 20% 98%;
534 | --muted: 215 27.9% 16.9%;
535 | --muted-foreground: 217.9 10.6% 64.9%;
536 | --accent: 215 27.9% 16.9%;
537 | --accent-foreground: 210 20% 98%;
538 | --destructive: 0 62.8% 30.6%;
539 | --destructive-foreground: 210 20% 98%;
540 | --border: 215 27.9% 16.9%;
541 | --input: 215 27.9% 16.9%;
542 | --ring: 263.4 70% 50.4%;
543 | }
544 |
545 | @layer base {
546 | * {
547 | @apply border-border;
548 | }
549 | body {
550 | @apply bg-background text-foreground;
551 | }
552 | }
553 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { ThemeProvider } from "./components/theme-provider";
5 | import { Navbar } from "./components/Navbar";
6 | import prisma from "./lib/db";
7 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
8 | import { unstable_noStore as noStore } from "next/cache";
9 |
10 | const inter = Inter({ subsets: ["latin"] });
11 |
12 | export const metadata: Metadata = {
13 | title: "Create Next App",
14 | description: "Generated by create next app",
15 | };
16 |
17 | async function getData(userId: string) {
18 | noStore();
19 | if (userId) {
20 | const data = await prisma.user.findUnique({
21 | where: {
22 | id: userId,
23 | },
24 | select: {
25 | colorScheme: true,
26 | },
27 | });
28 | return data;
29 | }
30 | }
31 |
32 | export default async function RootLayout({
33 | children,
34 | }: Readonly<{
35 | children: React.ReactNode;
36 | }>) {
37 | const { getUser } = getKindeServerSession();
38 | const user = await getUser();
39 | const data = await getData(user?.id as string);
40 | return (
41 |
42 |
45 |
51 |
52 | {children}
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | declare global {
8 | var prisma: undefined | ReturnType;
9 | }
10 |
11 | const prisma = globalThis.prisma ?? prismaClientSingleton();
12 |
13 | export default prisma;
14 |
15 | if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
16 |
--------------------------------------------------------------------------------
/app/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe";
2 |
3 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
4 | apiVersion: "2023-10-16",
5 | typescript: true,
6 | });
7 |
8 | export const getStripeSession = async ({
9 | priceId,
10 | domainUrl,
11 | customerId,
12 | }: {
13 | priceId: string;
14 | domainUrl: string;
15 | customerId: string;
16 | }) => {
17 | const session = await stripe.checkout.sessions.create({
18 | customer: customerId,
19 | mode: "subscription",
20 | billing_address_collection: "auto",
21 | line_items: [{ price: priceId, quantity: 1 }],
22 | payment_method_types: ["card"],
23 | customer_update: {
24 | address: "auto",
25 | name: "auto",
26 | },
27 | success_url: `${domainUrl}/payment/success`,
28 | cancel_url: `${domainUrl}/payment/cancelled`,
29 | });
30 |
31 | return session.url as string;
32 | };
33 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { RegisterLink } from "@kinde-oss/kinde-auth-nextjs/components";
3 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
4 | import { redirect } from "next/navigation";
5 |
6 | export default async function Home() {
7 | const { isAuthenticated } = getKindeServerSession();
8 |
9 | if (await isAuthenticated()) {
10 | return redirect("/dashboard");
11 | }
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 | Sort your notes easily
20 |
21 |
22 |
23 |
24 | Create Notes with ease
25 |
26 |
27 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
28 | nonumy eirmod tempor invidunt ut labore et d
29 |
30 |
31 |
32 |
33 |
34 |
35 | Sign Up for free
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/app/payment/cancelled/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card } from "@/components/ui/card";
3 | import { XIcon } from "lucide-react";
4 | import Link from "next/link";
5 |
6 | export default function CancelledRoute() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Payment Failed
17 |
18 |
19 | No worries, you wont be charged. Please try again
20 |
21 |
22 |
23 |
24 |
25 | Go back to Dashboard
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/app/payment/success/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card } from "@/components/ui/card";
3 | import { Check } from "lucide-react";
4 | import Link from "next/link";
5 |
6 | export default function SuccessRoute() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Payment Successfull
18 |
19 |
20 |
21 | Congrats on your subscription, please check your email for
22 | further instructions
23 |
24 |
25 |
26 |
27 |
28 | Go back to Dashboard
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/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"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/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"
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 &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/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, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
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 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marshal-saas",
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 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@kinde-oss/kinde-auth-nextjs": "^2.1.3",
14 | "@prisma/client": "^5.8.1",
15 | "@radix-ui/react-avatar": "^1.0.4",
16 | "@radix-ui/react-dropdown-menu": "^2.0.6",
17 | "@radix-ui/react-label": "^2.0.2",
18 | "@radix-ui/react-select": "^2.0.0",
19 | "@radix-ui/react-slot": "^1.0.2",
20 | "class-variance-authority": "^0.7.0",
21 | "clsx": "^2.1.0",
22 | "lucide-react": "^0.314.0",
23 | "next": "14.1.0",
24 | "next-themes": "^0.2.1",
25 | "react": "^18",
26 | "react-dom": "^18",
27 | "stripe": "^14.13.0",
28 | "tailwind-merge": "^2.2.0",
29 | "tailwindcss-animate": "^1.0.7"
30 | },
31 | "devDependencies": {
32 | "@types/node": "^20",
33 | "@types/react": "^18",
34 | "@types/react-dom": "^18",
35 | "autoprefixer": "^10.0.1",
36 | "eslint": "^8",
37 | "eslint-config-next": "14.1.0",
38 | "postcss": "^8",
39 | "prisma": "^5.8.1",
40 | "tailwindcss": "^3.3.0",
41 | "typescript": "^5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("DATABASE_URL")
11 | directUrl = env("DIRECT_URL")
12 | }
13 |
14 | model User {
15 | id String @id @unique
16 | name String?
17 | email String @unique
18 | stripeCustomerId String? @unique
19 | colorScheme String @default("theme-orange")
20 | Subscription Subscription?
21 | Notes Note[]
22 | }
23 |
24 | model Subscription {
25 | stripeSubscriptionId String @id @unique
26 | invterval String
27 | status String
28 | planId String
29 | currentPeriodStart Int
30 | currentPeriodEnd Int
31 | createdAt DateTime @default(now())
32 | updatedAt DateTime @updatedAt
33 | user User @relation(fields: [userId], references: [id])
34 | userId String @unique
35 | }
36 |
37 | model Note {
38 | id String @id @default(uuid())
39 | title String
40 | description String
41 |
42 | createdAt DateTime @default(now())
43 | updatedAt DateTime @updatedAt
44 |
45 | User User? @relation(fields: [userId], references: [id])
46 | userId String?
47 | }
48 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------