├── .env.example ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── astro.config.mjs ├── components.json ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.svg ├── src ├── components │ ├── UserDetails.astro │ ├── auth │ │ ├── SignInButton.astro │ │ ├── SignedIn.astro │ │ ├── SignedOut.astro │ │ └── UserButton.astro │ └── ui │ │ └── button.tsx ├── env.d.ts ├── layouts │ └── Layout.astro ├── lib │ ├── authStore.ts │ ├── clerk.ts │ └── utils.ts ├── middleware.js ├── pages │ ├── dashboard.astro │ └── index.astro └── styles │ └── globals.css ├── tailwind.config.mjs └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_CLERK_PUBLISHABLE_KEY= 2 | CLERK_SECRET_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro + Clerk + Shadcn UI 2 | 3 | ## Techs 4 | 5 | - Astro.build 6 | 7 | - Clerk 8 | 9 | - Shadcn UI 10 | 11 | - Tailwind 12 | 13 | - React 14 | 15 | - Vanilla JS 16 | 17 | - Nanostores 18 | 19 | 20 | ## Steps to reproduce from scratch 21 | 22 | - [ ] [Use shadcn-ui documentation to setup it to Astro](https://ui.shadcn.com/docs/installation/astro) 23 | 24 | - [ ] configure your .env based on .env.example 25 | 26 | - [ ] install clerk deps for the front and backend 27 | 28 | ```shell 29 | pnpm add @clerk/clerk-js @clerk/clerk-sdk-node 30 | ``` 31 | 32 | - [ ] enable Astro SSR 33 | 34 | - [ ] create src/middleware.js file to intercept routes and manage redirections 35 | 36 | ```ts 37 | // src/middleware.js 38 | import clerkClient from '@clerk/clerk-sdk-node' 39 | 40 | const publishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 41 | const secretKey = import.meta.env.CLERK_SECRET_KEY 42 | 43 | const protectedPageUrls = ['/dashboard'] 44 | 45 | export async function onRequest ({ request, redirect }, next) { 46 | const url = new URL(request.url) 47 | if (!protectedPageUrls.some(path => url.pathname.startsWith(path))) { 48 | return next() 49 | } 50 | 51 | const { isSignedIn } = await clerkClient.authenticateRequest({ request, publishableKey, secretKey }) 52 | if (!isSignedIn) { 53 | return redirect('/') 54 | } 55 | 56 | return next() 57 | }; 58 | ``` 59 | 60 | - [ ] use nano stores `pnpm add nanostores` 61 | 62 | - [ ] configure authStore, the Clerk initialization, and .env with Clerk keys 63 | 64 | ```ts 65 | // @/lib/authStore.ts 66 | import type Clerk from '@clerk/clerk-js' 67 | import { atom } from 'nanostores' 68 | 69 | export const auth = atom(null) 70 | ``` 71 | 72 | ```ts 73 | // @/lib/clerk.ts 74 | import Clerk from '@clerk/clerk-js' 75 | import { auth } from './authStore' 76 | 77 | const clerkPublishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 78 | let clerk: Clerk 79 | 80 | export const initializeClerk = () => { 81 | const authNano = auth.get() 82 | if (authNano) return 83 | 84 | clerk = new Clerk(clerkPublishableKey) 85 | clerk 86 | .load() 87 | .then(() => { 88 | auth.set(clerk) 89 | }) 90 | .catch(error => console.error(error)) 91 | } 92 | ``` 93 | 94 | ## The Components 95 | 96 | ### @/components/auth/SignedIn.astro 97 | 98 | When user are signed in, they can see those content 99 | 100 | ```ts 101 | --- 102 | const { classes } = Astro.props 103 | --- 104 | 105 | 108 | 109 | 127 | ``` 128 | 129 | --- 130 | 131 | ### @/components/auth/SignedOut.astro 132 | 133 | When user are signed out, they can see those content 134 | 135 | ```ts 136 | --- 137 | --- 138 | 139 | 142 | 143 | 157 | ``` 158 | 159 | --- 160 | 161 | ### @/components/auth/SignInButton.astro 162 | 163 | When user are logged out, they can sign in with this button 164 | 165 | ```ts 166 | --- 167 | 168 | --- 169 | 170 | 177 | 178 | 195 | ``` 196 | 197 | --- 198 | 199 | ### @/components/auth/UserButton.astro 200 | 201 | When user are logged in, they can you this button to manage account or log out 202 | 203 | ```ts 204 | --- 205 | --- 206 | 207 |
208 | 209 | 221 | ``` 222 | 223 | ## Layouts 224 | 225 | ### @/layouts/Layout.astro 226 | 227 | ```ts 228 | --- 229 | import SignedIn from '@/components/auth/SignedIn.astro'; 230 | import UserButton from '@/components/auth/UserButton.astro'; 231 | import '@/styles/globals.css' 232 | --- 233 | 234 | 235 | 236 | 237 | 238 | 239 | Astro + Clerk 240 | 241 | 242 |
245 | 246 | Astro + Clerk 247 | 248 |
249 | 250 | 251 | 252 | 253 |
254 |
255 | 256 |
257 | 313 | 314 | 315 | 316 | 320 | ``` 321 | 322 | 323 | ## Pages 324 | 325 | ### @/pages/dashboard.astro 326 | 327 | ```ts 328 | --- 329 | import Layout from '@/layouts/Layout.astro' 330 | import UserDetails from '@/components/UserDetails.astro' 331 | 332 | import { createClerkClient } from '@clerk/clerk-sdk-node' 333 | const publishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 334 | const secretKey = import.meta.env.CLERK_SECRET_KEY 335 | 336 | const request = Astro.request 337 | const clerk = createClerkClient({ publishableKey, secretKey }) 338 | const { toAuth } = await clerk.authenticateRequest({ request, publishableKey, secretKey }) 339 | const auth = toAuth() 340 | const user = await clerk.users.getUser(auth!.userId!) 341 | --- 342 | 343 | 344 |
345 |
346 | 347 |
348 |
349 |
350 | 351 | 365 | ``` 366 | 367 | --- 368 | 369 | ### @/pages/index.astro 370 | 371 | ```ts 372 | --- 373 | import Layout from '@/layouts/Layout.astro' 374 | 375 | import SignedIn from '@/components/auth/SignedIn.astro' 376 | import SignedOut from '@/components/auth/SignedOut.astro' 377 | import SignInButton from '@/components/auth/SignInButton.astro' 378 | 379 | import { buttonVariants } from "@/components/ui/button" 380 | --- 381 | 382 | 383 |
384 |
385 |

