87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className,
93 | )}
94 | {...props}
95 | />
96 | ));
97 | TableCell.displayName = "TableCell";
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | TableCaption.displayName = "TableCaption";
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | };
121 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, Helvetica,
7 | sans-serif;
8 | }
9 |
10 | @layer base {
11 | :root {
12 | --background: 0 0% 100%;
13 | --foreground: 20 14.3% 4.1%;
14 | --card: 0 0% 100%;
15 | --card-foreground: 20 14.3% 4.1%;
16 | --popover: 0 0% 100%;
17 | --popover-foreground: 20 14.3% 4.1%;
18 | --primary: 24.6 95% 53.1%;
19 | --primary-foreground: 60 9.1% 97.8%;
20 | --secondary: 60 4.8% 95.9%;
21 | --secondary-foreground: 24 9.8% 10%;
22 | --muted: 60 4.8% 95.9%;
23 | --muted-foreground: 25 5.3% 44.7%;
24 | --accent: 60 4.8% 95.9%;
25 | --accent-foreground: 24 9.8% 10%;
26 | --destructive: 0 84.2% 60.2%;
27 | --destructive-foreground: 60 9.1% 97.8%;
28 | --border: 20 5.9% 90%;
29 | --input: 20 5.9% 90%;
30 | --ring: 24.6 95% 53.1%;
31 | --radius: 1rem;
32 | --chart-1: 12 76% 61%;
33 | --chart-2: 173 58% 39%;
34 | --chart-3: 197 37% 24%;
35 | --chart-4: 43 74% 66%;
36 | --chart-5: 27 87% 67%;
37 | --sidebar-background: 0 0% 98%;
38 | --sidebar-foreground: 240 5.3% 26.1%;
39 | --sidebar-primary: 240 5.9% 10%;
40 | --sidebar-primary-foreground: 0 0% 98%;
41 | --sidebar-accent: 240 4.8% 95.9%;
42 | --sidebar-accent-foreground: 240 5.9% 10%;
43 | --sidebar-border: 220 13% 91%;
44 | --sidebar-ring: 217.2 91.2% 59.8%;
45 | --color-1: 0 100% 63%;
46 | --color-2: 270 100% 63%;
47 | --color-3: 210 100% 63%;
48 | --color-4: 195 100% 63%;
49 | --color-5: 90 100% 63%;
50 | }
51 |
52 | .dark {
53 | --background: 20 14.3% 4.1%;
54 | --foreground: 60 9.1% 97.8%;
55 | --card: 20 14.3% 4.1%;
56 | --card-foreground: 60 9.1% 97.8%;
57 | --popover: 20 14.3% 4.1%;
58 | --popover-foreground: 60 9.1% 97.8%;
59 | --primary: 20.5 90.2% 48.2%;
60 | --primary-foreground: 60 9.1% 97.8%;
61 | --secondary: 12 6.5% 15.1%;
62 | --secondary-foreground: 60 9.1% 97.8%;
63 | --muted: 12 6.5% 15.1%;
64 | --muted-foreground: 24 5.4% 63.9%;
65 | --accent: 12 6.5% 15.1%;
66 | --accent-foreground: 60 9.1% 97.8%;
67 | --destructive: 0 72.2% 50.6%;
68 | --destructive-foreground: 60 9.1% 97.8%;
69 | --border: 12 6.5% 15.1%;
70 | --input: 12 6.5% 15.1%;
71 | --ring: 20.5 90.2% 48.2%;
72 | --chart-1: 220 70% 50%;
73 | --chart-2: 160 60% 45%;
74 | --chart-3: 30 80% 55%;
75 | --chart-4: 280 65% 60%;
76 | --chart-5: 340 75% 55%;
77 | --sidebar-background: 240 5.9% 10%;
78 | --sidebar-foreground: 240 4.8% 95.9%;
79 | --sidebar-primary: 224.3 76.3% 48%;
80 | --sidebar-primary-foreground: 0 0% 100%;
81 | --sidebar-accent: 240 3.7% 15.9%;
82 | --sidebar-accent-foreground: 240 4.8% 95.9%;
83 | --sidebar-border: 240 3.7% 15.9%;
84 | --sidebar-ring: 217.2 91.2% 59.8%;
85 | --color-1: 0 100% 63%;
86 | --color-2: 270 100% 63%;
87 | --color-3: 210 100% 63%;
88 | --color-4: 195 100% 63%;
89 | --color-5: 90 100% 63%;
90 | }
91 | }
92 |
93 | @layer base {
94 | * {
95 | @apply border-border;
96 | }
97 | body {
98 | @apply bg-background text-foreground;
99 | }
100 | }
101 |
102 | .no-visible-scrollbar {
103 | scrollbar-width: none;
104 | -ms-overflow-style: none;
105 | -webkit-overflow-scrolling: touch;
106 | }
107 |
108 | .no-visible-scrollbar::-webkit-scrollbar {
109 | display: none;
110 | }
111 |
--------------------------------------------------------------------------------
/app/api/get-subscription/route.ts:
--------------------------------------------------------------------------------
1 | // app/api/get-subscription/route.ts
2 | import { NextResponse, NextRequest } from 'next/server';
3 | import DodoPayments from 'dodopayments'; // Import the Dodo Payments SDK
4 |
5 | // Initialize the Dodo Payments client
6 | const client = new DodoPayments({
7 | bearerToken: process.env.DODO_PAYMENTS_API_KEY, // your Dodo Payments API key
8 | });
9 |
10 | // Replace with your actual database/data fetching logic
11 | async function getSubscriptionDataFromDatabaseByEmail(email: string) {
12 | try {
13 | // **Replace this with your actual database query to get the subscription ID associated with the email**
14 | const subscriptionId = await getSubscriptionIdFromDatabaseByEmail(email);
15 |
16 | if (!subscriptionId) {
17 | console.log("Subscription id does not exists for the current email");
18 | return null; // Subscription not found in your database
19 | }
20 |
21 | // Use the Dodo Payments API to retrieve the subscription by subscription ID
22 | const subscription = await client.subscriptions.retrieve(subscriptionId);
23 |
24 | if (subscription) {
25 | // Adapt the Dodo Payments API response to your desired format
26 | const subscriptionData = {
27 | created_at: subscription.created_at,
28 | currency: subscription.currency,
29 | customer: subscription.customer,
30 | // discount_id: subscription.discount_id,
31 | metadata: subscription.metadata,
32 | next_billing_date: subscription.next_billing_date,
33 | payment_frequency_count: subscription.payment_frequency_count,
34 | payment_frequency_interval: subscription.payment_frequency_interval,
35 | product_id: subscription.product_id,
36 | quantity: subscription.quantity,
37 | recurring_pre_tax_amount: subscription.recurring_pre_tax_amount,
38 | status: subscription.status,
39 | subscription_id: subscription.subscription_id,
40 | subscription_period_count: subscription.subscription_period_count,
41 | subscription_period_interval: subscription.subscription_period_interval,
42 | // tax_inclusive: subscription.tax_inclusive,
43 | trial_period_days: subscription.trial_period_days
44 | };
45 |
46 | return subscriptionData;
47 | } else {
48 | console.log("Subscription not found in Dodo Payments");
49 | return null; // Subscription not found in Dodo Payments
50 | }
51 | } catch (error: any) {
52 | console.error('Error fetching subscription from Dodo Payments:', error);
53 | return null; // Handle errors gracefully
54 | }
55 | }
56 |
57 | async function getSubscriptionIdFromDatabaseByEmail(email: string): Promise {
58 | // TODO: Implement your actual database query to fetch the subscription ID based on the email.
59 | // This is a placeholder - replace with your actual database logic.
60 | console.log("Calling database to get subscription id information for email", email);
61 | return "subscription_id"
62 | }
63 |
64 | export async function GET(req: NextRequest) {
65 | try {
66 | const searchParams = req.nextUrl.searchParams
67 | const email = searchParams.get('email');
68 |
69 | if (!email) {
70 | return NextResponse.json({ error: 'Missing email parameter' }, { status: 400 });
71 | }
72 |
73 | const subscription = await getSubscriptionDataFromDatabaseByEmail(email);
74 |
75 | if (!subscription) {
76 | return NextResponse.json({ error: 'Subscription not found' }, { status: 404 });
77 | }
78 |
79 | return NextResponse.json({ subscription });
80 | } catch (error: any) {
81 | console.error('Error fetching subscription:', error);
82 | return NextResponse.json({ error: 'Failed to fetch subscription' }, { status: 500 });
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | export default {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: "hsl(var(--background))",
14 | foreground: "hsl(var(--foreground))",
15 | card: {
16 | DEFAULT: "hsl(var(--card))",
17 | foreground: "hsl(var(--card-foreground))",
18 | },
19 | popover: {
20 | DEFAULT: "hsl(var(--popover))",
21 | foreground: "hsl(var(--popover-foreground))",
22 | },
23 | primary: {
24 | DEFAULT: "hsl(var(--primary))",
25 | foreground: "hsl(var(--primary-foreground))",
26 | },
27 | secondary: {
28 | DEFAULT: "hsl(var(--secondary))",
29 | foreground: "hsl(var(--secondary-foreground))",
30 | },
31 | muted: {
32 | DEFAULT: "hsl(var(--muted))",
33 | foreground: "hsl(var(--muted-foreground))",
34 | },
35 | accent: {
36 | DEFAULT: "hsl(var(--accent))",
37 | foreground: "hsl(var(--accent-foreground))",
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))",
42 | },
43 | border: "hsl(var(--border))",
44 | input: "hsl(var(--input))",
45 | ring: "hsl(var(--ring))",
46 | chart: {
47 | "1": "hsl(var(--chart-1))",
48 | "2": "hsl(var(--chart-2))",
49 | "3": "hsl(var(--chart-3))",
50 | "4": "hsl(var(--chart-4))",
51 | "5": "hsl(var(--chart-5))",
52 | },
53 | sidebar: {
54 | DEFAULT: "hsl(var(--sidebar-background))",
55 | foreground: "hsl(var(--sidebar-foreground))",
56 | primary: "hsl(var(--sidebar-primary))",
57 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
58 | accent: "hsl(var(--sidebar-accent))",
59 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
60 | border: "hsl(var(--sidebar-border))",
61 | ring: "hsl(var(--sidebar-ring))",
62 | },
63 | "color-1": "hsl(var(--color-1))",
64 | "color-2": "hsl(var(--color-2))",
65 | "color-3": "hsl(var(--color-3))",
66 | "color-4": "hsl(var(--color-4))",
67 | "color-5": "hsl(var(--color-5))",
68 | },
69 | borderRadius: {
70 | lg: "var(--radius)",
71 | md: "calc(var(--radius) - 2px)",
72 | sm: "calc(var(--radius) - 4px)",
73 | },
74 | animation: {
75 | "border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
76 | meteor: "meteor 5s linear infinite",
77 | rainbow: "rainbow var(--speed, 2s) infinite linear",
78 | },
79 | keyframes: {
80 | "border-beam": {
81 | "100%": {
82 | "offset-distance": "100%",
83 | },
84 | },
85 | meteor: {
86 | "0%": {
87 | transform: "rotate(215deg) translateX(0)",
88 | opacity: "1",
89 | },
90 | "70%": {
91 | opacity: "1",
92 | },
93 | "100%": {
94 | transform: "rotate(215deg) translateX(-500px)",
95 | opacity: "0",
96 | },
97 | },
98 | rainbow: {
99 | "0%": {
100 | "background-position": "0%",
101 | },
102 | "100%": {
103 | "background-position": "200%",
104 | },
105 | },
106 | },
107 | },
108 | },
109 | plugins: [require("tailwindcss-animate")],
110 | } satisfies Config;
111 |
--------------------------------------------------------------------------------
/hooks/use-cached-session.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState, useEffect } from "react";
3 | import { authClient } from "@/lib/auth/client";
4 |
5 | // Types for session and user
6 | interface User {
7 | id: string;
8 | createdAt: Date;
9 | updatedAt: Date;
10 | email: string;
11 | emailVerified: boolean;
12 | name: string;
13 | image?: string | null;
14 | }
15 |
16 | interface Session {
17 | id: string;
18 | userId: string;
19 | createdAt: Date;
20 | updatedAt: Date;
21 | expiresAt: Date;
22 | token: string;
23 | ipAddress?: string | null;
24 | userAgent?: string | null;
25 | }
26 |
27 | interface SessionResponse {
28 | user: User;
29 | session: Session;
30 | }
31 |
32 | // Utility to get encryption key
33 | const getKey = async () => {
34 | const keyBuffer = Uint8Array.from(
35 | atob(process.env.NEXT_PUBLIC_CACHE_ENCRYPTION_KEY || ""),
36 | (c) => c.charCodeAt(0),
37 | );
38 | return window.crypto.subtle.importKey("raw", keyBuffer, "AES-GCM", true, [
39 | "encrypt",
40 | "decrypt",
41 | ]);
42 | };
43 |
44 | // Encrypt data
45 | const encryptData = async (
46 | data: SessionResponse | null,
47 | ): Promise => {
48 | try {
49 | const key = await getKey();
50 | const jsonString = JSON.stringify(data);
51 | const encoder = new TextEncoder();
52 | const encodedData = encoder.encode(jsonString);
53 |
54 | const iv = window.crypto.getRandomValues(new Uint8Array(12));
55 | const encryptedData = await window.crypto.subtle.encrypt(
56 | {
57 | name: "AES-GCM",
58 | iv,
59 | },
60 | key,
61 | encodedData,
62 | );
63 |
64 | const combined = new Uint8Array(iv.length + encryptedData.byteLength);
65 | combined.set(iv);
66 | combined.set(new Uint8Array(encryptedData), iv.length);
67 |
68 | return btoa(String.fromCharCode(...combined));
69 | } catch (error) {
70 | console.error("Encryption error:", error);
71 | return null;
72 | }
73 | };
74 |
75 | // Decrypt data
76 | const decryptData = async (
77 | encryptedData: string,
78 | ): Promise => {
79 | try {
80 | const key = await getKey();
81 | const combined = Uint8Array.from(atob(encryptedData), (c) =>
82 | c.charCodeAt(0),
83 | );
84 |
85 | const iv = combined.slice(0, 12);
86 | const data = combined.slice(12);
87 |
88 | const decryptedBuffer = await window.crypto.subtle.decrypt(
89 | {
90 | name: "AES-GCM",
91 | iv,
92 | },
93 | key,
94 | data,
95 | );
96 |
97 | const decoder = new TextDecoder();
98 | const jsonString = decoder.decode(decryptedBuffer);
99 | return JSON.parse(jsonString);
100 | } catch (error) {
101 | console.error("Decryption error:", error);
102 | return null;
103 | }
104 | };
105 |
106 | // Custom hook for caching session
107 | export const useCachedSession = () => {
108 | const { data: sessionData, isPending: isSessionPending } =
109 | authClient.useSession();
110 | const [cachedData, setCachedData] = useState(null);
111 | const [isLoading, setIsLoading] = useState(true);
112 |
113 | useEffect(() => {
114 | const loadCachedData = async () => {
115 | const cached = localStorage.getItem("cache-user-session");
116 | if (cached) {
117 | const decrypted = await decryptData(cached);
118 | if (decrypted) {
119 | setCachedData(decrypted);
120 | }
121 | }
122 | setIsLoading(false);
123 | };
124 |
125 | loadCachedData();
126 | }, []);
127 |
128 | useEffect(() => {
129 | const updateCache = async () => {
130 | if (sessionData) {
131 | const encrypted = await encryptData(sessionData);
132 | if (encrypted) {
133 | localStorage.setItem("cache-user-session", encrypted);
134 | setCachedData(sessionData);
135 | }
136 | }
137 | };
138 |
139 | updateCache();
140 | }, [sessionData]);
141 |
142 | return {
143 | data: cachedData || sessionData,
144 | isPending: isLoading && isSessionPending,
145 | };
146 | };
147 |
148 |
--------------------------------------------------------------------------------
/app/api/webhooks/dodo/route.ts:
--------------------------------------------------------------------------------
1 | // app/api/webhooks/dodo/route.ts
2 | import { NextRequest, NextResponse } from 'next/server';
3 | import { Webhook } from "standardwebhooks";
4 | import { headers } from "next/headers";
5 |
6 | // Define the WebhookPayload interface
7 | interface WebhookPayload {
8 | event: string;
9 | data: {
10 | subscription_id?: string;
11 | payment_id?: string;
12 | status?: string;
13 | customer_id?: string;
14 | next_billing_date?: string;
15 | amount?: number;
16 | error?: string;
17 | // other fields depending on the event type
18 | };
19 | }
20 |
21 | // Placeholder function for updating the subscription status in your database
22 | async function updateSubscriptionStatus(customerId: string, subscriptionId: string, status: string) {
23 | // **Replace this with your actual database update logic**
24 | console.log(`Updating subscription status for customer ${customerId}, subscription ${subscriptionId} to ${status}`);
25 | // Example using a hypothetical database client:
26 | // await db.updateSubscription(customerId, subscriptionId, status);
27 | }
28 |
29 | export async function POST(request: NextRequest) {
30 | const headersList = await headers();
31 | const webhookSecret = process.env.DODO_WEBHOOK_SECRET;
32 |
33 | if (!webhookSecret) {
34 | console.error("Missing webhook secret");
35 | return NextResponse.json({ error: "Server configuration error" }, { status: 500 });
36 | }
37 |
38 | try {
39 | const rawBody = await request.text();
40 |
41 | const webhookHeaders = {
42 | "webhook-id": headersList.get("webhook-id") || "",
43 | "webhook-signature": headersList.get("webhook-signature") || "",
44 | "webhook-timestamp": headersList.get("webhook-timestamp") || "",
45 | };
46 |
47 | // Initialize the webhook with your secret
48 | const webhook = new Webhook(webhookSecret);
49 |
50 | // Verify the webhook signature
51 | await webhook.verify(rawBody, webhookHeaders);
52 |
53 | // Parse the payload
54 | const payload = JSON.parse(rawBody) as WebhookPayload;
55 | const { event, data } = payload;
56 |
57 | // Handle different subscription events
58 | switch (event) {
59 | case 'subscription.active':
60 | if (data.customer_id && data.subscription_id && data.status) {
61 | await updateSubscriptionStatus(data.customer_id, data.subscription_id, data.status);
62 | console.log('Subscription activated:', data.subscription_id);
63 | }
64 | break;
65 | case 'subscription.on_hold':
66 | if (data.customer_id && data.subscription_id && data.status) {
67 | await updateSubscriptionStatus(data.customer_id, data.subscription_id, data.status);
68 | console.log('Subscription on hold:', data.subscription_id);
69 | }
70 | break;
71 | case 'subscription.failed':
72 | if (data.customer_id && data.subscription_id && data.status) {
73 | await updateSubscriptionStatus(data.customer_id, data.subscription_id, data.status);
74 | console.log('Subscription failed:', data.subscription_id);
75 | }
76 | break;
77 | case 'subscription.renewed':
78 | if (data.customer_id && data.subscription_id && data.status) {
79 | await updateSubscriptionStatus(data.customer_id, data.subscription_id, data.status);
80 | console.log('Subscription renewed:', data.subscription_id);
81 | }
82 | break;
83 | case 'payment.succeeded':
84 | // Payment succeeded, you might want to update some payment-related info
85 | console.log('Payment succeeded:', data.payment_id);
86 | break;
87 | case 'payment.failed':
88 | // Payment failed, handle accordingly
89 | console.log('Payment failed:', data.payment_id);
90 | break;
91 | default:
92 | console.log(`Unhandled event type: ${event}`);
93 | }
94 |
95 | return NextResponse.json({ received: true });
96 | } catch (error: any) {
97 | console.error('Webhook error:', error);
98 | const errorMessage = error instanceof Error ? error.message : 'Unknown error';
99 |
100 | return NextResponse.json(
101 | { error: errorMessage },
102 | { status: 400 }
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/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";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/app/app/settings/_components/appearance-page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React from "react";
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { LucideIcon, Monitor, Moon, Sun } from "lucide-react";
5 | import { useTheme } from "next-themes";
6 |
7 | const ThemeCard = ({
8 | title,
9 | icon: Icon,
10 | isSelected,
11 | }: {
12 | title: string;
13 | icon: LucideIcon;
14 | isSelected: boolean;
15 | }) => {
16 | const isSystem = title === "System";
17 |
18 | return (
19 |
20 |
31 | {isSystem ? (
32 | <>
33 | {/* Light half */}
34 |
35 |
36 |
37 |
38 |
39 | {[...Array(3)].map((_, i) => (
40 |
46 | ))}
47 |
48 |
49 | {/* Dark half */}
50 |
51 |
52 |
53 |
54 |
55 | {[...Array(3)].map((_, i) => (
56 |
62 | ))}
63 |
64 |
65 | >
66 | ) : (
67 | <>
68 |
73 |
74 |
75 |
76 | {[...Array(3)].map((_, i) => (
77 |
83 | ))}
84 |
85 | >
86 | )}
87 |
88 |
{title}
89 |
90 | );
91 | };
92 |
93 | export function AppearancePage() {
94 | const { setTheme, theme } = useTheme();
95 | return (
96 |
97 |
98 | Theme
99 | Customize your UI theme
100 |
101 |
102 |
103 |
setTheme("system")}
106 | >
107 |
112 |
113 |
setTheme("light")}
116 | >
117 |
122 |
123 |
setTheme("dark")}
126 | >
127 |
128 |
129 |
130 |
131 |
132 | );
133 | }
134 |
--------------------------------------------------------------------------------
/app/app/_components/app-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | Home,
6 | LogOut,
7 | Palette,
8 | Code,
9 | Coffee,
10 | Settings,
11 | Loader2,
12 | Loader,
13 | LucideIcon,
14 | } from "lucide-react";
15 |
16 | import {
17 | Sidebar,
18 | SidebarContent,
19 | SidebarFooter,
20 | SidebarGroup,
21 | SidebarHeader,
22 | SidebarMenu,
23 | SidebarMenuButton,
24 | SidebarMenuItem,
25 | SidebarRail,
26 | useSidebar,
27 | } from "@/components/ui/sidebar";
28 | import { usePathname } from "next/navigation";
29 | import { useRouter } from "@/hooks/use-router";
30 | import { UserButton } from "./user-btn";
31 | import { Logo } from "@/components/logo";
32 | import { cn } from "@/lib/utils";
33 |
34 | const navigation: {
35 | title: string;
36 | url: string;
37 | icon: LucideIcon;
38 | }[] = [
39 | {
40 | title: "Home",
41 | url: "/app/home",
42 | icon: Home,
43 | },
44 | {
45 | title: "Settings",
46 | url: "/app/settings",
47 | icon: Settings,
48 | },
49 | ];
50 |
51 | export function AppSidebar() {
52 | const pathname = usePathname();
53 | const router = useRouter();
54 | const { open, toggleSidebar } = useSidebar();
55 | const [loading, setLoading] = React.useState(true);
56 | const [loadedPathnames, setLoadedPathnames] = React.useState([]);
57 | const [loadingPathname, setLoadingPathname] = React.useState("");
58 |
59 | // Create a ref to track if the component is mounted
60 | const isMounted = React.useRef(false);
61 |
62 | // Collect all routes that need to be prefetched
63 | const allRoutes = React.useMemo(() => {
64 | const routes: string[] = [];
65 | navigation.forEach((item) => {
66 | routes.push(item.url);
67 | });
68 | return routes;
69 | }, []);
70 |
71 | const prefetchAllRoutes = () => {
72 | // Small delay to ensure we don't interfere with initial page load
73 | setTimeout(() => {
74 | allRoutes.forEach((route) => {
75 | if (route !== pathname) {
76 | router.prefetch(route);
77 | }
78 | });
79 | setLoading(false);
80 | if (loadedPathnames.length === 0) {
81 | setLoadedPathnames([pathname]);
82 | }
83 | }, 200);
84 | };
85 |
86 | // Handle initial route prefetching
87 | React.useEffect(() => {
88 | if (!isMounted.current) {
89 | isMounted.current = true;
90 |
91 | // Wait for the page to be fully loaded
92 | if (typeof window !== "undefined") {
93 | if (document.readyState === "complete") {
94 | prefetchAllRoutes();
95 | } else {
96 | window.addEventListener("load", prefetchAllRoutes);
97 | return () => window.removeEventListener("load", prefetchAllRoutes);
98 | }
99 | }
100 | }
101 | }, [prefetchAllRoutes]);
102 |
103 | const handleNavigation = (url: string) => () => {
104 | router.replace(url);
105 | };
106 |
107 | return (
108 |
109 |
115 |
116 | {open && <>Starstack by Asend Labs>}
117 |
118 |
119 |
120 |
121 | {navigation.map((item) => (
122 |
123 | {
126 | handleNavigation(item.url)();
127 | toggleSidebar();
128 | }}
129 | >
130 |
131 |
132 | {loading && loadingPathname === item.url ? (
133 |
134 | ) : (
135 |
136 | )}
137 | {item.title}
138 |
139 |
140 |
141 |
142 | ))}
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | );
152 | }
153 |
154 | export default AppSidebar;
155 |
--------------------------------------------------------------------------------
/emails/magic-link.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | Body,
4 | Container,
5 | Head,
6 | Heading,
7 | Html,
8 | Link,
9 | Preview,
10 | Text,
11 | Button,
12 | } from "@react-email/components";
13 | import { sendEmail } from "@/lib/email";
14 | import { toast } from "sonner";
15 |
16 |
17 | const MagicLinkEmailBody = ({ email, url }: { email: string; url: string }) => {
18 | return (
19 |
20 |
21 |
25 |
56 |
57 |
65 |
74 |
86 | To continue with your email on Startstack click on the button below.
87 |
88 |
106 | Continue with Magic link
107 |
108 |
120 | This message was sent to{" "}
121 |
130 | {email}
131 | {" "}
132 | because a login/signup attempt was made using this email. If this
133 | wasn't you, you can safely ignore this email.
134 |
135 |
136 |
137 |
138 | );
139 | };
140 |
141 | export async function sendMagicLink(email: string, url: string) {
142 | try {
143 | const res = await sendEmail(
144 | email,
145 | "Continue with your Email",
146 | <>
147 |
148 | >,
149 | );
150 | return res;
151 | } catch (error) {
152 | return error;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const Sheet = SheetPrimitive.Root;
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger;
13 |
14 | const SheetClose = SheetPrimitive.Close;
15 |
16 | const SheetPortal = SheetPrimitive.Portal;
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ));
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | },
50 | );
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ));
75 | SheetContent.displayName = SheetPrimitive.Content.displayName;
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | );
89 | SheetHeader.displayName = "SheetHeader";
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | );
103 | SheetFooter.displayName = "SheetFooter";
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ));
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName;
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ));
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName;
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | };
141 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react";
5 |
6 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
7 |
8 | const TOAST_LIMIT = 1;
9 | const TOAST_REMOVE_DELAY = 1000000;
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string;
13 | title?: React.ReactNode;
14 | description?: React.ReactNode;
15 | action?: ToastActionElement;
16 | };
17 |
18 | const actionTypes = {
19 | ADD_TOAST: "ADD_TOAST",
20 | UPDATE_TOAST: "UPDATE_TOAST",
21 | DISMISS_TOAST: "DISMISS_TOAST",
22 | REMOVE_TOAST: "REMOVE_TOAST",
23 | } as const;
24 |
25 | let count = 0;
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER;
29 | return count.toString();
30 | }
31 |
32 | type ActionType = typeof actionTypes;
33 |
34 | type Action =
35 | | {
36 | type: ActionType["ADD_TOAST"];
37 | toast: ToasterToast;
38 | }
39 | | {
40 | type: ActionType["UPDATE_TOAST"];
41 | toast: Partial;
42 | }
43 | | {
44 | type: ActionType["DISMISS_TOAST"];
45 | toastId?: ToasterToast["id"];
46 | }
47 | | {
48 | type: ActionType["REMOVE_TOAST"];
49 | toastId?: ToasterToast["id"];
50 | };
51 |
52 | interface State {
53 | toasts: ToasterToast[];
54 | }
55 |
56 | const toastTimeouts = new Map>();
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return;
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId);
65 | dispatch({
66 | type: "REMOVE_TOAST",
67 | toastId: toastId,
68 | });
69 | }, TOAST_REMOVE_DELAY);
70 |
71 | toastTimeouts.set(toastId, timeout);
72 | };
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case "ADD_TOAST":
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | };
81 |
82 | case "UPDATE_TOAST":
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t,
87 | ),
88 | };
89 |
90 | case "DISMISS_TOAST": {
91 | const { toastId } = action;
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId);
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id);
100 | });
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t,
112 | ),
113 | };
114 | }
115 | case "REMOVE_TOAST":
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | };
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | };
126 | }
127 | };
128 |
129 | const listeners: Array<(state: State) => void> = [];
130 |
131 | let memoryState: State = { toasts: [] };
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action);
135 | listeners.forEach((listener) => {
136 | listener(memoryState);
137 | });
138 | }
139 |
140 | type Toast = Omit;
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId();
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: "UPDATE_TOAST",
148 | toast: { ...props, id },
149 | });
150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
151 |
152 | dispatch({
153 | type: "ADD_TOAST",
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss();
160 | },
161 | },
162 | });
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | };
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState);
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState);
176 | return () => {
177 | const index = listeners.indexOf(setState);
178 | if (index > -1) {
179 | listeners.splice(index, 1);
180 | }
181 | };
182 | }, [state]);
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188 | };
189 | }
190 |
191 | export { useToast, toast };
192 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
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";
16 | import { Label } from "@/components/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(
28 | {} as FormFieldContextValue,
29 | );
30 |
31 | const FormField = <
32 | TFieldValues extends FieldValues = FieldValues,
33 | TName extends FieldPath = FieldPath,
34 | >({
35 | ...props
36 | }: ControllerProps) => {
37 | return (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const useFormField = () => {
45 | const fieldContext = React.useContext(FormFieldContext);
46 | const itemContext = React.useContext(FormItemContext);
47 | const { getFieldState, formState } = useFormContext();
48 |
49 | const fieldState = getFieldState(fieldContext.name, formState);
50 |
51 | if (!fieldContext) {
52 | throw new Error("useFormField should be used within ");
53 | }
54 |
55 | const { id } = itemContext;
56 |
57 | return {
58 | id,
59 | name: fieldContext.name,
60 | formItemId: `${id}-form-item`,
61 | formDescriptionId: `${id}-form-item-description`,
62 | formMessageId: `${id}-form-item-message`,
63 | ...fieldState,
64 | };
65 | };
66 |
67 | type FormItemContextValue = {
68 | id: string;
69 | };
70 |
71 | const FormItemContext = React.createContext(
72 | {} as FormItemContextValue,
73 | );
74 |
75 | const FormItem = React.forwardRef<
76 | HTMLDivElement,
77 | React.HTMLAttributes
78 | >(({ className, ...props }, ref) => {
79 | const id = React.useId();
80 |
81 | return (
82 |
83 |
84 |
85 | );
86 | });
87 | FormItem.displayName = "FormItem";
88 |
89 | const FormLabel = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => {
93 | const { error, formItemId } = useFormField();
94 |
95 | return (
96 |
102 | );
103 | });
104 | FormLabel.displayName = "FormLabel";
105 |
106 | const FormControl = React.forwardRef<
107 | React.ElementRef,
108 | React.ComponentPropsWithoutRef
109 | >(({ ...props }, ref) => {
110 | const { error, formItemId, formDescriptionId, formMessageId } =
111 | useFormField();
112 |
113 | return (
114 |
125 | );
126 | });
127 | FormControl.displayName = "FormControl";
128 |
129 | const FormDescription = React.forwardRef<
130 | HTMLParagraphElement,
131 | React.HTMLAttributes
132 | >(({ className, ...props }, ref) => {
133 | const { formDescriptionId } = useFormField();
134 |
135 | return (
136 |
142 | );
143 | });
144 | FormDescription.displayName = "FormDescription";
145 |
146 | const FormMessage = React.forwardRef<
147 | HTMLParagraphElement,
148 | React.HTMLAttributes
149 | >(({ className, children, ...props }, ref) => {
150 | const { error, formMessageId } = useFormField();
151 | const body = error ? String(error?.message) : children;
152 |
153 | if (!body) {
154 | return null;
155 | }
156 |
157 | return (
158 |
164 | {body}
165 |
166 | );
167 | });
168 | FormMessage.displayName = "FormMessage";
169 |
170 | export {
171 | useFormField,
172 | Form,
173 | FormItem,
174 | FormLabel,
175 | FormControl,
176 | FormDescription,
177 | FormMessage,
178 | FormField,
179 | };
180 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ToastPrimitives from "@radix-ui/react-toast";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 | import { X } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 |
10 | const ToastProvider = ToastPrimitives.Provider;
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ));
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | },
41 | );
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | );
55 | });
56 | Toast.displayName = ToastPrimitives.Root.displayName;
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ));
71 | ToastAction.displayName = ToastPrimitives.Action.displayName;
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ));
89 | ToastClose.displayName = ToastPrimitives.Close.displayName;
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ));
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName;
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ));
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName;
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef;
116 |
117 | type ToastActionElement = React.ReactElement;
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | };
130 |
--------------------------------------------------------------------------------
/app/app/settings/_components/settings.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect } from "react";
4 | import { AccountPage } from "./account-page";
5 | import Loading from "@/app/loading";
6 | import { AppearancePage } from "./appearance-page";
7 | import { NotificationPage } from "./notifications-page";
8 | import { Button } from "@/components/ui/button";
9 | import { cn } from "@/lib/utils";
10 | import { useQueryState } from "nuqs";
11 | import { Bell, Building2, CreditCard, Palette, UserCircle } from "lucide-react";
12 | import { Session } from "@/types/auth";
13 |
14 | // Define a type or interface for the subscription data
15 | interface SubscriptionData {
16 | created_at: string;
17 | currency: string;
18 | customer: any; // Replace 'any' with a more specific type if you have one...if u need :(
19 | discount_id: string | null;
20 | metadata: any; // Replace 'any' with a more specific type if you have one...can't do it now
21 | next_billing_date: string;
22 | payment_frequency_count: number;
23 | payment_frequency_interval: string;
24 | product_id: string;
25 | quantity: number;
26 | recurring_pre_tax_amount: number;
27 | status: string;
28 | subscription_id: string;
29 | subscription_period_count: number;
30 | subscription_period_interval: string;
31 | tax_inclusive: boolean;
32 | trial_period_days: number;
33 | }
34 |
35 | // Function to fetch subscription status
36 | async function getSubscriptionStatus(email: string): Promise {
37 | try {
38 | const response = await fetch(`/api/get-subscription?email=${email}`);
39 | const data = await response.json();
40 | if (!response.ok) {
41 | throw new Error(data.error || 'Failed to fetch subscription status');
42 | }
43 | return data.subscription as SubscriptionData;
44 | } catch (error: any) {
45 | console.error('Error fetching subscription status:', error);
46 | return null;
47 | }
48 | }
49 |
50 | const settingsTabs = [
51 | { name: "Account & Security", value: "account", icon: UserCircle },
52 | { name: "Billing & Plans", value: "billing", icon: CreditCard },
53 | { name: "Emails & Notifications", value: "notifications", icon: Bell },
54 | { name: "Appearance", value: "appearance", icon: Palette },
55 | ];
56 |
57 | export function Settings({
58 | activeSessions,
59 | session,
60 | }: {
61 | session: Session | null;
62 | activeSessions: Session["session"][];
63 | }) {
64 | const [tab, setTab] = useQueryState("page", {});
65 | const [subscription, setSubscription] = useState(null);
66 | const [loading, setLoading] = useState(true);
67 |
68 | useEffect(() => {
69 | if (!tab) setTab("account");
70 | }, [tab, setTab]);
71 |
72 | useEffect(() => {
73 | const fetchSubscription = async () => {
74 | setLoading(true);
75 | if (session?.user?.email) {
76 | const subscriptionData = await getSubscriptionStatus(session.user.email);
77 | setSubscription(subscriptionData);
78 | }
79 | setLoading(false);
80 | };
81 |
82 | fetchSubscription();
83 | }, [session]);
84 |
85 | return (
86 |
87 |
88 | {settingsTabs.map((mappedTab) => (
89 | {
99 | setTab(mappedTab.value);
100 | }}
101 | >
102 |
103 | {mappedTab.name}
104 |
105 | ))}
106 |
107 |
108 | {tab === "account" && (
109 |
110 | )}
111 | {tab === "billing" && (
112 |
113 |
Billing & Plans
114 | {loading ? (
115 |
116 | ) : subscription ? (
117 |
118 |
Subscription Details
119 |
Status: {subscription.status}
120 |
Product ID: {subscription.product_id}
121 | {/* Display additional subscription details */}
122 |
Created At: {subscription.created_at}
123 |
Next Billing Date: {subscription.next_billing_date}
124 |
Amount: {subscription.recurring_pre_tax_amount} {subscription.currency}
125 | {/* Display other relevant subscription details */}
126 |
127 | ) : (
128 |
129 |
No active subscription found.
130 |
131 | )}
132 |
133 | )}
134 | {tab === "notifications" && }
135 | {tab === "appearance" && }
136 |
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/app/_components/header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import Link from "next/link";
5 | import { HeaderButton } from "./header-button";
6 | import {
7 | GithubIcon,
8 | LogInIcon,
9 | MenuIcon,
10 | XIcon,
11 | Home as HomeIcon,
12 | Star as FeaturesIcon,
13 | DollarSign as PricingIcon,
14 | Info as AboutIcon,
15 | UserCircle,
16 | } from "lucide-react";
17 | import { ModeToggle } from "@/components/mode-toggle";
18 | import { useIsMobile } from "@/hooks/use-mobile";
19 | import { useSession } from "@/lib/auth/client";
20 |
21 | // Menu data structure
22 | interface MenuItem {
23 | label: string;
24 | href: string;
25 | icon: React.ReactNode;
26 | }
27 |
28 | // Example menu data items with icons
29 | const menuItems: MenuItem[] = [
30 | {
31 | label: "Home",
32 | href: "/",
33 | icon: ,
34 | },
35 | {
36 | label: "Features",
37 | href: "/features",
38 | icon: ,
39 | },
40 | {
41 | label: "Pricing",
42 | href: "/pricing",
43 | icon: ,
44 | },
45 | {
46 | label: "About",
47 | href: "/about",
48 | icon: ,
49 | },
50 | ];
51 |
52 | export function Header() {
53 | const [isMenuOpen, setIsMenuOpen] = useState(false);
54 | const isMobile = useIsMobile();
55 |
56 | const toggleMenu = () => {
57 | setIsMenuOpen(!isMenuOpen);
58 | };
59 |
60 | useEffect(() => {
61 | if (!isMobile && isMenuOpen) {
62 | setIsMenuOpen(false);
63 | }
64 | }, [isMenuOpen, isMobile]);
65 |
66 | const { data: session } = useSession();
67 |
68 | return (
69 |
70 | {/* Hamburger Menu Icon */}
71 |
72 |
73 | {isMenuOpen ? (
74 |
75 | ) : (
76 |
77 | )}
78 |
79 |
80 |
81 | {/* Logo */}
82 |
86 |
87 | Startstack
88 |
89 |
90 | {/* Desktop Navigation */}
91 |
92 | {menuItems.map((item) => (
93 |
99 | ))}
100 |
101 |
102 | {/* Desktop Buttons */}
103 |
104 | }
108 | />
109 |
110 |
120 | ) : (
121 |
122 | )
123 | }
124 | />
125 |
126 |
127 |
128 |
129 | {/* Mobile Menu */}
130 | {isMenuOpen && (
131 |
132 | {/* Menu Items */}
133 |
134 | {/* Main Navigation Items */}
135 | {menuItems.map((item) => (
136 |
142 | ))}
143 |
144 | {/* GitHub Link */}
145 | }
149 | />
150 |
151 | {/* Login Button */}
152 |
162 | ) : (
163 |
164 | )
165 | }
166 | />
167 |
168 | {/* Mode Toggle in Mobile Menu */}
169 |
170 |
171 |
172 | )}
173 |
174 | );
175 | }
176 |
--------------------------------------------------------------------------------
/components/forms/login-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Card,
5 | CardContent,
6 | CardHeader,
7 | CardTitle,
8 | CardDescription,
9 | } from "@/components/ui/card";
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | import { useState } from "react";
12 | import { signIn } from "@/lib/auth/client";
13 | import { loginSchema } from "@/schemas/auth.schema";
14 | import { useForm } from "react-hook-form";
15 | import { z } from "zod";
16 | import { Button } from "@/components/ui/button";
17 | import {
18 | Form,
19 | FormControl,
20 | FormField,
21 | FormItem,
22 | FormLabel,
23 | FormMessage,
24 | } from "@/components/ui/form";
25 | import { Input } from "@/components/ui/input";
26 | import { toast } from "sonner";
27 | import { Loader2 } from "lucide-react";
28 | import { useRouter } from "next/navigation";
29 |
30 | export function LoginForm() {
31 | const [loading, setLoading] = useState(false);
32 | const router = useRouter();
33 |
34 | const form = useForm>({
35 | resolver: zodResolver(loginSchema),
36 | defaultValues: {
37 | email: "",
38 | },
39 | });
40 |
41 | const onSubmit = async (data: z.infer) => {
42 | try {
43 | setLoading(true);
44 | setTimeout(() => {
45 | router.push("/login/sent");
46 | }, 250);
47 | const result = await signIn.magicLink({
48 | email: data.email,
49 | callbackURL: "/app/home",
50 | });
51 |
52 | if (result.error) {
53 | toast.error(result.error.message);
54 | return;
55 | }
56 | } catch (error) {
57 | toast.error(
58 | "Something went wrong. Contact support if the issue persists",
59 | );
60 | } finally {
61 | setLoading(false);
62 | }
63 | };
64 |
65 | const handleGithubLogin = async () => {
66 | try {
67 | setLoading(true);
68 | await signIn.social({
69 | provider: "github",
70 | callbackURL: "/app/home",
71 | });
72 | } catch (error) {
73 | toast.error(
74 | "Something went wrong. Contact support if the issue persists",
75 | );
76 | setLoading(false);
77 | }
78 | };
79 |
80 | return (
81 |
82 |
83 | Welcome back
84 |
85 | Enter your email below to login to your account.
86 |
87 |
88 |
89 |
159 |
160 |
161 |
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/app/app/_components/user-btn.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ChevronsUpDown, Loader2, LogOut, Settings } from "lucide-react";
3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4 | import { Skeleton } from "@/components/ui/skeleton";
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuGroup,
9 | DropdownMenuItem,
10 | DropdownMenuLabel,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu";
14 | import {
15 | SidebarMenu,
16 | SidebarMenuButton,
17 | SidebarMenuItem,
18 | useSidebar,
19 | } from "@/components/ui/sidebar";
20 | import { authClient } from "@/lib/auth/client";
21 | import { toast } from "sonner";
22 | import { useRouter } from "@/hooks/use-router";
23 | import React, { useState } from "react";
24 | import Link from "next/link";
25 | import { useCachedSession } from "@/hooks/use-cached-session";
26 |
27 | export function UserButton() {
28 | const { isMobile } = useSidebar();
29 | const { data: session, isPending } = useCachedSession();
30 | const [loading, setLoading] = useState(true);
31 | const router = useRouter();
32 | const [loggingOut, setLoggingOut] = useState(false);
33 |
34 | const handleLogout = async () => {
35 | setLoggingOut(true);
36 | try {
37 | // Clear cached session
38 | localStorage.removeItem("cache-user-session");
39 |
40 | const { error } = await authClient.signOut({
41 | fetchOptions: {
42 | onSuccess: () => {
43 | router.push("/login");
44 | },
45 | },
46 | });
47 | if (error) {
48 | toast.error(error.message);
49 | return;
50 | }
51 | toast.success("You have been logged out successfully.");
52 | } catch (error) {
53 | toast.info("Something went wrong. Please try again.");
54 | } finally {
55 | setLoggingOut(false);
56 | }
57 | };
58 |
59 | React.useEffect(() => {
60 | if (session) {
61 | setLoading(false);
62 | }
63 | }, [session]);
64 |
65 | if (loading || isPending) {
66 | return (
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
90 |
91 |
95 |
96 | {session?.user?.name?.slice(0, 1).toUpperCase()}
97 |
98 |
99 |
100 |
101 | {session?.user?.name}
102 |
103 | {session?.user?.email}
104 |
105 |
106 |
107 |
108 |
114 |
115 |
116 |
117 |
121 |
122 | {session?.user?.name?.slice(0, 1).toUpperCase()}
123 |
124 |
125 |
126 |
127 | {session?.user?.name}
128 |
129 |
130 | {session?.user?.email}
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | Settings
141 |
142 |
143 |
144 |
145 |
146 | {loggingOut ? (
147 |
148 |
149 | Log Out
150 |
151 | ) : (
152 |
153 |
154 | Log Out
155 |
156 | )}
157 |
158 |
159 |
160 |
161 |
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/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 | {children}
132 |
133 | ));
134 | SelectItem.displayName = SelectPrimitive.Item.displayName;
135 |
136 | const SelectSeparator = React.forwardRef<
137 | React.ElementRef,
138 | React.ComponentPropsWithoutRef
139 | >(({ className, ...props }, ref) => (
140 |
145 | ));
146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
147 |
148 | export {
149 | Select,
150 | SelectGroup,
151 | SelectValue,
152 | SelectTrigger,
153 | SelectContent,
154 | SelectLabel,
155 | SelectItem,
156 | SelectSeparator,
157 | SelectScrollUpButton,
158 | SelectScrollDownButton,
159 | };
160 |
--------------------------------------------------------------------------------
/app/pricing/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { PageTitle } from "@/components/page-title";
5 | import { Button } from "@/components/ui/button";
6 | import { useToast } from "@/hooks/use-toast";
7 | import Image from 'next/image';
8 | import { Header } from "@/app/_components/header";
9 | import { Footer } from "@/app/_components/footer";
10 | import { useRouter } from 'next/navigation'; // Import useRouter
11 |
12 |
13 | // Define your pricing plans
14 | const pricingPlans = [
15 | {
16 | title: "Basic",
17 | price: "$9.99",
18 | frequency: "month",
19 | description: "Perfect for individuals and small projects",
20 | features: [
21 | "Feature 1",
22 | "Feature 2",
23 | "Feature 3",
24 | ],
25 | productId: "prod_basic123", // Replace with your actual DODO product ID
26 | isPopular: false,
27 | },
28 | {
29 | title: "Pro",
30 | price: "$19.99",
31 | frequency: "month",
32 | description: "Best for growing teams and businesses",
33 | features: [
34 | "All Basic features",
35 | "Feature 4",
36 | "Feature 5",
37 | "Feature 6",
38 | ],
39 | productId: "pdt_a1EG4eSKFx8iPO1t53SPM", // Replace with your actual DODO product ID
40 | isPopular: true,
41 | },
42 | {
43 | title: "Enterprise",
44 | price: "$49.99",
45 | frequency: "month",
46 | description: "Advanced features for larger organizations",
47 | features: [
48 | "All Pro features",
49 | "Feature 7",
50 | "Feature 8",
51 | "Feature 9",
52 | "Feature 10",
53 | ],
54 | productId: "prod_enterprise789", // Replace with your actual DODO product ID
55 | isPopular: false,
56 | },
57 | ];
58 |
59 | // New Client Component for handling subscriptions
60 | // New Client Component for handling subscriptions
61 | function SubscriptionHandler({ productId, planName }: { productId: string; planName: string }) {
62 | const { toast } = useToast();
63 | const router = useRouter(); // Use useRouter hook
64 |
65 | const handleSubscription = () => {
66 | try {
67 | // 1. Redirect to Dodo Payments checkout page.
68 | const dodoCheckoutURL = `https://www.checkout.dodopayments.com/buy/${productId}`;
69 | window.location.href = dodoCheckoutURL;
70 | } catch (error: any) {
71 | console.error('Error creating subscription:', error);
72 | toast({
73 | title: "Subscription Error",
74 | description: error.message || "Failed to create subscription. Please try again.",
75 | variant: "destructive",
76 | });
77 | }
78 | };
79 |
80 | return (
81 |
82 | {`Subscribe to ${planName}`}
83 |
84 | );
85 | }
86 |
87 |
88 | export default function PricingPage() {
89 | return (
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | Simple, transparent pricing
98 |
99 |
100 | Choose the plan that works best for your needs
101 |
102 |
103 |
104 |
105 | {pricingPlans.map((plan) => (
106 |
110 | {plan.isPopular && (
111 |
112 | Most Popular
113 |
114 | )}
115 |
116 |
117 |
{plan.title}
118 |
119 | {plan.price}
120 | /{plan.frequency}
121 |
122 |
{plan.description}
123 |
124 |
125 |
126 | {plan.features.map((feature, index) => (
127 |
128 |
138 |
139 |
140 |
141 | {feature}
142 |
143 | ))}
144 |
145 |
146 |
147 |
148 |
149 | ))}
150 |
151 |
152 |
153 |
154 | Have questions about our pricing? Contact us
155 |
156 |
157 |
158 |
159 |
160 |
161 |
167 |
168 |
169 |
170 |
171 | );
172 | }
173 |
--------------------------------------------------------------------------------
/components/forms/signup-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Card,
5 | CardContent,
6 | CardHeader,
7 | CardTitle,
8 | CardDescription,
9 | } from "@/components/ui/card";
10 | import { zodResolver } from "@hookform/resolvers/zod";
11 | import { useState } from "react";
12 | import { signIn } from "@/lib/auth/client";
13 | import { signUpSchema } from "@/schemas/auth.schema";
14 | import { useForm } from "react-hook-form";
15 | import { z } from "zod";
16 | import { Button } from "@/components/ui/button";
17 | import {
18 | Form,
19 | FormControl,
20 | FormField,
21 | FormItem,
22 | FormLabel,
23 | FormMessage,
24 | } from "@/components/ui/form";
25 | import { Input } from "@/components/ui/input";
26 | import { toast } from "sonner";
27 | import { Loader2 } from "lucide-react";
28 | import { useRouter } from "next/navigation";
29 |
30 | export function SignupForm() {
31 | const [loading, setLoading] = useState(false);
32 |
33 | const form = useForm>({
34 | resolver: zodResolver(signUpSchema),
35 | defaultValues: {
36 | fullName: "",
37 | email: "",
38 | },
39 | });
40 |
41 | const router = useRouter();
42 |
43 | const onSubmit = async (data: z.infer) => {
44 | try {
45 | setLoading(true);
46 | setTimeout(() => {
47 | router.push("/signup/sent");
48 | }, 250);
49 | const result = await signIn.magicLink({
50 | email: data.email,
51 | callbackURL: "/app/home",
52 | });
53 |
54 | if (result.error) {
55 | toast.error(result.error.message);
56 | return;
57 | }
58 | } catch (error) {
59 | toast.error(
60 | "Something went wrong. Contact support if the issue persists",
61 | );
62 | } finally {
63 | setLoading(false);
64 | }
65 | };
66 | const onGithubSubmit = async () => {
67 | setLoading(true);
68 | try {
69 | await signIn.social({
70 | provider: "github",
71 | callbackURL: "/",
72 | });
73 | } catch (error) {
74 | toast.error(
75 | "Something went wrong. Contact support if the issue persists",
76 | );
77 | }
78 | };
79 |
80 | return (
81 |
82 |
83 | Create an account
84 |
85 | Just give us a name and an email to get started.
86 |
87 |
88 |
89 |
183 |
184 |
185 | By signing up, you agree to our{" "}
186 |
187 | Terms of Service
188 | {" "}
189 | and{" "}
190 |
191 | Privacy Policy
192 |
193 | .
194 |
195 |
196 |
197 | );
198 | }
199 |
--------------------------------------------------------------------------------
/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 |
74 |
75 | ));
76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
77 |
78 | const DropdownMenuItem = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef & {
81 | inset?: boolean;
82 | }
83 | >(({ className, inset, ...props }, ref) => (
84 | svg]:size-4 [&>svg]:shrink-0",
88 | inset && "pl-8",
89 | className,
90 | )}
91 | {...props}
92 | />
93 | ));
94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
95 |
96 | const DropdownMenuCheckboxItem = React.forwardRef<
97 | React.ElementRef,
98 | React.ComponentPropsWithoutRef
99 | >(({ className, children, checked, ...props }, ref) => (
100 |
109 |
110 |
111 |
112 |
113 |
114 | {children}
115 |
116 | ));
117 | DropdownMenuCheckboxItem.displayName =
118 | DropdownMenuPrimitive.CheckboxItem.displayName;
119 |
120 | const DropdownMenuRadioItem = React.forwardRef<
121 | React.ElementRef,
122 | React.ComponentPropsWithoutRef
123 | >(({ className, children, ...props }, ref) => (
124 |
132 |
133 |
134 |
135 |
136 |
137 | {children}
138 |
139 | ));
140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
141 |
142 | const DropdownMenuLabel = React.forwardRef<
143 | React.ElementRef,
144 | React.ComponentPropsWithoutRef & {
145 | inset?: boolean;
146 | }
147 | >(({ className, inset, ...props }, ref) => (
148 |
157 | ));
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ));
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
171 |
172 | const DropdownMenuShortcut = ({
173 | className,
174 | ...props
175 | }: React.HTMLAttributes) => {
176 | return (
177 |
181 | );
182 | };
183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
184 |
185 | export {
186 | DropdownMenu,
187 | DropdownMenuTrigger,
188 | DropdownMenuContent,
189 | DropdownMenuItem,
190 | DropdownMenuCheckboxItem,
191 | DropdownMenuRadioItem,
192 | DropdownMenuLabel,
193 | DropdownMenuSeparator,
194 | DropdownMenuShortcut,
195 | DropdownMenuGroup,
196 | DropdownMenuPortal,
197 | DropdownMenuSub,
198 | DropdownMenuSubContent,
199 | DropdownMenuSubTrigger,
200 | DropdownMenuRadioGroup,
201 | };
202 |
--------------------------------------------------------------------------------