tr]:last:border-b-0",
48 | className,
49 | )}
50 | {...props}
51 | />
52 | );
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | );
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className,
75 | )}
76 | {...props}
77 | />
78 | );
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | | [role=checkbox]]:translate-y-[2px]",
87 | className,
88 | )}
89 | {...props}
90 | />
91 | );
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | );
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | };
117 |
--------------------------------------------------------------------------------
/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | ChevronLeftIcon,
4 | ChevronRightIcon,
5 | MoreHorizontalIcon,
6 | } from "lucide-react";
7 |
8 | import { cn } from "@/lib/utils";
9 | import { Button, buttonVariants } from "@/components/ui/button";
10 |
11 | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12 | return (
13 |
20 | );
21 | }
22 |
23 | function PaginationContent({
24 | className,
25 | ...props
26 | }: React.ComponentProps<"ul">) {
27 | return (
28 |
33 | );
34 | }
35 |
36 | function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37 | return ;
38 | }
39 |
40 | type PaginationLinkProps = {
41 | isActive?: boolean;
42 | } & Pick, "size"> &
43 | React.ComponentProps<"a">;
44 |
45 | function PaginationLink({
46 | className,
47 | isActive,
48 | size = "icon",
49 | ...props
50 | }: PaginationLinkProps) {
51 | return (
52 |
65 | );
66 | }
67 |
68 | function PaginationPrevious({
69 | className,
70 | ...props
71 | }: React.ComponentProps) {
72 | return (
73 |
79 |
80 | Previous
81 |
82 | );
83 | }
84 |
85 | function PaginationNext({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
96 | Next
97 |
98 |
99 | );
100 | }
101 |
102 | function PaginationEllipsis({
103 | className,
104 | ...props
105 | }: React.ComponentProps<"span">) {
106 | return (
107 |
113 |
114 | More pages
115 |
116 | );
117 | }
118 |
119 | export {
120 | Pagination,
121 | PaginationContent,
122 | PaginationLink,
123 | PaginationItem,
124 | PaginationPrevious,
125 | PaginationNext,
126 | PaginationEllipsis,
127 | };
128 |
--------------------------------------------------------------------------------
/SUPABASE_CLERK_SETUP.md:
--------------------------------------------------------------------------------
1 | # Clerk + Supabase Integration Setup Guide
2 |
3 | This guide will help you set up the integration between Clerk and Supabase using the modern third-party auth approach. This setup uses Clerk for user management and Supabase for your application data.
4 |
5 | ## Prerequisites
6 |
7 | - A Clerk account and application
8 | - A Supabase project
9 | - Next.js application with both `@clerk/nextjs` and `@supabase/supabase-js` installed
10 |
11 | ## Step 1: Configure Supabase Third-Party Auth
12 |
13 | 1. Go to your **Supabase Dashboard**
14 | 2. Navigate to **Authentication > Integrations**
15 | 3. Add a new **Third-Party Auth** integration
16 | 4. Select **Clerk** from the list
17 | 5. Follow the configuration steps provided by Supabase
18 |
19 | ## Step 2: Configure Clerk for Supabase
20 |
21 | 1. Visit **Clerk's Connect with Supabase** page in your Clerk dashboard
22 | 2. Follow the setup instructions to configure your Clerk instance for Supabase compatibility
23 | 3. This will set up the necessary JWT claims and configuration
24 |
25 | ## Step 3: Environment Variables
26 |
27 | Copy your `.env.example` to `.env.local` and fill in the required values:
28 |
29 | ```bash
30 | # Clerk Authentication
31 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
32 | CLERK_SECRET_KEY=sk_test_...
33 |
34 | # Supabase
35 | NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
36 | NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
37 | ```
38 |
39 | ## Step 4: Test the Integration
40 |
41 | 1. Start your Next.js application
42 | 2. Sign up/in through Clerk
43 | 3. Try making authenticated requests to Supabase
44 |
45 | ## Usage Examples
46 |
47 | ### Server-side data fetching:
48 |
49 | ```typescript
50 | import { createSupabaseServerClient } from '@/lib/supabase'
51 |
52 | export async function getData() {
53 | const supabase = await createSupabaseServerClient()
54 |
55 | const { data, error } = await supabase
56 | .from('your_table')
57 | .select('*')
58 |
59 | return data
60 | }
61 | ```
62 |
63 | ### Client-side usage:
64 |
65 | ```typescript
66 | import { supabase } from '@/lib/supabase'
67 | import { useAuth } from '@clerk/nextjs'
68 |
69 | function MyComponent() {
70 | const { getToken } = useAuth()
71 |
72 | const fetchData = async () => {
73 | const token = await getToken()
74 |
75 | const { data, error } = await supabase
76 | .from('your_table')
77 | .select('*')
78 | .auth(token)
79 | }
80 | }
81 | ```
82 |
83 | ### Get current user (from Clerk):
84 |
85 | ```typescript
86 | import { getCurrentUser } from '@/lib/user'
87 |
88 | export async function ProfilePage() {
89 | const user = await getCurrentUser()
90 |
91 | if (!user) {
92 | redirect('/sign-in')
93 | }
94 |
95 | return Welcome {user.firstName}!
96 | }
97 | ```
98 |
99 | ## Row Level Security (RLS)
100 |
101 | Create RLS policies in your Supabase tables that use Clerk user IDs. Example:
102 |
103 | ```sql
104 | -- Enable RLS on your table
105 | ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
106 |
107 | -- Allow users to read their own data
108 | CREATE POLICY "Users can read own data" ON your_table
109 | FOR SELECT USING (auth.jwt() ->> 'sub' = user_id);
110 |
111 | -- Allow users to insert their own data
112 | CREATE POLICY "Users can insert own data" ON your_table
113 | FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = user_id);
114 | ```
115 |
116 | ## Troubleshooting
117 |
118 | ### Authentication errors
119 | - Ensure third-party auth is properly configured in Supabase
120 | - Verify your Supabase URL and keys are correct
121 | - Check that RLS policies allow the operations you're trying to perform
122 |
123 | ### Token issues
124 | - Make sure you're using `createSupabaseServerClient()` for server-side operations
125 | - For client-side, ensure you're passing the Clerk token to Supabase
--------------------------------------------------------------------------------
/.devcontainer/init-firewall.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -euo pipefail # Exit on error, undefined vars, and pipeline failures
3 | IFS=$'\n\t' # Stricter word splitting
4 |
5 | # Flush existing rules and delete existing ipsets
6 | iptables -F
7 | iptables -X
8 | iptables -t nat -F
9 | iptables -t nat -X
10 | iptables -t mangle -F
11 | iptables -t mangle -X
12 | ipset destroy allowed-domains 2>/dev/null || true
13 |
14 | # First allow DNS and localhost before any restrictions
15 | # Allow outbound DNS
16 | iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
17 | # Allow inbound DNS responses
18 | iptables -A INPUT -p udp --sport 53 -j ACCEPT
19 | # Allow outbound SSH
20 | iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
21 | # Allow inbound SSH responses
22 | iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
23 | # Allow localhost
24 | iptables -A INPUT -i lo -j ACCEPT
25 | iptables -A OUTPUT -o lo -j ACCEPT
26 |
27 | # Create ipset with CIDR support
28 | ipset create allowed-domains hash:net
29 |
30 | # Fetch GitHub meta information and aggregate + add their IP ranges
31 | echo "Fetching GitHub IP ranges..."
32 | gh_ranges=$(curl -s https://api.github.com/meta)
33 | if [ -z "$gh_ranges" ]; then
34 | echo "ERROR: Failed to fetch GitHub IP ranges"
35 | exit 1
36 | fi
37 |
38 | if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
39 | echo "ERROR: GitHub API response missing required fields"
40 | exit 1
41 | fi
42 |
43 | echo "Processing GitHub IPs..."
44 | while read -r cidr; do
45 | if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
46 | echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
47 | exit 1
48 | fi
49 | echo "Adding GitHub range $cidr"
50 | ipset add allowed-domains "$cidr"
51 | done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
52 |
53 | # Resolve and add other allowed domains
54 | for domain in \
55 | "registry.npmjs.org" \
56 | "api.anthropic.com" \
57 | "sentry.io" \
58 | "statsig.anthropic.com" \
59 | "statsig.com"; do
60 | echo "Resolving $domain..."
61 | ips=$(dig +short A "$domain")
62 | if [ -z "$ips" ]; then
63 | echo "ERROR: Failed to resolve $domain"
64 | exit 1
65 | fi
66 |
67 | while read -r ip; do
68 | if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
69 | echo "ERROR: Invalid IP from DNS for $domain: $ip"
70 | exit 1
71 | fi
72 | echo "Adding $ip for $domain"
73 | ipset add allowed-domains "$ip"
74 | done < <(echo "$ips")
75 | done
76 |
77 | # Get host IP from default route
78 | HOST_IP=$(ip route | grep default | cut -d" " -f3)
79 | if [ -z "$HOST_IP" ]; then
80 | echo "ERROR: Failed to detect host IP"
81 | exit 1
82 | fi
83 |
84 | HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
85 | echo "Host network detected as: $HOST_NETWORK"
86 |
87 | # Set up remaining iptables rules
88 | iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
89 | iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
90 |
91 | # Set default policies to DROP first
92 | iptables -P INPUT DROP
93 | iptables -P FORWARD DROP
94 | iptables -P OUTPUT DROP
95 |
96 | # First allow established connections for already approved traffic
97 | iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
98 | iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
99 |
100 | # Then allow only specific outbound traffic to allowed domains
101 | iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
102 |
103 | echo "Firewall configuration complete"
104 | echo "Verifying firewall rules..."
105 | if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
106 | echo "ERROR: Firewall verification failed - was able to reach https://example.com"
107 | exit 1
108 | else
109 | echo "Firewall verification passed - unable to reach https://example.com as expected"
110 | fi
111 |
112 | # Verify GitHub API access
113 | if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
114 | echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
115 | exit 1
116 | else
117 | echo "Firewall verification passed - able to reach https://api.github.com as expected"
118 | fi
119 |
--------------------------------------------------------------------------------
/src/components/chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useChat } from "ai/react";
4 | import { Button } from "@/components/ui/button";
5 | import { Input } from "@/components/ui/input";
6 | import { Card } from "@/components/ui/card";
7 | import { AlertCircle, Bot, User } from "lucide-react";
8 |
9 | export default function Chat() {
10 | const {
11 | messages,
12 | input,
13 | handleInputChange,
14 | handleSubmit,
15 | error,
16 | isLoading: chatLoading,
17 | } = useChat({
18 | onError: (error) => {
19 | console.error("Chat error:", error);
20 | },
21 | });
22 |
23 | return (
24 |
25 |
26 | 🤖 AI Chat
27 |
28 | Powered by OpenAI GPT-4o • Protected by Clerk Authentication
29 |
30 |
31 |
32 | {error && (
33 |
34 |
35 |
36 |
37 | {error.message ||
38 | "An error occurred while processing your request."}
39 |
40 |
41 |
42 | )}
43 |
44 |
45 | {messages.length === 0 ? (
46 |
47 |
48 |
49 | Start a conversation
50 |
51 |
52 | Ask me anything! I'm here to help with coding, questions, or
53 | just chat.
54 |
55 |
56 | ) : (
57 | messages.map((m) => (
58 |
59 |
60 | {m.role === "user" ? (
61 |
62 | ) : (
63 |
64 | )}
65 |
66 | {m.role === "user" ? "You" : "AI Assistant"}
67 |
68 |
69 |
70 | {m.content}
71 |
72 |
73 | ))
74 | )}
75 |
76 | {chatLoading && (
77 |
78 |
79 |
80 | AI Assistant
81 |
82 |
95 |
96 | )}
97 |
98 |
99 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/src/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 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form";
15 |
16 | import { cn } from "@/lib/utils";
17 | import { Label } from "@/components/ui/label";
18 |
19 | const Form = FormProvider;
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName;
26 | };
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue,
30 | );
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext);
47 | const itemContext = React.useContext(FormItemContext);
48 | const { getFieldState } = useFormContext();
49 | const formState = useFormState({ name: fieldContext.name });
50 | const fieldState = getFieldState(fieldContext.name, formState);
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ");
54 | }
55 |
56 | const { id } = itemContext;
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | };
66 | };
67 |
68 | type FormItemContextValue = {
69 | id: string;
70 | };
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue,
74 | );
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
86 |
87 | );
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField();
95 |
96 | return (
97 |
104 | );
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | }
125 |
126 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127 | const { formDescriptionId } = useFormField();
128 |
129 | return (
130 |
136 | );
137 | }
138 |
139 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140 | const { error, formMessageId } = useFormField();
141 | const body = error ? String(error?.message ?? "") : props.children;
142 |
143 | if (!body) {
144 | return null;
145 | }
146 |
147 | return (
148 |
154 | {body}
155 |
156 | );
157 | }
158 |
159 | export {
160 | useFormField,
161 | Form,
162 | FormItem,
163 | FormLabel,
164 | FormControl,
165 | FormDescription,
166 | FormMessage,
167 | FormField,
168 | };
169 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5 |
6 | import { cn } from "@/lib/utils";
7 | import { buttonVariants } from "@/components/ui/button";
8 |
9 | function AlertDialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function AlertDialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | );
21 | }
22 |
23 | function AlertDialogPortal({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 | );
29 | }
30 |
31 | function AlertDialogOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | );
45 | }
46 |
47 | function AlertDialogContent({
48 | className,
49 | ...props
50 | }: React.ComponentProps) {
51 | return (
52 |
53 |
54 |
62 |
63 | );
64 | }
65 |
66 | function AlertDialogHeader({
67 | className,
68 | ...props
69 | }: React.ComponentProps<"div">) {
70 | return (
71 |
76 | );
77 | }
78 |
79 | function AlertDialogFooter({
80 | className,
81 | ...props
82 | }: React.ComponentProps<"div">) {
83 | return (
84 |
92 | );
93 | }
94 |
95 | function AlertDialogTitle({
96 | className,
97 | ...props
98 | }: React.ComponentProps) {
99 | return (
100 |
105 | );
106 | }
107 |
108 | function AlertDialogDescription({
109 | className,
110 | ...props
111 | }: React.ComponentProps) {
112 | return (
113 |
118 | );
119 | }
120 |
121 | function AlertDialogAction({
122 | className,
123 | ...props
124 | }: React.ComponentProps) {
125 | return (
126 |
130 | );
131 | }
132 |
133 | function AlertDialogCancel({
134 | className,
135 | ...props
136 | }: React.ComponentProps) {
137 | return (
138 |
142 | );
143 | }
144 |
145 | export {
146 | AlertDialog,
147 | AlertDialogPortal,
148 | AlertDialogOverlay,
149 | AlertDialogTrigger,
150 | AlertDialogContent,
151 | AlertDialogHeader,
152 | AlertDialogFooter,
153 | AlertDialogTitle,
154 | AlertDialogDescription,
155 | AlertDialogAction,
156 | AlertDialogCancel,
157 | };
158 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { XIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return ;
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return ;
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return ;
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | );
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | showCloseButton = true,
53 | ...props
54 | }: React.ComponentProps & {
55 | showCloseButton?: boolean;
56 | }) {
57 | return (
58 |
59 |
60 |
68 | {children}
69 | {showCloseButton && (
70 |
74 |
75 | Close
76 |
77 | )}
78 |
79 |
80 | );
81 | }
82 |
83 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84 | return (
85 |
90 | );
91 | }
92 |
93 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94 | return (
95 |
103 | );
104 | }
105 |
106 | function DialogTitle({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
116 | );
117 | }
118 |
119 | function DialogDescription({
120 | className,
121 | ...props
122 | }: React.ComponentProps) {
123 | return (
124 |
129 | );
130 | }
131 |
132 | export {
133 | Dialog,
134 | DialogClose,
135 | DialogContent,
136 | DialogDescription,
137 | DialogFooter,
138 | DialogHeader,
139 | DialogOverlay,
140 | DialogPortal,
141 | DialogTitle,
142 | DialogTrigger,
143 | };
144 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --font-sans: var(--font-geist-sans);
10 | --font-mono: var(--font-geist-mono);
11 | --font-parkinsans: var(--font-parkinsans);
12 | --color-sidebar-ring: var(--sidebar-ring);
13 | --color-sidebar-border: var(--sidebar-border);
14 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
15 | --color-sidebar-accent: var(--sidebar-accent);
16 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
17 | --color-sidebar-primary: var(--sidebar-primary);
18 | --color-sidebar-foreground: var(--sidebar-foreground);
19 | --color-sidebar: var(--sidebar);
20 | --color-chart-5: var(--chart-5);
21 | --color-chart-4: var(--chart-4);
22 | --color-chart-3: var(--chart-3);
23 | --color-chart-2: var(--chart-2);
24 | --color-chart-1: var(--chart-1);
25 | --color-ring: var(--ring);
26 | --color-input: var(--input);
27 | --color-border: var(--border);
28 | --color-destructive: var(--destructive);
29 | --color-accent-foreground: var(--accent-foreground);
30 | --color-accent: var(--accent);
31 | --color-muted-foreground: var(--muted-foreground);
32 | --color-muted: var(--muted);
33 | --color-secondary-foreground: var(--secondary-foreground);
34 | --color-secondary: var(--secondary);
35 | --color-primary-foreground: var(--primary-foreground);
36 | --color-primary: var(--primary);
37 | --color-popover-foreground: var(--popover-foreground);
38 | --color-popover: var(--popover);
39 | --color-card-foreground: var(--card-foreground);
40 | --color-card: var(--card);
41 | --radius-sm: calc(var(--radius) - 4px);
42 | --radius-md: calc(var(--radius) - 2px);
43 | --radius-lg: var(--radius);
44 | --radius-xl: calc(var(--radius) + 4px);
45 | }
46 |
47 | :root {
48 | --radius: 0.625rem;
49 | --background: oklch(1 0 0);
50 | --foreground: oklch(0.145 0 0);
51 | --card: oklch(1 0 0);
52 | --card-foreground: oklch(0.145 0 0);
53 | --popover: oklch(1 0 0);
54 | --popover-foreground: oklch(0.145 0 0);
55 | --primary: oklch(0.205 0 0);
56 | --primary-foreground: oklch(0.985 0 0);
57 | --secondary: oklch(0.97 0 0);
58 | --secondary-foreground: oklch(0.205 0 0);
59 | --muted: oklch(0.97 0 0);
60 | --muted-foreground: oklch(0.556 0 0);
61 | --accent: oklch(0.97 0 0);
62 | --accent-foreground: oklch(0.205 0 0);
63 | --destructive: oklch(0.577 0.245 27.325);
64 | --border: oklch(0.922 0 0);
65 | --input: oklch(0.922 0 0);
66 | --ring: oklch(0.708 0 0);
67 | --chart-1: oklch(0.646 0.222 41.116);
68 | --chart-2: oklch(0.6 0.118 184.704);
69 | --chart-3: oklch(0.398 0.07 227.392);
70 | --chart-4: oklch(0.828 0.189 84.429);
71 | --chart-5: oklch(0.769 0.188 70.08);
72 | --sidebar: oklch(0.985 0 0);
73 | --sidebar-foreground: oklch(0.145 0 0);
74 | --sidebar-primary: oklch(0.205 0 0);
75 | --sidebar-primary-foreground: oklch(0.985 0 0);
76 | --sidebar-accent: oklch(0.97 0 0);
77 | --sidebar-accent-foreground: oklch(0.205 0 0);
78 | --sidebar-border: oklch(0.922 0 0);
79 | --sidebar-ring: oklch(0.708 0 0);
80 | }
81 |
82 | .dark {
83 | --background: oklch(0.145 0 0);
84 | --foreground: oklch(0.985 0 0);
85 | --card: oklch(0.205 0 0);
86 | --card-foreground: oklch(0.985 0 0);
87 | --popover: oklch(0.205 0 0);
88 | --popover-foreground: oklch(0.985 0 0);
89 | --primary: oklch(0.922 0 0);
90 | --primary-foreground: oklch(0.205 0 0);
91 | --secondary: oklch(0.269 0 0);
92 | --secondary-foreground: oklch(0.985 0 0);
93 | --muted: oklch(0.269 0 0);
94 | --muted-foreground: oklch(0.708 0 0);
95 | --accent: oklch(0.269 0 0);
96 | --accent-foreground: oklch(0.985 0 0);
97 | --destructive: oklch(0.704 0.191 22.216);
98 | --border: oklch(1 0 0 / 10%);
99 | --input: oklch(1 0 0 / 15%);
100 | --ring: oklch(0.556 0 0);
101 | --chart-1: oklch(0.488 0.243 264.376);
102 | --chart-2: oklch(0.696 0.17 162.48);
103 | --chart-3: oklch(0.769 0.188 70.08);
104 | --chart-4: oklch(0.627 0.265 303.9);
105 | --chart-5: oklch(0.645 0.246 16.439);
106 | --sidebar: oklch(0.205 0 0);
107 | --sidebar-foreground: oklch(0.985 0 0);
108 | --sidebar-primary: oklch(0.488 0.243 264.376);
109 | --sidebar-primary-foreground: oklch(0.985 0 0);
110 | --sidebar-accent: oklch(0.269 0 0);
111 | --sidebar-accent-foreground: oklch(0.985 0 0);
112 | --sidebar-border: oklch(1 0 0 / 10%);
113 | --sidebar-ring: oklch(0.556 0 0);
114 | }
115 |
116 | @layer base {
117 | * {
118 | @apply border-border outline-ring/50;
119 | }
120 | body {
121 | @apply bg-background text-foreground;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/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 { XIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return ;
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return ;
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return ;
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return ;
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | );
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left";
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 |
76 |
77 | Close
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
91 | );
92 | }
93 |
94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95 | return (
96 |
101 | );
102 | }
103 |
104 | function SheetTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | );
115 | }
116 |
117 | function SheetDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | );
128 | }
129 |
130 | export {
131 | Sheet,
132 | SheetTrigger,
133 | SheetClose,
134 | SheetContent,
135 | SheetHeader,
136 | SheetFooter,
137 | SheetTitle,
138 | SheetDescription,
139 | };
140 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Drawer({
9 | ...props
10 | }: React.ComponentProps) {
11 | return ;
12 | }
13 |
14 | function DrawerTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return ;
18 | }
19 |
20 | function DrawerPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return ;
24 | }
25 |
26 | function DrawerClose({
27 | ...props
28 | }: React.ComponentProps) {
29 | return ;
30 | }
31 |
32 | function DrawerOverlay({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | );
46 | }
47 |
48 | function DrawerContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
56 |
68 |
69 | {children}
70 |
71 |
72 | );
73 | }
74 |
75 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
85 | );
86 | }
87 |
88 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
89 | return (
90 |
95 | );
96 | }
97 |
98 | function DrawerTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | );
109 | }
110 |
111 | function DrawerDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | );
122 | }
123 |
124 | export {
125 | Drawer,
126 | DrawerPortal,
127 | DrawerOverlay,
128 | DrawerTrigger,
129 | DrawerClose,
130 | DrawerContent,
131 | DrawerHeader,
132 | DrawerFooter,
133 | DrawerTitle,
134 | DrawerDescription,
135 | };
136 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to CodeGuide Starter Kit
2 |
3 | We welcome contributions to the CodeGuide Starter Kit! This document provides guidelines for contributing to the project.
4 |
5 | ## Getting Started
6 |
7 | 1. Fork the repository
8 | 2. Clone your fork locally
9 | 3. Install dependencies: `npm install`
10 | 4. Copy `.env.example` to `.env.local` and configure your environment variables
11 | 5. Start the development server: `npm run dev`
12 |
13 | ## Development Process
14 |
15 | ### Setting Up Your Development Environment
16 |
17 | 1. **Prerequisites**:
18 | - Node.js 18 or later
19 | - npm or yarn package manager
20 | - Git
21 |
22 | 2. **Environment Setup**:
23 | - Follow the setup instructions in `SUPABASE_CLERK_SETUP.md`
24 | - Ensure all required environment variables are configured
25 | - Test the application runs correctly with `npm run dev`
26 |
27 | ### Making Changes
28 |
29 | 1. Create a new branch for your feature or bug fix:
30 | ```bash
31 | git checkout -b feature/your-feature-name
32 | ```
33 |
34 | 2. Make your changes following our coding standards (see below)
35 |
36 | 3. Test your changes thoroughly:
37 | ```bash
38 | npm run lint
39 | npm run build
40 | ```
41 |
42 | 4. Commit your changes with a descriptive commit message:
43 | ```bash
44 | git commit -m "feat: add new feature description"
45 | ```
46 |
47 | 5. Push to your fork and create a pull request
48 |
49 | ## Coding Standards
50 |
51 | ### TypeScript
52 | - Use strict TypeScript throughout the codebase
53 | - Define proper types for all props, functions, and API responses
54 | - Utilize the configured path aliases (`@/`) for imports
55 |
56 | ### React & Next.js
57 | - Use server components by default, client components only when needed
58 | - Follow the established patterns in the codebase
59 | - Utilize the App Router structure (`/src/app`)
60 |
61 | ### Styling
62 | - Use TailwindCSS for all styling
63 | - Follow the established design system with shadcn/ui components
64 | - Support both light and dark themes using CSS custom properties
65 | - Use existing UI components before creating new ones
66 |
67 | ### Database
68 | - All new tables must implement Row Level Security (RLS)
69 | - Use Clerk user IDs (`auth.jwt() ->> 'sub'`) in RLS policies
70 | - Follow the established patterns in `supabase/migrations/`
71 |
72 | ### Code Organization
73 | - Place reusable utilities in `src/lib/`
74 | - Use PascalCase for components
75 | - Group related functionality in appropriate directories
76 | - Keep components focused and single-purpose
77 |
78 | ## Pull Request Guidelines
79 |
80 | ### Before Submitting
81 | - [ ] Code follows the project's style guidelines
82 | - [ ] Self-review of the code has been performed
83 | - [ ] Code is properly commented where necessary
84 | - [ ] Changes have been tested locally
85 | - [ ] Lint checks pass (`npm run lint`)
86 | - [ ] Build succeeds (`npm run build`)
87 |
88 | ### PR Description
89 | Please include:
90 | - Clear description of what the PR does
91 | - Why the change is needed
92 | - Screenshots for UI changes
93 | - Any breaking changes or migration notes
94 |
95 | ### Review Process
96 | - All PRs require at least one review
97 | - Address all review feedback before merging
98 | - Maintain a clean git history
99 |
100 | ## Types of Contributions
101 |
102 | ### Bug Reports
103 | When filing a bug report, please include:
104 | - Clear description of the issue
105 | - Steps to reproduce
106 | - Expected vs actual behavior
107 | - Environment details (OS, Node version, etc.)
108 | - Screenshots or error logs if applicable
109 |
110 | ### Feature Requests
111 | For new features:
112 | - Describe the problem you're solving
113 | - Explain why this feature would be beneficial
114 | - Consider backward compatibility
115 | - Discuss implementation approach if applicable
116 |
117 | ### Documentation
118 | - Keep documentation up to date with code changes
119 | - Improve existing documentation clarity
120 | - Add examples for complex features
121 | - Update CLAUDE.md when adding new patterns
122 |
123 | ## Code Examples
124 |
125 | ### Adding a New Component
126 | ```typescript
127 | // src/components/ui/my-component.tsx
128 | import { cn } from "@/lib/utils"
129 |
130 | interface MyComponentProps {
131 | className?: string
132 | children: React.ReactNode
133 | }
134 |
135 | export function MyComponent({ className, children }: MyComponentProps) {
136 | return (
137 |
138 | {children}
139 |
140 | )
141 | }
142 | ```
143 |
144 | ### Database Migration Example
145 | ```sql
146 | -- supabase/migrations/002_new_feature.sql
147 | CREATE TABLE example_table (
148 | id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
149 | user_id TEXT NOT NULL,
150 | name TEXT NOT NULL,
151 | created_at TIMESTAMPTZ DEFAULT NOW()
152 | );
153 |
154 | ALTER TABLE example_table ENABLE ROW LEVEL SECURITY;
155 |
156 | CREATE POLICY "Users can manage own records" ON example_table
157 | FOR ALL USING (auth.jwt() ->> 'sub' = user_id);
158 | ```
159 |
160 | ## Community Guidelines
161 |
162 | - Be respectful and inclusive
163 | - Help others learn and grow
164 | - Provide constructive feedback
165 | - Follow our code of conduct
166 | - Ask questions if you're unsure about anything
167 |
168 | ## Getting Help
169 |
170 | - Check existing issues and discussions
171 | - Review the project documentation
172 | - Ask questions in pull request discussions
173 | - Reach out to maintainers for guidance
174 |
175 | Thank you for contributing to CodeGuide Starter Kit!
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Command as CommandPrimitive } from "cmdk";
5 | import { SearchIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from "@/components/ui/dialog";
15 |
16 | function Command({
17 | className,
18 | ...props
19 | }: React.ComponentProps) {
20 | return (
21 |
29 | );
30 | }
31 |
32 | function CommandDialog({
33 | title = "Command Palette",
34 | description = "Search for a command to run...",
35 | children,
36 | className,
37 | showCloseButton = true,
38 | ...props
39 | }: React.ComponentProps & {
40 | title?: string;
41 | description?: string;
42 | className?: string;
43 | showCloseButton?: boolean;
44 | }) {
45 | return (
46 |
60 | );
61 | }
62 |
63 | function CommandInput({
64 | className,
65 | ...props
66 | }: React.ComponentProps) {
67 | return (
68 |
72 |
73 |
81 |
82 | );
83 | }
84 |
85 | function CommandList({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
98 | );
99 | }
100 |
101 | function CommandEmpty({
102 | ...props
103 | }: React.ComponentProps) {
104 | return (
105 |
110 | );
111 | }
112 |
113 | function CommandGroup({
114 | className,
115 | ...props
116 | }: React.ComponentProps) {
117 | return (
118 |
126 | );
127 | }
128 |
129 | function CommandSeparator({
130 | className,
131 | ...props
132 | }: React.ComponentProps) {
133 | return (
134 |
139 | );
140 | }
141 |
142 | function CommandItem({
143 | className,
144 | ...props
145 | }: React.ComponentProps) {
146 | return (
147 |
155 | );
156 | }
157 |
158 | function CommandShortcut({
159 | className,
160 | ...props
161 | }: React.ComponentProps<"span">) {
162 | return (
163 |
171 | );
172 | }
173 |
174 | export {
175 | Command,
176 | CommandDialog,
177 | CommandInput,
178 | CommandList,
179 | CommandEmpty,
180 | CommandGroup,
181 | CommandItem,
182 | CommandShortcut,
183 | CommandSeparator,
184 | };
185 |
--------------------------------------------------------------------------------
/src/components/ui/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react";
7 | import { ArrowLeft, ArrowRight } from "lucide-react";
8 |
9 | import { cn } from "@/lib/utils";
10 | import { Button } from "@/components/ui/button";
11 |
12 | type CarouselApi = UseEmblaCarouselType[1];
13 | type UseCarouselParameters = Parameters;
14 | type CarouselOptions = UseCarouselParameters[0];
15 | type CarouselPlugin = UseCarouselParameters[1];
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions;
19 | plugins?: CarouselPlugin;
20 | orientation?: "horizontal" | "vertical";
21 | setApi?: (api: CarouselApi) => void;
22 | };
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0];
26 | api: ReturnType[1];
27 | scrollPrev: () => void;
28 | scrollNext: () => void;
29 | canScrollPrev: boolean;
30 | canScrollNext: boolean;
31 | } & CarouselProps;
32 |
33 | const CarouselContext = React.createContext(null);
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext);
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ");
40 | }
41 |
42 | return context;
43 | }
44 |
45 | function Carousel({
46 | orientation = "horizontal",
47 | opts,
48 | setApi,
49 | plugins,
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps<"div"> & CarouselProps) {
54 | const [carouselRef, api] = useEmblaCarousel(
55 | {
56 | ...opts,
57 | axis: orientation === "horizontal" ? "x" : "y",
58 | },
59 | plugins,
60 | );
61 | const [canScrollPrev, setCanScrollPrev] = React.useState(false);
62 | const [canScrollNext, setCanScrollNext] = React.useState(false);
63 |
64 | const onSelect = React.useCallback((api: CarouselApi) => {
65 | if (!api) return;
66 | setCanScrollPrev(api.canScrollPrev());
67 | setCanScrollNext(api.canScrollNext());
68 | }, []);
69 |
70 | const scrollPrev = React.useCallback(() => {
71 | api?.scrollPrev();
72 | }, [api]);
73 |
74 | const scrollNext = React.useCallback(() => {
75 | api?.scrollNext();
76 | }, [api]);
77 |
78 | const handleKeyDown = React.useCallback(
79 | (event: React.KeyboardEvent) => {
80 | if (event.key === "ArrowLeft") {
81 | event.preventDefault();
82 | scrollPrev();
83 | } else if (event.key === "ArrowRight") {
84 | event.preventDefault();
85 | scrollNext();
86 | }
87 | },
88 | [scrollPrev, scrollNext],
89 | );
90 |
91 | React.useEffect(() => {
92 | if (!api || !setApi) return;
93 | setApi(api);
94 | }, [api, setApi]);
95 |
96 | React.useEffect(() => {
97 | if (!api) return;
98 | onSelect(api);
99 | api.on("reInit", onSelect);
100 | api.on("select", onSelect);
101 |
102 | return () => {
103 | api?.off("select", onSelect);
104 | };
105 | }, [api, onSelect]);
106 |
107 | return (
108 |
121 |
129 | {children}
130 |
131 |
132 | );
133 | }
134 |
135 | function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136 | const { carouselRef, orientation } = useCarousel();
137 |
138 | return (
139 |
153 | );
154 | }
155 |
156 | function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157 | const { orientation } = useCarousel();
158 |
159 | return (
160 |
171 | );
172 | }
173 |
174 | function CarouselPrevious({
175 | className,
176 | variant = "outline",
177 | size = "icon",
178 | ...props
179 | }: React.ComponentProps) {
180 | const { orientation, scrollPrev, canScrollPrev } = useCarousel();
181 |
182 | return (
183 |
201 | );
202 | }
203 |
204 | function CarouselNext({
205 | className,
206 | variant = "outline",
207 | size = "icon",
208 | ...props
209 | }: React.ComponentProps) {
210 | const { orientation, scrollNext, canScrollNext } = useCarousel();
211 |
212 | return (
213 |
231 | );
232 | }
233 |
234 | export {
235 | type CarouselApi,
236 | Carousel,
237 | CarouselContent,
238 | CarouselItem,
239 | CarouselPrevious,
240 | CarouselNext,
241 | };
242 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
4 | import Chat from "@/components/chat";
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | CheckCircle,
8 | Zap,
9 | Database,
10 | Shield,
11 | ExternalLink,
12 | } from "lucide-react";
13 | import { ThemeToggle } from "@/components/theme-toggle";
14 | import Image from "next/image";
15 |
16 | export default function Home() {
17 |
18 | return (
19 |
20 | {/* Hero Section */}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
46 |
47 | CodeGuide Starter
48 |
49 |
50 |
51 | Build faster with your AI coding agent
52 |
53 |
54 |
55 |
56 |
57 | ⚠️
58 | Setup Required
59 |
60 | Add environment variables to get started
61 |
62 |
63 |
64 |
65 | {/* Clerk */}
66 |
67 |
68 |
69 |
70 |
71 | Clerk Auth
72 |
73 |
74 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
75 | CLERK_SECRET_KEY
76 |
77 |
88 |
89 |
90 | {/* Supabase */}
91 |
92 |
93 |
94 |
95 |
96 | Supabase DB
97 |
98 |
99 | NEXT_PUBLIC_SUPABASE_URL
100 | NEXT_PUBLIC_SUPABASE_ANON_KEY
101 |
102 |
113 |
114 |
115 | {/* AI */}
116 |
117 |
118 |
119 |
120 |
121 | AI SDK
122 |
123 |
124 | OPENAI_API_KEY
125 | ANTHROPIC_API_KEY
126 |
127 |
128 |
138 |
148 |
149 |
150 |
151 |
152 | {/* Chat Section */}
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://codeguide.dev)
2 |
3 | # CodeGuide Starter Kit
4 |
5 | A modern web application starter template built with Next.js 15, featuring authentication, database integration, AI capabilities, and dark mode support.
6 |
7 | ## Tech Stack
8 |
9 | - **Framework:** [Next.js 15](https://nextjs.org/) (App Router)
10 | - **Language:** TypeScript
11 | - **Authentication:** [Clerk](https://clerk.com/)
12 | - **Database:** [Supabase](https://supabase.com/)
13 | - **Styling:** [Tailwind CSS v4](https://tailwindcss.com/)
14 | - **UI Components:** [shadcn/ui](https://ui.shadcn.com/)
15 | - **AI Integration:** [Vercel AI SDK](https://sdk.vercel.ai/)
16 | - **Theme System:** [next-themes](https://github.com/pacocoursey/next-themes)
17 |
18 | ## Prerequisites
19 |
20 | Before you begin, ensure you have the following:
21 | - Node.js 18+ installed
22 | - A [Clerk](https://clerk.com/) account for authentication
23 | - A [Supabase](https://supabase.com/) account for database
24 | - Optional: [OpenAI](https://platform.openai.com/) or [Anthropic](https://console.anthropic.com/) API key for AI features
25 | - Generated project documents from [CodeGuide](https://codeguide.dev/) for best development experience
26 |
27 | ## Getting Started
28 |
29 | 1. **Clone the repository**
30 | ```bash
31 | git clone
32 | cd codeguide-starter-kit
33 | ```
34 |
35 | 2. **Install dependencies**
36 | ```bash
37 | npm install
38 | # or
39 | yarn install
40 | # or
41 | pnpm install
42 | ```
43 |
44 | 3. **Environment Variables Setup**
45 | - Copy the `.env.example` file to `.env.local`:
46 | ```bash
47 | cp .env.example .env.local
48 | ```
49 | - Fill in the environment variables in `.env.local` (see Configuration section below)
50 |
51 | 4. **Start the development server**
52 | ```bash
53 | npm run dev
54 | # or
55 | yarn dev
56 | # or
57 | pnpm dev
58 | ```
59 |
60 | 5. **Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.**
61 |
62 | The homepage includes a setup dashboard with direct links to configure each service.
63 |
64 | ## Configuration
65 |
66 | ### Clerk Setup
67 | 1. Go to [Clerk Dashboard](https://dashboard.clerk.com/)
68 | 2. Create a new application
69 | 3. Go to API Keys
70 | 4. Copy the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`
71 |
72 | ### Supabase Setup
73 | 1. Go to [Supabase Dashboard](https://supabase.com/dashboard)
74 | 2. Create a new project
75 | 3. Go to Authentication → Integrations → Add Clerk (for third-party auth)
76 | 4. Go to Project Settings > API
77 | 5. Copy the `Project URL` as `NEXT_PUBLIC_SUPABASE_URL`
78 | 6. Copy the `anon` public key as `NEXT_PUBLIC_SUPABASE_ANON_KEY`
79 |
80 | ### AI Integration Setup (Optional)
81 | 1. Go to [OpenAI Platform](https://platform.openai.com/) or [Anthropic Console](https://console.anthropic.com/)
82 | 2. Create an API key
83 | 3. Add to your environment variables
84 |
85 | ## Environment Variables
86 |
87 | Create a `.env.local` file in the root directory with the following variables:
88 |
89 | ```env
90 | # Clerk Authentication
91 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
92 | CLERK_SECRET_KEY=your_secret_key
93 |
94 | # Supabase
95 | NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
96 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
97 |
98 | # AI Integration (Optional)
99 | OPENAI_API_KEY=your_openai_api_key
100 | ANTHROPIC_API_KEY=your_anthropic_api_key
101 | ```
102 |
103 | ## Features
104 |
105 | - 🔐 Authentication with Clerk (middleware protection)
106 | - 🗄️ Supabase Database with third-party auth integration
107 | - 🤖 AI Chat Interface with OpenAI/Anthropic support
108 | - 🎨 40+ shadcn/ui components (New York style)
109 | - 🌙 Dark mode with system preference detection
110 | - 🎯 Built-in setup dashboard with service status
111 | - 🚀 App Router with Server Components
112 | - 🔒 Row Level Security examples with Clerk user IDs
113 | - 📱 Responsive design with TailwindCSS v4
114 | - 🎨 Custom fonts (Geist Sans, Geist Mono, Parkinsans)
115 |
116 | ## Project Structure
117 |
118 | ```
119 | codeguide-starter-kit/
120 | ├── src/
121 | │ ├── app/ # Next.js app router pages
122 | │ │ ├── api/chat/ # AI chat API endpoint
123 | │ │ ├── globals.css # Global styles with dark mode
124 | │ │ ├── layout.tsx # Root layout with providers
125 | │ │ └── page.tsx # Hero + setup dashboard
126 | │ ├── components/ # React components
127 | │ │ ├── ui/ # shadcn/ui components (40+)
128 | │ │ ├── chat.tsx # AI chat interface
129 | │ │ ├── theme-provider.tsx # Theme context
130 | │ │ └── theme-toggle.tsx # Dark mode toggle
131 | │ ├── lib/ # Utility functions
132 | │ │ ├── supabase.ts # Supabase client with Clerk auth
133 | │ │ ├── user.ts # User utilities
134 | │ │ ├── utils.ts # General utilities
135 | │ │ └── env-check.ts # Environment validation
136 | │ └── middleware.ts # Clerk route protection
137 | ├── supabase/
138 | │ └── migrations/ # Database migrations with RLS examples
139 | ├── CLAUDE.md # AI coding agent documentation
140 | ├── SUPABASE_CLERK_SETUP.md # Integration setup guide
141 | └── components.json # shadcn/ui configuration
142 | ```
143 |
144 | ## Database Integration
145 |
146 | This starter includes modern Clerk + Supabase integration:
147 |
148 | - **Third-party auth** (not deprecated JWT templates)
149 | - **Row Level Security** policies using `auth.jwt() ->> 'sub'` for Clerk user IDs
150 | - **Example migrations** with various RLS patterns (user-owned, public/private, collaboration)
151 | - **Server-side client** with automatic Clerk token handling
152 |
153 | ## AI Coding Agent Integration
154 |
155 | This starter is optimized for AI coding agents:
156 |
157 | - **`CLAUDE.md`** - Comprehensive project context and patterns
158 | - **Setup guides** with detailed integration steps
159 | - **Example migrations** with RLS policy templates
160 | - **Clear file structure** and naming conventions
161 | - **TypeScript integration** with proper type definitions
162 |
163 | ## Documentation Setup
164 |
165 | To implement the generated documentation from CodeGuide:
166 |
167 | 1. Create a `documentation` folder in the root directory:
168 | ```bash
169 | mkdir documentation
170 | ```
171 |
172 | 2. Place all generated markdown files from CodeGuide in this directory:
173 | ```bash
174 | # Example structure
175 | documentation/
176 | ├── project_requirements_document.md
177 | ├── app_flow_document.md
178 | ├── frontend_guideline_document.md
179 | └── backend_structure_document.md
180 | ```
181 |
182 | 3. These documentation files will be automatically tracked by git and can be used as a reference for your project's features and implementation details.
183 |
184 | ## Contributing
185 |
186 | Contributions are welcome! Please feel free to submit a Pull Request.
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return ;
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return ;
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default";
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | );
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | );
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | );
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | );
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | );
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | };
186 |
--------------------------------------------------------------------------------
/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3 | import { cva } from "class-variance-authority";
4 | import { ChevronDownIcon } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function NavigationMenu({
9 | className,
10 | children,
11 | viewport = true,
12 | ...props
13 | }: React.ComponentProps & {
14 | viewport?: boolean;
15 | }) {
16 | return (
17 |
26 | {children}
27 | {viewport && }
28 |
29 | );
30 | }
31 |
32 | function NavigationMenuList({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | );
46 | }
47 |
48 | function NavigationMenuItem({
49 | className,
50 | ...props
51 | }: React.ComponentProps) {
52 | return (
53 |
58 | );
59 | }
60 |
61 | const navigationMenuTriggerStyle = cva(
62 | "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
63 | );
64 |
65 | function NavigationMenuTrigger({
66 | className,
67 | children,
68 | ...props
69 | }: React.ComponentProps) {
70 | return (
71 |
76 | {children}{" "}
77 |
81 |
82 | );
83 | }
84 |
85 | function NavigationMenuContent({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
99 | );
100 | }
101 |
102 | function NavigationMenuViewport({
103 | className,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
112 |
120 |
121 | );
122 | }
123 |
124 | function NavigationMenuLink({
125 | className,
126 | ...props
127 | }: React.ComponentProps) {
128 | return (
129 |
137 | );
138 | }
139 |
140 | function NavigationMenuIndicator({
141 | className,
142 | ...props
143 | }: React.ComponentProps) {
144 | return (
145 |
153 |
154 |
155 | );
156 | }
157 |
158 | export {
159 | NavigationMenu,
160 | NavigationMenuList,
161 | NavigationMenuItem,
162 | NavigationMenuContent,
163 | NavigationMenuTrigger,
164 | NavigationMenuLink,
165 | NavigationMenuIndicator,
166 | NavigationMenuViewport,
167 | navigationMenuTriggerStyle,
168 | };
169 |
--------------------------------------------------------------------------------
/supabase/migrations/001_example_tables_with_rls.sql:
--------------------------------------------------------------------------------
1 | -- Example migration showing RLS implementation for Clerk + Supabase integration
2 | -- This creates sample tables with proper RLS policies based on Clerk user IDs
3 |
4 | -- Create a posts table as an example
5 | CREATE TABLE IF NOT EXISTS public.posts (
6 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
7 | title TEXT NOT NULL,
8 | content TEXT,
9 | user_id TEXT NOT NULL, -- This will store the Clerk user ID
10 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
11 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
12 | );
13 |
14 | -- Create a comments table as another example
15 | CREATE TABLE IF NOT EXISTS public.comments (
16 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
17 | post_id UUID NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,
18 | content TEXT NOT NULL,
19 | user_id TEXT NOT NULL, -- This will store the Clerk user ID
20 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
21 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
22 | );
23 |
24 | -- Create a profiles table for user-specific data
25 | CREATE TABLE IF NOT EXISTS public.profiles (
26 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
27 | user_id TEXT UNIQUE NOT NULL, -- This will store the Clerk user ID
28 | bio TEXT,
29 | website TEXT,
30 | avatar_url TEXT,
31 | is_public BOOLEAN DEFAULT false,
32 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
33 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
34 | );
35 |
36 | -- Enable Row Level Security on all tables
37 | ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
38 | ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY;
39 | ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
40 |
41 | -- RLS Policies for posts table
42 | -- Users can read all posts (public access)
43 | CREATE POLICY "Anyone can read posts" ON public.posts
44 | FOR SELECT USING (true);
45 |
46 | -- Users can only insert posts as themselves
47 | CREATE POLICY "Users can insert own posts" ON public.posts
48 | FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = user_id);
49 |
50 | -- Users can only update their own posts
51 | CREATE POLICY "Users can update own posts" ON public.posts
52 | FOR UPDATE USING (auth.jwt() ->> 'sub' = user_id);
53 |
54 | -- Users can only delete their own posts
55 | CREATE POLICY "Users can delete own posts" ON public.posts
56 | FOR DELETE USING (auth.jwt() ->> 'sub' = user_id);
57 |
58 | -- RLS Policies for comments table
59 | -- Users can read all comments (public access)
60 | CREATE POLICY "Anyone can read comments" ON public.comments
61 | FOR SELECT USING (true);
62 |
63 | -- Users can only insert comments as themselves
64 | CREATE POLICY "Users can insert own comments" ON public.comments
65 | FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = user_id);
66 |
67 | -- Users can only update their own comments
68 | CREATE POLICY "Users can update own comments" ON public.comments
69 | FOR UPDATE USING (auth.jwt() ->> 'sub' = user_id);
70 |
71 | -- Users can only delete their own comments
72 | CREATE POLICY "Users can delete own comments" ON public.comments
73 | FOR DELETE USING (auth.jwt() ->> 'sub' = user_id);
74 |
75 | -- RLS Policies for profiles table
76 | -- Users can read public profiles or their own profile
77 | CREATE POLICY "Users can read public profiles or own profile" ON public.profiles
78 | FOR SELECT USING (
79 | is_public = true OR auth.jwt() ->> 'sub' = user_id
80 | );
81 |
82 | -- Users can only insert their own profile
83 | CREATE POLICY "Users can insert own profile" ON public.profiles
84 | FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = user_id);
85 |
86 | -- Users can only update their own profile
87 | CREATE POLICY "Users can update own profile" ON public.profiles
88 | FOR UPDATE USING (auth.jwt() ->> 'sub' = user_id);
89 |
90 | -- Users can only delete their own profile
91 | CREATE POLICY "Users can delete own profile" ON public.profiles
92 | FOR DELETE USING (auth.jwt() ->> 'sub' = user_id);
93 |
94 | -- Create indexes for better performance
95 | CREATE INDEX IF NOT EXISTS idx_posts_user_id ON public.posts(user_id);
96 | CREATE INDEX IF NOT EXISTS idx_posts_created_at ON public.posts(created_at DESC);
97 | CREATE INDEX IF NOT EXISTS idx_comments_post_id ON public.comments(post_id);
98 | CREATE INDEX IF NOT EXISTS idx_comments_user_id ON public.comments(user_id);
99 | CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON public.profiles(user_id);
100 |
101 | -- Create updated_at triggers
102 | CREATE OR REPLACE FUNCTION update_updated_at_column()
103 | RETURNS TRIGGER AS $$
104 | BEGIN
105 | NEW.updated_at = NOW();
106 | RETURN NEW;
107 | END;
108 | $$ language 'plpgsql';
109 |
110 | CREATE TRIGGER update_posts_updated_at
111 | BEFORE UPDATE ON public.posts
112 | FOR EACH ROW
113 | EXECUTE FUNCTION update_updated_at_column();
114 |
115 | CREATE TRIGGER update_comments_updated_at
116 | BEFORE UPDATE ON public.comments
117 | FOR EACH ROW
118 | EXECUTE FUNCTION update_updated_at_column();
119 |
120 | CREATE TRIGGER update_profiles_updated_at
121 | BEFORE UPDATE ON public.profiles
122 | FOR EACH ROW
123 | EXECUTE FUNCTION update_updated_at_column();
124 |
125 | -- Example of more complex RLS policy with additional conditions
126 | -- Create a private_notes table that's completely private to each user
127 | CREATE TABLE IF NOT EXISTS public.private_notes (
128 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
129 | title TEXT NOT NULL,
130 | content TEXT,
131 | user_id TEXT NOT NULL, -- This will store the Clerk user ID
132 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
133 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
134 | );
135 |
136 | ALTER TABLE public.private_notes ENABLE ROW LEVEL SECURITY;
137 |
138 | -- Private notes can only be accessed by the owner
139 | CREATE POLICY "Users can only access own private notes" ON public.private_notes
140 | FOR ALL USING (auth.jwt() ->> 'sub' = user_id);
141 |
142 | -- Example of collaborative table with shared access
143 | CREATE TABLE IF NOT EXISTS public.collaborations (
144 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
145 | name TEXT NOT NULL,
146 | description TEXT,
147 | owner_id TEXT NOT NULL, -- Clerk user ID of the owner
148 | collaborators TEXT[] DEFAULT '{}', -- Array of Clerk user IDs
149 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
150 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
151 | );
152 |
153 | ALTER TABLE public.collaborations ENABLE ROW LEVEL SECURITY;
154 |
155 | -- Owners and collaborators can read
156 | CREATE POLICY "Owners and collaborators can read collaborations" ON public.collaborations
157 | FOR SELECT USING (
158 | auth.jwt() ->> 'sub' = owner_id OR
159 | auth.jwt() ->> 'sub' = ANY(collaborators)
160 | );
161 |
162 | -- Only owners can insert
163 | CREATE POLICY "Owners can insert collaborations" ON public.collaborations
164 | FOR INSERT WITH CHECK (auth.jwt() ->> 'sub' = owner_id);
165 |
166 | -- Only owners can update
167 | CREATE POLICY "Owners can update collaborations" ON public.collaborations
168 | FOR UPDATE USING (auth.jwt() ->> 'sub' = owner_id);
169 |
170 | -- Only owners can delete
171 | CREATE POLICY "Owners can delete collaborations" ON public.collaborations
172 | FOR DELETE USING (auth.jwt() ->> 'sub' = owner_id);
173 |
174 | CREATE INDEX IF NOT EXISTS idx_collaborations_owner_id ON public.collaborations(owner_id);
175 | CREATE INDEX IF NOT EXISTS idx_collaborations_collaborators ON public.collaborations USING GIN(collaborators);
176 |
177 | CREATE TRIGGER update_collaborations_updated_at
178 | BEFORE UPDATE ON public.collaborations
179 | FOR EACH ROW
180 | EXECUTE FUNCTION update_updated_at_column();
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import {
5 | ChevronDownIcon,
6 | ChevronLeftIcon,
7 | ChevronRightIcon,
8 | } from "lucide-react";
9 | import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
10 |
11 | import { cn } from "@/lib/utils";
12 | import { Button, buttonVariants } from "@/components/ui/button";
13 |
14 | function Calendar({
15 | className,
16 | classNames,
17 | showOutsideDays = true,
18 | captionLayout = "label",
19 | buttonVariant = "ghost",
20 | formatters,
21 | components,
22 | ...props
23 | }: React.ComponentProps & {
24 | buttonVariant?: React.ComponentProps["variant"];
25 | }) {
26 | const defaultClassNames = getDefaultClassNames();
27 |
28 | return (
29 | svg]:rotate-180`,
34 | String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35 | className,
36 | )}
37 | captionLayout={captionLayout}
38 | formatters={{
39 | formatMonthDropdown: (date) =>
40 | date.toLocaleString("default", { month: "short" }),
41 | ...formatters,
42 | }}
43 | classNames={{
44 | root: cn("w-fit", defaultClassNames.root),
45 | months: cn(
46 | "flex gap-4 flex-col md:flex-row relative",
47 | defaultClassNames.months,
48 | ),
49 | month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
50 | nav: cn(
51 | "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52 | defaultClassNames.nav,
53 | ),
54 | button_previous: cn(
55 | buttonVariants({ variant: buttonVariant }),
56 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
57 | defaultClassNames.button_previous,
58 | ),
59 | button_next: cn(
60 | buttonVariants({ variant: buttonVariant }),
61 | "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
62 | defaultClassNames.button_next,
63 | ),
64 | month_caption: cn(
65 | "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66 | defaultClassNames.month_caption,
67 | ),
68 | dropdowns: cn(
69 | "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70 | defaultClassNames.dropdowns,
71 | ),
72 | dropdown_root: cn(
73 | "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
74 | defaultClassNames.dropdown_root,
75 | ),
76 | dropdown: cn(
77 | "absolute bg-popover inset-0 opacity-0",
78 | defaultClassNames.dropdown,
79 | ),
80 | caption_label: cn(
81 | "select-none font-medium",
82 | captionLayout === "label"
83 | ? "text-sm"
84 | : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85 | defaultClassNames.caption_label,
86 | ),
87 | table: "w-full border-collapse",
88 | weekdays: cn("flex", defaultClassNames.weekdays),
89 | weekday: cn(
90 | "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
91 | defaultClassNames.weekday,
92 | ),
93 | week: cn("flex w-full mt-2", defaultClassNames.week),
94 | week_number_header: cn(
95 | "select-none w-(--cell-size)",
96 | defaultClassNames.week_number_header,
97 | ),
98 | week_number: cn(
99 | "text-[0.8rem] select-none text-muted-foreground",
100 | defaultClassNames.week_number,
101 | ),
102 | day: cn(
103 | "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
104 | defaultClassNames.day,
105 | ),
106 | range_start: cn(
107 | "rounded-l-md bg-accent",
108 | defaultClassNames.range_start,
109 | ),
110 | range_middle: cn("rounded-none", defaultClassNames.range_middle),
111 | range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
112 | today: cn(
113 | "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114 | defaultClassNames.today,
115 | ),
116 | outside: cn(
117 | "text-muted-foreground aria-selected:text-muted-foreground",
118 | defaultClassNames.outside,
119 | ),
120 | disabled: cn(
121 | "text-muted-foreground opacity-50",
122 | defaultClassNames.disabled,
123 | ),
124 | hidden: cn("invisible", defaultClassNames.hidden),
125 | ...classNames,
126 | }}
127 | components={{
128 | Root: ({ className, rootRef, ...props }) => {
129 | return (
130 |
136 | );
137 | },
138 | Chevron: ({ className, orientation, ...props }) => {
139 | if (orientation === "left") {
140 | return (
141 |
142 | );
143 | }
144 |
145 | if (orientation === "right") {
146 | return (
147 |
151 | );
152 | }
153 |
154 | return (
155 |
156 | );
157 | },
158 | DayButton: CalendarDayButton,
159 | WeekNumber: ({ children, ...props }) => {
160 | return (
161 |
162 |
163 | {children}
164 |
165 | |
166 | );
167 | },
168 | ...components,
169 | }}
170 | {...props}
171 | />
172 | );
173 | }
174 |
175 | function CalendarDayButton({
176 | className,
177 | day,
178 | modifiers,
179 | ...props
180 | }: React.ComponentProps) {
181 | const defaultClassNames = getDefaultClassNames();
182 |
183 | const ref = React.useRef(null);
184 | React.useEffect(() => {
185 | if (modifiers.focused) ref.current?.focus();
186 | }, [modifiers.focused]);
187 |
188 | return (
189 | |