386 | Auth starts here. 387 |

388 |

389 | A simple and powerful Astro template featuring authentication and 390 | user management powered by Clerk. 391 |

392 |
393 | 394 | 395 | 396 | 397 | 398 | 402 | View Demo 403 | 404 | 405 |
406 |
407 |
408 |
409 | ``` 410 | 411 | ## Inspiration 412 | 413 | https://github.com/vin-e/clerk-astro-demo -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import react from '@astrojs/react' 3 | import tailwind from '@astrojs/tailwind' 4 | 5 | export default defineConfig({ 6 | output: 'server', 7 | integrations: [ 8 | react(), 9 | tailwind({ 10 | applyBaseStyles: false, 11 | }) 12 | ] 13 | }) -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.mjs", 8 | "css": "./src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-clerk", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.3.4", 14 | "@astrojs/react": "^3.0.8", 15 | "@astrojs/tailwind": "^5.0.4", 16 | "@clerk/clerk-js": "^4.68.1", 17 | "@clerk/clerk-sdk-node": "^4.13.4", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@types/react": "^18.2.45", 20 | "@types/react-dom": "^18.2.18", 21 | "astro": "^4.0.7", 22 | "class-variance-authority": "^0.7.0", 23 | "clsx": "^2.0.0", 24 | "lucide-react": "^0.300.0", 25 | "nanostores": "^0.9.5", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "tailwind-merge": "^2.2.0", 29 | "tailwindcss": "^3.4.0", 30 | "tailwindcss-animate": "^1.0.7", 31 | "typescript": "^5.3.3" 32 | } 33 | } -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/UserDetails.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { user } = Astro.props 3 | --- 4 | 5 |
9 |
10 |

User

11 |
12 |
13 |
14 |
15 |
User ID
16 |
17 | {user.id} 18 |
19 |
20 | { 21 | user.firstName 22 | ? 23 |
24 |
First Name
25 |
26 | {user.firstName} 27 |
28 |
29 | 30 | : null 31 | } 32 | { 33 | user.lastName 34 | ? 35 |
36 |
Last Name
37 |
38 | {user.lastName} 39 |
40 |
41 | : null 42 | } 43 |
44 |
Email addresses
45 |
46 | { 47 | user.emailAddresses.map( 48 | (email: any) => 49 |
50 | {email.emailAddress} 51 | { 52 | user.primaryEmailAddressId === email.id && 53 | 54 | Primary 55 | 56 | } 57 |
58 | ) 59 | } 60 |
61 |
62 | { 63 | user.imageUrl 64 | ? 65 |
66 |
Profile Image
67 |
68 | 72 |
73 |
74 | : null 75 | } 76 |
77 |
78 |
-------------------------------------------------------------------------------- /src/components/auth/SignInButton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Button } from "../ui/button"; 3 | const props = Astro.props 4 | --- 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/auth/SignedIn.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { classes } = Astro.props 3 | --- 4 | 5 |
6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /src/components/auth/SignedOut.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/auth/UserButton.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SignedIn from '@/components/auth/SignedIn.astro'; 3 | import UserButton from '@/components/auth/UserButton.astro'; 4 | import '@/styles/globals.css' 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | 12 | Astro + Clerk 13 | 14 | 15 |
18 | 19 | Astro + Clerk 20 | 21 |
22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 |
30 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/lib/authStore.ts: -------------------------------------------------------------------------------- 1 | import type Clerk from '@clerk/clerk-js' 2 | import { atom } from 'nanostores' 3 | 4 | export const auth = atom(null) -------------------------------------------------------------------------------- /src/lib/clerk.ts: -------------------------------------------------------------------------------- 1 | import Clerk from '@clerk/clerk-js' 2 | import { auth } from './authStore' 3 | 4 | const clerkPublishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 5 | let clerk: Clerk 6 | 7 | export const initializeClerk = () => { 8 | const authNano = auth.get() 9 | if (authNano) return 10 | 11 | clerk = new Clerk(clerkPublishableKey) 12 | clerk 13 | .load() 14 | .then(() => { 15 | auth.set(clerk) 16 | }) 17 | .catch(error => console.error(error)) 18 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | import clerkClient from '@clerk/clerk-sdk-node' 2 | 3 | const publishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 4 | const secretKey = import.meta.env.CLERK_SECRET_KEY 5 | 6 | const protectedPageUrls = ['/dashboard'] 7 | 8 | export async function onRequest ({ request, redirect }, next) { 9 | const url = new URL(request.url) 10 | if (!protectedPageUrls.some(path => url.pathname.startsWith(path))) { 11 | return next() 12 | } 13 | 14 | const { isSignedIn } = await clerkClient.authenticateRequest({ request, publishableKey, secretKey }) 15 | if (!isSignedIn) { 16 | return redirect('/') 17 | } 18 | 19 | // return a Response or the result of calling `next()` 20 | return next() 21 | }; -------------------------------------------------------------------------------- /src/pages/dashboard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '@/layouts/Layout.astro' 3 | import UserDetails from '@/components/UserDetails.astro' 4 | 5 | import { createClerkClient } from '@clerk/clerk-sdk-node' 6 | const publishableKey = import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY 7 | const secretKey = import.meta.env.CLERK_SECRET_KEY 8 | 9 | const request = Astro.request 10 | const clerk = createClerkClient({ publishableKey, secretKey }) 11 | const { toAuth } = await clerk.authenticateRequest({ request, publishableKey, secretKey }) 12 | const auth = toAuth() 13 | const user = await clerk.users.getUser(auth!.userId!) 14 | --- 15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '@/layouts/Layout.astro' 3 | 4 | import SignedIn from '@/components/auth/SignedIn.astro' 5 | import SignedOut from '@/components/auth/SignedOut.astro' 6 | import SignInButton from '@/components/auth/SignInButton.astro' 7 | 8 | import { buttonVariants } from "@/components/ui/button" 9 | --- 10 | 11 | 12 |
13 |
14 |

15 | Auth starts here. 16 |

17 |

18 | A simple and powerful Astro template featuring authentication and 19 | user management powered by Clerk. 20 |

21 |
22 | 23 | 24 | Sign In 25 | 26 | 27 | 28 | 29 | 33 | View Demo 34 | 35 | 36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | darkMode: ["class"], 4 | content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], 5 | prefix: "", 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: "2rem", 10 | screens: { 11 | "2xl": "1400px", 12 | }, 13 | }, 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border))", 17 | input: "hsl(var(--input))", 18 | ring: "hsl(var(--ring))", 19 | background: "hsl(var(--background))", 20 | foreground: "hsl(var(--foreground))", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary))", 23 | foreground: "hsl(var(--primary-foreground))", 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary))", 27 | foreground: "hsl(var(--secondary-foreground))", 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive))", 31 | foreground: "hsl(var(--destructive-foreground))", 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted))", 35 | foreground: "hsl(var(--muted-foreground))", 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent))", 39 | foreground: "hsl(var(--accent-foreground))", 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover))", 43 | foreground: "hsl(var(--popover-foreground))", 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)", 54 | }, 55 | keyframes: { 56 | "accordion-down": { 57 | from: { height: "0" }, 58 | to: { height: "var(--radix-accordion-content-height)" }, 59 | }, 60 | "accordion-up": { 61 | from: { height: "var(--radix-accordion-content-height)" }, 62 | to: { height: "0" }, 63 | }, 64 | }, 65 | animation: { 66 | "accordion-down": "accordion-down 0.2s ease-out", 67 | "accordion-up": "accordion-up 0.2s ease-out", 68 | }, 69 | }, 70 | }, 71 | plugins: [require("tailwindcss-animate")], 72 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["src/*"] 9 | } 10 | } 11 | } --------------------------------------------------------------------------------