Public: anyone can read this; the crew can change it.

schedule functions #24

opened this view on origin

Check · Review · Land · Live — It conflicts with its base. Next: builder, it conflicts with its base.

Conflicts with main

function-scheduler → main 3 commits

  • Can't merge. Conflicts in 1 file: src/components/FloatingNavigation.tsx. Merge main into the branch and push again.

    Check faileddeploy e58c626

A guest

This pull comes from a guest: its preview runs without the app's variables or a forge token until a person who can write here approves it.

This pull cannot land as it is, whatever its review says, because it changes guarded paths: package.json (release machinery).

Review

Sign in to review this change.
77 files changed +2334 −10620
.biomeignore added +5
@@ -0,0 +1,5 @@
1 *.css
2 **/*.css
3 .claude/
4 src/app/globals.css
5 src/styles/
.eslintrc.json deleted −7
@@ -1,7 +0,0 @@
1 {
2 "extends": ["next/core-web-vitals", "next/typescript"],
3 "rules": {
4 "@typescript-eslint/no-unused-vars": "off",
5 "@typescript-eslint/no-explicit-any": "off"
6 }
7 }
README.md +222 −20
@@ -1,36 +1,238 @@
1 This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1 # trevato.dev
2
3 Personal portfolio and interactive playground built with Next.js, featuring code execution, 3D visualization, and modern web technologies.
4
5 ## Features
6
7 ### Portfolio
8 - **Hero** - Animated typing introduction with call-to-action
9 - **About** - Personal bio with social links
10 - **Work** - Showcase of websites built with interactive iframe previews
11 - **Projects** - GitHub repository integration with README rendering
12 - **Booking** - Cal.com integration for $500 consultations
13
14 ### Interactive Playground
15 - **Function Creator** - Full-featured code editor with TypeScript/Python execution via Vercel Sandbox
16 - Environment variable support
17 - Secure isolated execution
18 - **Scheduling system** with cron-based execution (runs via Vercel Cron every minute)
19 - Execution history tracking
20 - Supabase authentication required
21 - **3D Viewer** - Three.js object visualization
22 - **Color Picker** - Interactive color selection tool
23
24 ### Visual Experience
25 - Space-themed background with shooting stars and particles
26 - Smooth animations powered by Framer Motion
27 - Responsive design (mobile-first)
28 - Neon/glow effects with blue color scheme
29
30 ## Tech Stack
31
32 - **Framework**: Next.js 14 (App Router)
33 - **Runtime**: Bun
34 - **Language**: TypeScript (strict mode)
35 - **UI**: React 18, Tailwind CSS, shadcn/ui, Radix UI
36 - **Animation**: Framer Motion
37 - **3D Graphics**: Three.js, React Three Fiber
38 - **Authentication**: Supabase SSR
39 - **Code Execution**: Vercel Sandbox
40 - **Analytics**: Vercel Analytics
41 - **Linting**: Biome.js
42 - **Testing**: Vitest
243
344 ## Getting Started
445
5 First, run the development server:
46 ### Prerequisites
47 - [Bun](https://bun.sh/) installed
48 - Supabase project (for authentication features)
49 - Vercel Sandbox API access (for code execution)
50
51 ### Installation
652
753 ```bash
8 npm run dev
9 # or
10 yarn dev
11 # or
12 pnpm dev
13 # or
14 bun dev
54 # Clone the repository
55 git clone https://github.com/yourusername/trevato.dev.git
56 cd trevato.dev
57
58 # Install dependencies
59 bun install
60
61 # Set up environment variables
62 cp .env.local.example .env.local
63 # Edit .env.local with your credentials
1564 ```
1665
17 Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
66 ### Environment Variables
1867
19 You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
68 Create a `.env.local` file with:
2069
21 This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
70 ```env
71 # Supabase
72 NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
73 NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
2274
23 ## Learn More
75 # GitHub (for projects page)
76 GITHUB_API_TOKEN=your_github_token
2477
25 To learn more about Next.js, take a look at the following resources:
78 # App URL (for cron jobs)
79 NEXT_PUBLIC_APP_URL=http://localhost:3000
2680
27 - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
81 # Cron Secret (for production - generate a random string)
82 # This is used to authenticate Vercel Cron → Supabase Edge Function calls
83 CRON_SECRET=your_random_secret_string
2984
30 You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
85 # Sentry (for error tracking in production)
86 NEXT_PUBLIC_SENTRY_DSN=your_sentry_dsn
87 SENTRY_ORG=your_org
88 SENTRY_PROJECT=your_project
89 SENTRY_AUTH_TOKEN=your_auth_token
90 ```
3191
32 ## Deploy on Vercel
92 ### Database Setup
3393
34 The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
94 The scheduling feature requires database tables and Edge Function deployment:
3595
36 Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
96 ```bash
97 # 1. Apply database migration
98 # Option A: Via Supabase CLI (if running locally)
99 supabase db reset
100
101 # Option B: Via Supabase Dashboard
102 # Copy SQL from supabase/migrations/20251123_create_scheduled_functions.sql
103 # and run it in your Supabase SQL editor
104
105 # 2. Deploy Edge Function
106 supabase functions deploy execute-scheduled
107
108 # 3. Set Edge Function secrets
109 supabase secrets set CRON_SECRET=your_random_secret_string
110 supabase secrets set NEXT_PUBLIC_APP_URL=https://your-app.vercel.app
111 ```
112
113 ### Development
114
115 ```bash
116 # Start development server
117 bun dev
118
119 # Start Supabase locally (optional)
120 supabase start
121
122 # Run tests
123 bun test
124
125 # Run tests with UI
126 bun test:ui
127
128 # Run linter
129 bun run lint
130
131 # Auto-fix linting issues
132 bun run lint:fix
133
134 # Format code
135 bun run format
136 ```
137
138 Open [http://localhost:3000](http://localhost:3000) to see the result.
139
140 ### Building
141
142 ```bash
143 # Build for production
144 bun run build
145
146 # Start production server
147 bun start
148 ```
149
150 ## Project Structure
151
152 ```
153 trevato.dev/
154 ├── src/
155 │ ├── app/ # Next.js App Router pages
156 │ │ ├── api/ # API routes (sandbox execution, scheduling)
157 │ │ ├── auth/ # Authentication pages
158 │ │ ├── playground/ # Interactive playground features
159 │ │ └── projects/ # GitHub projects integration
160 │ ├── components/ # React components
161 │ │ ├── playground/ # Playground-specific components
162 │ │ └── ui/ # shadcn/ui components
163 │ ├── lib/ # Utility functions and mappings
164 │ ├── styles/ # Global styles and markdown CSS
165 │ ├── utils/ # Helper utilities
166 │ │ └── supabase/ # Supabase client/server utilities
167 │ └── __tests__/ # Test files
168 ├── public/ # Static assets
169 │ ├── models/ # 3D models
170 │ ├── projects/ # Project images
171 │ └── resume/ # Resume PDFs
172 ├── supabase/ # Supabase configuration
173 └── biome.json # Biome linter configuration
174 ```
175
176 ## Code Quality
177
178 - **Type Safety**: TypeScript with strict mode enabled
179 - **Linting**: Biome.js with recommended rules
180 - **Testing**: Vitest with @testing-library/react
181 - **Formatting**: Automated via Biome.js
182 - **Git Hooks**: Recommended to set up with lint-staged
183
184 ## Scheduling System
185
186 The Function Creator includes a production-ready scheduling system:
187
188 ### How It Works
189 1. User schedules a function with cron expression (e.g., `0 * * * *` for hourly)
190 2. Function is stored in Supabase with calculated next run time
191 3. Vercel Cron runs `/api/cron/execute-scheduled` every minute
192 4. Vercel endpoint triggers Supabase Edge Function with secret
193 5. Edge Function (with elevated permissions) fetches due functions and executes them via Vercel Sandbox
194 6. Execution history is logged and next run time is updated
195
196 ### Features
197 - Standard cron expression support (minute hour day month weekday)
198 - Per-function environment variables
199 - Execution history with output/error logging
200 - Enable/disable individual functions
201 - Automatic next-run calculation using cron-parser
202 - RLS policies ensure users only see their own functions
203
204 ### Database Tables
205 - `scheduled_functions` - Stores function code, cron schedules, and metadata
206 - `execution_history` - Logs each execution with status, output, and duration
207
208 ## Known Limitations
209
210 - CSS linting disabled for Tailwind directive compatibility
211 - 3 CSS parsing warnings (known Biome limitation with Tailwind)
212 - Vercel Cron limited to 1-minute intervals (finest granularity)
213
214 ## Performance
215
216 - Canvas animations optimized for smooth rendering
217 - Code splitting via Next.js defaults
218 - Image optimization with next/image
219 - Vercel Sandbox execution timeout: 5 minutes
220
221 ## Security
222
223 - Authentication via Supabase
224 - Code execution in isolated Vercel Sandbox environment
225 - Middleware protecting sensitive routes
226 - Environment variables properly configured
227
228 ## Contributing
229
230 This is a personal portfolio project. Feel free to fork and adapt for your own use.
231
232 ## License
233
234 MIT License - feel free to use this code for your own portfolio.
235
236 ---
237
238 Built with attention to detail by [trevato](https://trevato.dev)
biome.json added +89
@@ -0,0 +1,89 @@
1 {
2 "$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
3 "vcs": {
4 "enabled": true,
5 "clientKind": "git",
6 "useIgnoreFile": true,
7 "defaultBranch": "main"
8 },
9 "files": {
10 "ignoreUnknown": true
11 },
12 "formatter": {
13 "enabled": true,
14 "formatWithErrors": true,
15 "indentStyle": "space",
16 "indentWidth": 2,
17 "lineWidth": 100,
18 "lineEnding": "lf"
19 },
20 "linter": {
21 "enabled": true,
22 "rules": {
23 "recommended": true,
24 "a11y": {
25 "recommended": true,
26 "noLabelWithoutControl": "warn",
27 "noSvgWithoutTitle": "warn",
28 "useButtonType": "warn"
29 },
30 "correctness": {
31 "recommended": true,
32 "noUnusedVariables": "warn",
33 "noUnusedFunctionParameters": "warn",
34 "useExhaustiveDependencies": "warn"
35 },
36 "style": {
37 "recommended": true,
38 "noNonNullAssertion": "warn"
39 },
40 "suspicious": {
41 "recommended": true,
42 "noExplicitAny": "off",
43 "noArrayIndexKey": "warn",
44 "useIterableCallbackReturn": "warn"
45 },
46 "complexity": {
47 "recommended": true
48 },
49 "performance": {
50 "recommended": true
51 },
52 "security": {
53 "recommended": true
54 }
55 }
56 },
57 "javascript": {
58 "formatter": {
59 "quoteStyle": "double",
60 "jsxQuoteStyle": "double",
61 "semicolons": "asNeeded",
62 "trailingCommas": "es5"
63 }
64 },
65 "css": {
66 "parser": {
67 "allowWrongLineComments": true,
68 "cssModules": false
69 },
70 "formatter": {
71 "enabled": false
72 },
73 "linter": {
74 "enabled": false
75 }
76 },
77 "overrides": [
78 {
79 "includes": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
80 "linter": {
81 "rules": {
82 "suspicious": {
83 "noExplicitAny": "off"
84 }
85 }
86 }
87 }
88 ]
89 }
bun.lockb
Binary file not shown.
components.json +1 −1
@@ -17,4 +17,4 @@
1717 "lib": "@/lib",
1818 "hooks": "@/hooks"
1919 }
20 }↵
20 }
middleware.ts +3 −3
@@ -1,5 +1,5 @@
1 import { type NextRequest } from 'next/server'
2 import { updateSession } from '@/utils/supabase/middleware'
1 import type { NextRequest } from "next/server"
2 import { updateSession } from "@/utils/supabase/middleware"
33
44 export async function middleware(request: NextRequest) {
55 return await updateSession(request)
@@ -14,6 +14,6 @@ export const config = {
1414 * - favicon.ico (favicon file)
1515 * Feel free to modify this pattern to include more paths.
1616 */
17 '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
17 "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
1818 ],
1919 }
next.config.mjs +32 −3
@@ -1,4 +1,33 @@
1 /** @type {import('next').NextConfig} */
2 const nextConfig = {};
1 import { withSentryConfig } from "@sentry/nextjs"
32
4 export default nextConfig;
3 /** @type {import('next').NextConfig} */
4 const nextConfig = {
5 eslint: {
6 // Disable ESLint during builds since we're using Biome
7 ignoreDuringBuilds: true,
8 },
9 }
10
11 export default withSentryConfig(nextConfig, {
12 // Sentry configuration options
13 org: process.env.SENTRY_ORG,
14 project: process.env.SENTRY_PROJECT,
15
16 // Auth token for uploading source maps
17 authToken: process.env.SENTRY_AUTH_TOKEN,
18
19 // Only upload source maps in production
20 silent: process.env.NODE_ENV !== "production",
21
22 // Upload source maps during build
23 widenClientFileUpload: true,
24
25 // Transpile SDK for compatibility
26 transpileClientSDK: true,
27
28 // Hide source maps from generated client bundles
29 hideSourceMaps: true,
30
31 // Disable Sentry during development
32 disableLogger: process.env.NODE_ENV !== "production",
33 })
package-lock.json deleted −9658

Diff not shown — 9,658 changed lines (limit 5,000). −9658

View file
package.json +21 −4
@@ -6,7 +6,14 @@
66 "dev": "next dev",
77 "build": "next build",
88 "start": "next start",
9 "lint": "next lint"
9 "lint": "biome check .",
10 "lint:fix": "biome check --write .",
11 "format": "biome format --write .",
12 "format:check": "biome format .",
13 "check": "biome check .",
14 "test": "vitest",
15 "test:ui": "vitest --ui",
16 "test:coverage": "vitest --coverage"
1017 },
1118 "dependencies": {
1219 "@calcom/embed-react": "^1.5.0",
@@ -20,16 +27,20 @@
2027 "@radix-ui/react-tooltip": "^1.1.2",
2128 "@react-three/drei": "^9.112.0",
2229 "@react-three/fiber": "^8.17.7",
30 "@sentry/nextjs": "^10.26.0",
2331 "@supabase/ssr": "^0.5.1",
2432 "@uiw/react-textarea-code-editor": "^3.0.2",
2533 "@vercel/analytics": "^1.5.0",
2634 "@vercel/sandbox": "^1.0.2",
2735 "class-variance-authority": "^0.7.0",
2836 "clsx": "^2.1.1",
37 "cron-parser": "^5.4.0",
2938 "framer-motion": "^11.5.4",
3039 "lucide-react": "^0.439.0",
3140 "next": "14.2.9",
3241 "octokit": "^4.0.2",
42 "pino": "^10.1.0",
43 "pino-pretty": "^13.1.2",
3344 "react": "^18",
3445 "react-dom": "^18",
3546 "react-icons": "^5.3.0",
@@ -44,14 +55,20 @@
4455 "three": "^0.168.0"
4556 },
4657 "devDependencies": {
58 "@biomejs/biome": "^2.3.7",
59 "@testing-library/jest-dom": "^6.9.1",
60 "@testing-library/react": "^16.3.0",
61 "@testing-library/user-event": "^14.6.1",
4762 "@types/node": "^20",
4863 "@types/react": "^18",
4964 "@types/react-dom": "^18",
5065 "@types/react-syntax-highlighter": "^15.5.13",
51 "eslint": "^8",
52 "eslint-config-next": "14.2.9",
66 "@vitejs/plugin-react": "^5.1.1",
67 "@vitest/ui": "^4.0.13",
68 "jsdom": "^27.2.0",
5369 "postcss": "^8",
5470 "tailwindcss": "^3.4.1",
55 "typescript": "^5"
71 "typescript": "^5",
72 "vitest": "^4.0.13"
5673 }
5774 }
postcss.config.mjs +2 −2
@@ -3,6 +3,6 @@ const config = {
33 plugins: {
44 tailwindcss: {},
55 },
6 };
6 }
77
8 export default config;
8 export default config
sentry.client.config.ts added +49
@@ -0,0 +1,49 @@
1 import * as Sentry from "@sentry/nextjs"
2
3 Sentry.init({
4 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
5
6 // Performance Monitoring
7 tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0, // 10% in prod, 100% in dev
8
9 // Session Replay
10 replaysSessionSampleRate: 0.1, // 10% of sessions
11 replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors
12
13 // Don't send errors in development
14 enabled: process.env.NODE_ENV === "production",
15
16 // Environment
17 environment: process.env.NODE_ENV,
18
19 // Integrations
20 integrations: [
21 Sentry.replayIntegration({
22 maskAllText: false,
23 blockAllMedia: false,
24 }),
25 ],
26
27 // Filter out noise
28 ignoreErrors: [
29 // Browser extensions
30 "top.GLOBALS",
31 // Random plugins/extensions
32 "originalCreateNotification",
33 "canvas.contentDocument",
34 "MyApp_RemoveAllHighlights",
35 // Network errors
36 "NetworkError",
37 "Failed to fetch",
38 ],
39
40 // Attach user context
41 beforeSend(event, hint) {
42 // Scrub sensitive data
43 if (event.request) {
44 delete event.request.cookies
45 }
46
47 return event
48 },
49 })
sentry.edge.config.ts added +17
@@ -0,0 +1,17 @@
1 import * as Sentry from "@sentry/nextjs"
2
3 Sentry.init({
4 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
5
6 // Performance Monitoring
7 tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
8
9 // Don't send errors in development
10 enabled: process.env.NODE_ENV === "production",
11
12 // Environment
13 environment: process.env.NODE_ENV,
14
15 // Filter out noise
16 ignoreErrors: ["NetworkError", "Failed to fetch"],
17 })
sentry.server.config.ts added +31
@@ -0,0 +1,31 @@
1 import * as Sentry from "@sentry/nextjs"
2
3 Sentry.init({
4 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
5
6 // Performance Monitoring
7 tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
8
9 // Don't send errors in development
10 enabled: process.env.NODE_ENV === "production",
11
12 // Environment
13 environment: process.env.NODE_ENV,
14
15 // Filter out noise
16 ignoreErrors: ["NetworkError", "Failed to fetch", "ECONNREFUSED"],
17
18 // Attach context
19 beforeSend(event) {
20 // Scrub sensitive data from server events
21 if (event.request) {
22 delete event.request.cookies
23 if (event.request.headers) {
24 delete event.request.headers["authorization"]
25 delete event.request.headers["cookie"]
26 }
27 }
28
29 return event
30 },
31 })
src/__tests__/setup.test.ts added +11
@@ -0,0 +1,11 @@
1 import { describe, expect, it } from "vitest"
2
3 describe("Vitest Setup", () => {
4 it("should run basic test", () => {
5 expect(true).toBe(true)
6 })
7
8 it("should handle arithmetic", () => {
9 expect(2 + 2).toBe(4)
10 })
11 })
src/app/api/cron/execute-scheduled/route.ts added +80
@@ -0,0 +1,80 @@
1 import { type NextRequest, NextResponse } from "next/server"
2 import { createRouteLogger } from "@/lib/logger"
3
4 export const runtime = "nodejs"
5
6 const log = createRouteLogger("/api/cron/execute-scheduled")
7
8 // This endpoint is called by Vercel Cron every minute
9 // It triggers the Supabase Edge Function to do the actual work
10 export async function GET(request: NextRequest) {
11 const startTime = Date.now()
12
13 try {
14 // Verify this is a legitimate cron request from Vercel
15 const authHeader = request.headers.get("authorization")
16 if (process.env.NODE_ENV === "production") {
17 if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
18 log.warn("Unauthorized cron trigger attempt")
19 return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
20 }
21 }
22
23 log.info("Cron triggered, calling Edge Function")
24
25 // Call Supabase Edge Function to execute scheduled functions
26 const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
27 const cronSecret = process.env.CRON_SECRET
28
29 if (!supabaseUrl || !cronSecret) {
30 log.error("Missing required environment variables")
31 return NextResponse.json(
32 { error: "Missing required environment variables" },
33 { status: 500 }
34 )
35 }
36
37 const response = await fetch(`${supabaseUrl}/functions/v1/execute-scheduled`, {
38 method: "POST",
39 headers: {
40 Authorization: `Bearer ${cronSecret}`,
41 "Content-Type": "application/json",
42 },
43 })
44
45 if (!response.ok) {
46 const errorText = await response.text()
47 throw new Error(`Edge function call failed: ${response.status} ${errorText}`)
48 }
49
50 const result = await response.json()
51 const duration = Date.now() - startTime
52
53 log.info(
54 {
55 duration,
56 processed: result.processed,
57 successful: result.successful,
58 failed: result.failed,
59 },
60 "Cron execution completed"
61 )
62
63 return NextResponse.json({
64 success: true,
65 message: "Triggered edge function successfully",
66 result,
67 })
68 } catch (error) {
69 const duration = Date.now() - startTime
70 log.error({ err: error, duration }, "Cron trigger error")
71
72 return NextResponse.json(
73 {
74 error: "Failed to trigger scheduled execution",
75 details: error instanceof Error ? error.message : "Unknown error",
76 },
77 { status: 500 }
78 )
79 }
80 }
src/app/api/sandbox/execute/route.ts +57 −44
@@ -1,46 +1,51 @@
1 import { NextRequest, NextResponse } from 'next/server';
2 import { Sandbox } from '@vercel/sandbox';
3 import { createClient } from '@/utils/supabase/server';
1 import { Sandbox } from "@vercel/sandbox"
2 import { type NextRequest, NextResponse } from "next/server"
3 import { createClient } from "@/utils/supabase/server"
4 import { createRouteLogger } from "@/lib/logger"
45
5 export const runtime = 'nodejs';
6 export const maxDuration = 300; // 5 minutes
6 export const runtime = "nodejs"
7 export const maxDuration = 300 // 5 minutes
8
9 const log = createRouteLogger("/api/sandbox/execute")
710
811 export async function POST(request: NextRequest) {
12 const startTime = Date.now()
13
914 try {
1015 // Check authentication
11 const supabase = await createClient();
12 const { data: { user }, error: authError } = await supabase.auth.getUser();
16 const supabase = await createClient()
17 const {
18 data: { user },
19 error: authError,
20 } = await supabase.auth.getUser()
1321
1422 if (authError || !user) {
15 return NextResponse.json(
16 { error: 'Authentication required' },
17 { status: 401 }
18 );
23 log.warn("Unauthorized sandbox execution attempt")
24 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
1925 }
2026
21 const { code, language, envVars = {} } = await request.json();
27 const { code, language, envVars = {} } = await request.json()
28
29 log.info({ userId: user.id, language, hasEnvVars: Object.keys(envVars).length > 0 }, "Executing sandbox code")
2230
2331 if (!code || !language) {
24 return NextResponse.json(
25 { error: 'Code and language are required' },
26 { status: 400 }
27 );
32 return NextResponse.json({ error: "Code and language are required" }, { status: 400 })
2833 }
2934
3035 // Create a new sandbox with the appropriate runtime
31 const runtime = language === 'python' ? 'python3.13' : 'node22';
36 const runtime = language === "python" ? "python3.13" : "node22"
3237 const sandbox = await Sandbox.create({
3338 runtime,
34 });
39 })
3540
3641 try {
37 let output = '';
38 let errorOutput = '';
42 let output = ""
43 let errorOutput = ""
3944
4045 // Determine the file extension and execution command based on language
41 const fileName = language === 'python' ? 'main.py' : 'main.ts';
42 const execCmd = language === 'python' ? 'python3' : 'npx';
43 const execArgs = language === 'python' ? ['main.py'] : ['tsx', 'main.ts'];
46 const fileName = language === "python" ? "main.py" : "main.ts"
47 const execCmd = language === "python" ? "python3" : "npx"
48 const execArgs = language === "python" ? ["main.py"] : ["tsx", "main.ts"]
4449
4550 // Write the code to a file in the sandbox
4651 await sandbox.writeFiles([
@@ -48,24 +53,24 @@ export async function POST(request: NextRequest) {
4853 path: fileName,
4954 content: Buffer.from(code),
5055 },
51 ]);
56 ])
5257
5358 // Install tsx for TypeScript execution if needed
54 if (language === 'typescript') {
59 if (language === "typescript") {
5560 const installResult = await sandbox.runCommand({
56 cmd: 'npm',
57 args: ['install', '-g', 'tsx'],
58 });
61 cmd: "npm",
62 args: ["install", "-g", "tsx"],
63 })
5964
6065 // Wait for install to complete
61 const installExitCode = installResult.exitCode;
66 const installExitCode = installResult.exitCode
6267 if (installExitCode !== 0) {
63 const installError = await installResult.stderr();
68 const installError = await installResult.stderr()
6469 return NextResponse.json({
6570 success: false,
6671 error: `Failed to install tsx: ${installError}`,
6772 exitCode: installExitCode,
68 });
73 })
6974 }
7075 }
7176
@@ -74,34 +79,42 @@ export async function POST(request: NextRequest) {
7479 cmd: execCmd,
7580 args: execArgs,
7681 env: envVars,
77 });
82 })
7883
7984 // Get the output
80 const exitCode = result.exitCode;
81 output = await result.stdout();
82 errorOutput = await result.stderr();
85 const exitCode = result.exitCode
86 output = await result.stdout()
87 errorOutput = await result.stderr()
88
89 const duration = Date.now() - startTime
90
91 if (exitCode === 0) {
92 log.info({ duration, sandboxId: sandbox.sandboxId }, "Sandbox execution successful")
93 } else {
94 log.warn({ duration, exitCode, sandboxId: sandbox.sandboxId }, "Sandbox execution failed")
95 }
8396
8497 return NextResponse.json({
8598 success: exitCode === 0,
86 output: output || 'Code executed successfully',
99 output: output || "Code executed successfully",
87100 error: errorOutput || null,
88101 exitCode,
89102 sandboxId: sandbox.sandboxId,
90 });
91
103 })
92104 } finally {
93105 // Clean up the sandbox
94 await sandbox.stop();
106 await sandbox.stop()
95107 }
96
97108 } catch (error) {
98 console.error('Sandbox execution error:', error);
109 const duration = Date.now() - startTime
110 log.error({ err: error, duration }, "Sandbox execution error")
111
99112 return NextResponse.json(
100113 {
101 error: 'Failed to execute code',
102 details: error instanceof Error ? error.message : 'Unknown error'
114 error: "Failed to execute code",
115 details: error instanceof Error ? error.message : "Unknown error",
103116 },
104117 { status: 500 }
105 );
118 )
106119 }
107120 }
src/app/api/sandbox/schedule/route.ts +226 −49
@@ -1,83 +1,260 @@
1 import { NextRequest, NextResponse } from 'next/server';
2 import { createClient } from '@/utils/supabase/server';
1 import { type NextRequest, NextResponse } from "next/server"
2 import cronParser from "cron-parser"
3 import { createClient } from "@/utils/supabase/server"
34
4 export const runtime = 'nodejs';
5 export const runtime = "nodejs"
56
6 // This is a simplified version - for production, you'd want to use Vercel Cron Jobs
7 // or integrate with a service like Vercel Cron, Upstash QStash, or similar
7 interface ScheduledFunction {
8 id: string
9 name: string
10 code: string
11 language: string
12 cron_expression: string
13 env_vars: Record<string, string>
14 enabled: boolean
15 created_at: string
16 next_run_at: string | null
17 last_run_at: string | null
18 execution_count: number
19 }
20
21 // Schedule a new function
822 export async function POST(request: NextRequest) {
923 try {
10 // Check authentication
11 const supabase = await createClient();
12 const { data: { user }, error: authError } = await supabase.auth.getUser();
24 const supabase = await createClient()
25 const {
26 data: { user },
27 error: authError,
28 } = await supabase.auth.getUser()
1329
1430 if (authError || !user) {
15 return NextResponse.json(
16 { error: 'Authentication required' },
17 { status: 401 }
18 );
31 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
1932 }
2033
21 const { code, language, cronExpression } = await request.json();
34 const { name, code, language, cronExpression, envVars } = await request.json()
2235
23 if (!code || !language || !cronExpression) {
36 if (!name || !code || !language || !cronExpression) {
2437 return NextResponse.json(
25 { error: 'Code, language, and cron expression are required' },
38 { error: "Name, code, language, and cron expression are required" },
2639 { status: 400 }
27 );
40 )
2841 }
2942
30 // Validate cron expression (basic validation)
31 const cronRegex = /^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/;
32
33 if (!cronRegex.test(cronExpression)) {
43 // Validate cron expression
44 let nextRun: Date
45 try {
46 const interval = cronParser.parse(cronExpression)
47 nextRun = interval.next().toDate()
48 } catch (_error) {
3449 return NextResponse.json(
35 { error: 'Invalid cron expression format' },
50 { error: "Invalid cron expression format. Use standard cron format: minute hour day month weekday" },
3651 { status: 400 }
37 );
52 )
3853 }
3954
40 // In a production environment, you would:
41 // 1. Store the function in a database with the cron schedule
42 // 2. Use Vercel Cron Jobs (vercel.json) or a service like Upstash QStash
43 // 3. Create a separate API endpoint that checks scheduled functions and executes them
55 // Insert into database
56 const { data: scheduledFunction, error: insertError } = await supabase
57 .from("scheduled_functions")
58 .insert({
59 user_id: user.id,
60 name,
61 code,
62 language,
63 cron_expression: cronExpression,
64 env_vars: envVars || {},
65 next_run_at: nextRun.toISOString(),
66 })
67 .select()
68 .single()
69
70 if (insertError) {
71 return NextResponse.json(
72 { error: "Failed to schedule function", details: insertError.message },
73 { status: 500 }
74 )
75 }
4476
45 // For now, we'll return a success message with instructions
4677 return NextResponse.json({
4778 success: true,
48 message: 'Function scheduled successfully',
79 message: "Function scheduled successfully",
4980 scheduled: {
50 code: code.substring(0, 100) + '...', // Truncate for response
51 language,
81 id: scheduledFunction.id,
82 name: scheduledFunction.name,
5283 cronExpression,
53 nextRun: calculateNextRun(cronExpression),
54 note: 'This is a demo implementation. In production, integrate with Vercel Cron Jobs or a scheduling service.',
84 nextRun: nextRun.toISOString(),
85 language,
5586 },
56 });
57
87 })
5888 } catch (error) {
59 console.error('Scheduling error:', error);
6089 return NextResponse.json(
6190 {
62 error: 'Failed to schedule function',
63 details: error instanceof Error ? error.message : 'Unknown error'
91 error: "Failed to schedule function",
92 details: error instanceof Error ? error.message : "Unknown error",
6493 },
6594 { status: 500 }
66 );
95 )
6796 }
6897 }
6998
70 // Helper function to calculate next run time (simplified)
71 function calculateNextRun(cronExpression: string): string {
72 // This is a simplified version - in production, use a library like 'cron-parser'
73 const now = new Date();
74 const nextRun = new Date(now.getTime() + 60 * 60 * 1000); // Add 1 hour as example
75 return nextRun.toISOString();
99 // Get all scheduled functions for the current user
100 export async function GET() {
101 try {
102 const supabase = await createClient()
103 const {
104 data: { user },
105 error: authError,
106 } = await supabase.auth.getUser()
107
108 if (authError || !user) {
109 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
110 }
111
112 const { data: functions, error: fetchError } = await supabase
113 .from("scheduled_functions")
114 .select("*")
115 .eq("user_id", user.id)
116 .order("created_at", { ascending: false })
117
118 if (fetchError) {
119 return NextResponse.json(
120 { error: "Failed to fetch scheduled functions", details: fetchError.message },
121 { status: 500 }
122 )
123 }
124
125 return NextResponse.json({
126 functions: functions || [],
127 })
128 } catch (error) {
129 return NextResponse.json(
130 {
131 error: "Failed to fetch scheduled functions",
132 details: error instanceof Error ? error.message : "Unknown error",
133 },
134 { status: 500 }
135 )
136 }
76137 }
77138
78 export async function GET() {
79 return NextResponse.json({
80 message: 'Scheduled functions endpoint',
81 note: 'Use POST to schedule a new function',
82 });
139 // Update a scheduled function (enable/disable, update code, etc.)
140 export async function PATCH(request: NextRequest) {
141 try {
142 const supabase = await createClient()
143 const {
144 data: { user },
145 error: authError,
146 } = await supabase.auth.getUser()
147
148 if (authError || !user) {
149 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
150 }
151
152 const { id, enabled, name, code, language, cronExpression, envVars } = await request.json()
153
154 if (!id) {
155 return NextResponse.json({ error: "Function ID is required" }, { status: 400 })
156 }
157
158 const updates: Partial<ScheduledFunction> = {}
159
160 if (typeof enabled === "boolean") {
161 updates.enabled = enabled
162 }
163
164 if (name) updates.name = name
165 if (code) updates.code = code
166 if (language) updates.language = language
167 if (envVars) updates.env_vars = envVars
168
169 if (cronExpression) {
170 try {
171 const interval = cronParser.parse(cronExpression)
172 const nextRun = interval.next().toDate()
173 updates.cron_expression = cronExpression
174 updates.next_run_at = nextRun.toISOString()
175 } catch (_error) {
176 return NextResponse.json({ error: "Invalid cron expression format" }, { status: 400 })
177 }
178 }
179
180 const { data: updatedFunction, error: updateError } = await supabase
181 .from("scheduled_functions")
182 .update(updates)
183 .eq("id", id)
184 .eq("user_id", user.id) // Ensure user owns this function
185 .select()
186 .single()
187
188 if (updateError) {
189 return NextResponse.json(
190 { error: "Failed to update function", details: updateError.message },
191 { status: 500 }
192 )
193 }
194
195 if (!updatedFunction) {
196 return NextResponse.json({ error: "Function not found or access denied" }, { status: 404 })
197 }
198
199 return NextResponse.json({
200 success: true,
201 function: updatedFunction,
202 })
203 } catch (error) {
204 return NextResponse.json(
205 {
206 error: "Failed to update function",
207 details: error instanceof Error ? error.message : "Unknown error",
208 },
209 { status: 500 }
210 )
211 }
212 }
213
214 // Delete a scheduled function
215 export async function DELETE(request: NextRequest) {
216 try {
217 const supabase = await createClient()
218 const {
219 data: { user },
220 error: authError,
221 } = await supabase.auth.getUser()
222
223 if (authError || !user) {
224 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
225 }
226
227 const { searchParams } = new URL(request.url)
228 const id = searchParams.get("id")
229
230 if (!id) {
231 return NextResponse.json({ error: "Function ID is required" }, { status: 400 })
232 }
233
234 const { error: deleteError } = await supabase
235 .from("scheduled_functions")
236 .delete()
237 .eq("id", id)
238 .eq("user_id", user.id) // Ensure user owns this function
239
240 if (deleteError) {
241 return NextResponse.json(
242 { error: "Failed to delete function", details: deleteError.message },
243 { status: 500 }
244 )
245 }
246
247 return NextResponse.json({
248 success: true,
249 message: "Function deleted successfully",
250 })
251 } catch (error) {
252 return NextResponse.json(
253 {
254 error: "Failed to delete function",
255 details: error instanceof Error ? error.message : "Unknown error",
256 },
257 { status: 500 }
258 )
259 }
83260 }
src/app/auth/sign-in/page.tsx +39 −43
@@ -1,16 +1,16 @@
1 'use client'
1 "use client"
22
3 import { useState } from 'react'
4 import { useRouter } from 'next/navigation'
5 import { createClient } from '@/utils/supabase/client'
6 import { Card } from '@/components/ui/card'
7 import { Button } from '@/components/ui/button'
8 import { toast } from '@/hooks/use-toast'
9 import { Mail, KeyRound } from 'lucide-react'
3 import { KeyRound, Mail } from "lucide-react"
4 import { useRouter } from "next/navigation"
5 import { useState } from "react"
6 import { Button } from "@/components/ui/button"
7 import { Card } from "@/components/ui/card"
8 import { toast } from "@/hooks/use-toast"
9 import { createClient } from "@/utils/supabase/client"
1010
1111 export default function AuthPage() {
12 const [email, setEmail] = useState('')
13 const [otp, setOtp] = useState('')
12 const [email, setEmail] = useState("")
13 const [otp, setOtp] = useState("")
1414 const [isLoading, setIsLoading] = useState(false)
1515 const [isVerifying, setIsVerifying] = useState(false)
1616 const [emailSent, setEmailSent] = useState(false)
@@ -31,22 +31,22 @@ export default function AuthPage() {
3131
3232 if (error) {
3333 toast({
34 title: 'Error',
34 title: "Error",
3535 description: error.message,
36 variant: 'destructive',
36 variant: "destructive",
3737 })
3838 } else {
3939 setEmailSent(true)
4040 toast({
41 title: 'Check your email',
42 description: 'We sent you a magic link and a 6-digit code',
41 title: "Check your email",
42 description: "We sent you a magic link and a 6-digit code",
4343 })
4444 }
45 } catch (error) {
45 } catch (_error) {
4646 toast({
47 title: 'Error',
48 description: 'Something went wrong',
49 variant: 'destructive',
47 title: "Error",
48 description: "Something went wrong",
49 variant: "destructive",
5050 })
5151 } finally {
5252 setIsLoading(false)
@@ -58,9 +58,9 @@ export default function AuthPage() {
5858
5959 if (otp.length !== 6) {
6060 toast({
61 title: 'Invalid code',
62 description: 'Please enter a 6-digit code',
63 variant: 'destructive',
61 title: "Invalid code",
62 description: "Please enter a 6-digit code",
63 variant: "destructive",
6464 })
6565 return
6666 }
@@ -71,27 +71,27 @@ export default function AuthPage() {
7171 const { error } = await supabase.auth.verifyOtp({
7272 email,
7373 token: otp,
74 type: 'email',
74 type: "email",
7575 })
7676
7777 if (error) {
7878 toast({
79 title: 'Verification failed',
79 title: "Verification failed",
8080 description: error.message,
81 variant: 'destructive',
81 variant: "destructive",
8282 })
8383 } else {
8484 toast({
85 title: 'Success',
86 description: 'You are now signed in',
85 title: "Success",
86 description: "You are now signed in",
8787 })
88 router.push('/playground/function-creator')
88 router.push("/playground/function-creator")
8989 }
90 } catch (error) {
90 } catch (_error) {
9191 toast({
92 title: 'Error',
93 description: 'Something went wrong',
94 variant: 'destructive',
92 title: "Error",
93 description: "Something went wrong",
94 variant: "destructive",
9595 })
9696 } finally {
9797 setIsVerifying(false)
@@ -104,9 +104,7 @@ export default function AuthPage() {
104104 <div className="space-y-6">
105105 <div className="text-center space-y-2">
106106 <h1 className="text-3xl font-bold text-white">Sign In</h1>
107 <p className="text-gray-300">
108 Enter your email to access the Function Creator
109 </p>
107 <p className="text-gray-300">Enter your email to access the Function Creator</p>
110108 </div>
111109
112110 {!emailSent ? (
@@ -132,15 +130,13 @@ export default function AuthPage() {
132130 className="w-full bg-blue-600 hover:bg-blue-700"
133131 >
134132 <Mail className="w-4 h-4 mr-2" />
135 {isLoading ? 'Sending...' : 'Send Magic Link'}
133 {isLoading ? "Sending..." : "Send Magic Link"}
136134 </Button>
137135 </form>
138136 ) : (
139137 <div className="space-y-4">
140138 <div className="bg-green-500/10 border border-green-500/30 rounded p-4 space-y-2">
141 <p className="text-green-400 font-medium">
142 Check your email!
143 </p>
139 <p className="text-green-400 font-medium">Check your email!</p>
144140 <p className="text-sm text-gray-300">
145141 We sent you a magic link and a 6-digit code to <strong>{email}</strong>
146142 </p>
@@ -164,7 +160,7 @@ export default function AuthPage() {
164160 id="otp"
165161 type="text"
166162 value={otp}
167 onChange={(e) => setOtp(e.target.value.replace(/\D/g, '').slice(0, 6))}
163 onChange={(e) => setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))}
168164 placeholder="000000"
169165 maxLength={6}
170166 className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-center text-2xl tracking-widest"
@@ -177,14 +173,14 @@ export default function AuthPage() {
177173 className="w-full bg-blue-600 hover:bg-blue-700"
178174 >
179175 <KeyRound className="w-4 h-4 mr-2" />
180 {isVerifying ? 'Verifying...' : 'Verify Code'}
176 {isVerifying ? "Verifying..." : "Verify Code"}
181177 </Button>
182178 </form>
183179
184180 <Button
185181 onClick={() => {
186182 setEmailSent(false)
187 setOtp('')
183 setOtp("")
188184 }}
189185 variant="outline"
190186 className="w-full"
@@ -196,8 +192,8 @@ export default function AuthPage() {
196192
197193 <div className="text-xs text-gray-400 text-center">
198194 <p>
199 Authentication is required to prevent abuse and manage rate limits.
200 No password needed - we will send you a magic link and code.
195 Authentication is required to prevent abuse and manage rate limits. No password needed
196 - we will send you a magic link and code.
201197 </p>
202198 </div>
203199 </div>
src/app/layout.tsx +20 −19
@@ -1,14 +1,15 @@
1 import type { Metadata } from "next";
2 import { Inter } from "next/font/google";
3 import "./globals.css";
4 import { SpaceBackground } from "@/components/SpaceBackground";
5 import { FloatingNavigation } from "@/components/FloatingNavigation";
6 import { InteractiveBackground } from "@/components/InteractiveBackground";
7 import { Toaster } from "@/components/ui/toaster";
8 import { Trevato } from "@/components/Trevato";
9 import { Analytics } from "@vercel/analytics/react";
1 import type { Metadata } from "next"
2 import { Inter } from "next/font/google"
3 import "./globals.css"
4 import { Analytics } from "@vercel/analytics/react"
5 import { ClientErrorBoundary } from "@/components/ClientErrorBoundary"
6 import { FloatingNavigation } from "@/components/FloatingNavigation"
7 import { InteractiveBackground } from "@/components/InteractiveBackground"
8 import { SpaceBackground } from "@/components/SpaceBackground"
9 import { Trevato } from "@/components/Trevato"
10 import { Toaster } from "@/components/ui/toaster"
1011
11 const inter = Inter({ subsets: ["latin"] });
12 const inter = Inter({ subsets: ["latin"] })
1213
1314 export const metadata: Metadata = {
1415 title: {
@@ -18,7 +19,8 @@ export const metadata: Metadata = {
1819 description: "Building systems from first principles. Working with people who know their domain.",
1920 openGraph: {
2021 title: "trevato.dev",
21 description: "Building systems from first principles. Working with people who know their domain.",
22 description:
23 "Building systems from first principles. Working with people who know their domain.",
2224 url: "https://trevato.dev",
2325 siteName: "trevato.dev",
2426 images: [
@@ -46,31 +48,30 @@ export const metadata: Metadata = {
4648 title: "trevordobbertin",
4749 card: "summary_large_image",
4850 },
49 };
51 }
5052
5153 export default function RootLayout({
5254 children,
5355 }: Readonly<{
54 children: React.ReactNode;
56 children: React.ReactNode
5557 }>) {
5658 return (
5759 <html lang="en">
5860 <head>
59 <meta
60 name="viewport"
61 content="width=device-width, initial-scale=1, viewport-fit=cover"
62 />
61 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
6362 </head>
6463 <body className={`${inter.className} bg-black text-white`}>
6564 <SpaceBackground>
6665 <InteractiveBackground />
6766 <FloatingNavigation />
68 <main className="min-h-screen p-4 md:p-8">{children}</main>
67 <main className="min-h-screen p-4 md:p-8">
68 <ClientErrorBoundary>{children}</ClientErrorBoundary>
69 </main>
6970 <Trevato />
7071 </SpaceBackground>
7172 <Toaster />
7273 <Analytics />
7374 </body>
7475 </html>
75 );
76 )
7677 }
src/app/page.tsx +2 −2
@@ -1,7 +1,7 @@
1 import { Contributions } from "@/components/Contributions"
12 import { HeroSection } from "@/components/HeroSection"
2 import { ServicesOverview } from "@/components/ServicesOverview"
33 import { Me } from "@/components/Me"
4 import { Contributions } from '@/components/Contributions'
4 import { ServicesOverview } from "@/components/ServicesOverview"
55
66 export default function Home() {
77 return (
src/app/playground/function-creator/page.tsx +25 −20
@@ -1,11 +1,13 @@
1 'use client'
1 "use client"
22
3 import { useEffect, useState } from 'react'
4 import { createClient } from '@/utils/supabase/client'
5 import { FunctionCreator } from '@/components/playground/FunctionCreator'
6 import { Card } from '@/components/ui/card'
7 import { Button } from '@/components/ui/button'
8 import Link from 'next/link'
3 import Link from "next/link"
4 import { useEffect, useState } from "react"
5 import { ErrorBoundary } from "@/components/ErrorBoundary"
6 import { FunctionCreator } from "@/components/playground/FunctionCreator"
7 import { Button } from "@/components/ui/button"
8 import { Card } from "@/components/ui/card"
9 import { Spinner } from "@/components/ui/spinner"
10 import { createClient } from "@/utils/supabase/client"
911
1012 export default function FunctionCreatorPage() {
1113 const [isAuthenticated, setIsAuthenticated] = useState(false)
@@ -14,7 +16,9 @@ export default function FunctionCreatorPage() {
1416
1517 useEffect(() => {
1618 const checkAuth = async () => {
17 const { data: { user } } = await supabase.auth.getUser()
19 const {
20 data: { user },
21 } = await supabase.auth.getUser()
1822 setIsAuthenticated(!!user)
1923 setIsLoading(false)
2024 }
@@ -22,7 +26,9 @@ export default function FunctionCreatorPage() {
2226 checkAuth()
2327
2428 // Listen for auth changes
25 const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
29 const {
30 data: { subscription },
31 } = supabase.auth.onAuthStateChange((_event, session) => {
2632 setIsAuthenticated(!!session)
2733 })
2834
@@ -31,8 +37,9 @@ export default function FunctionCreatorPage() {
3137
3238 if (isLoading) {
3339 return (
34 <div className="container mx-auto px-4 flex items-center justify-center min-h-[80vh]">
35 <div className="text-white">Loading...</div>
40 <div className="container mx-auto px-4 flex flex-col items-center justify-center min-h-[80vh] gap-4">
41 <Spinner size="lg" className="text-blue-500" />
42 <p className="text-gray-400">Checking authentication...</p>
3643 </div>
3744 )
3845 }
@@ -44,20 +51,16 @@ export default function FunctionCreatorPage() {
4451 <div className="space-y-6 text-center">
4552 <div className="space-y-2">
4653 <h1 className="text-3xl font-bold text-white">Authentication Required</h1>
47 <p className="text-gray-300">
48 Sign in to access the Function Creator
49 </p>
54 <p className="text-gray-300">Sign in to access the Function Creator</p>
5055 </div>
5156 <div className="text-sm text-gray-400">
5257 <p>
53 We require authentication to prevent abuse and manage rate limits.
54 This feature uses computational resources that need to be monitored.
58 We require authentication to prevent abuse and manage rate limits. This feature uses
59 computational resources that need to be monitored.
5560 </p>
5661 </div>
5762 <Link href="/auth/sign-in">
58 <Button className="w-full bg-blue-600 hover:bg-blue-700">
59 Sign In
60 </Button>
63 <Button className="w-full bg-blue-600 hover:bg-blue-700">Sign In</Button>
6164 </Link>
6265 </div>
6366 </Card>
@@ -73,7 +76,9 @@ export default function FunctionCreatorPage() {
7376 </p>
7477 <div className="max-w-[95vw] sm:max-w-4xl mx-auto">
7578 <div className="bg-gray-900 bg-opacity-40 p-4 rounded-lg backdrop-filter backdrop-blur-xl">
76 <FunctionCreator />
79 <ErrorBoundary>
80 <FunctionCreator />
81 </ErrorBoundary>
7782 </div>
7883 </div>
7984 </div>
src/app/playground/page.tsx +14 −12
@@ -1,25 +1,25 @@
1 import Link from 'next/link'
1 import { Box, CodeIcon, Palette } from "lucide-react"
2 import Link from "next/link"
23 import { Card } from "@/components/ui/card"
3 import { CodeIcon, Box, Palette } from 'lucide-react'
44
55 export default function PlaygroundPage() {
66 const tools = [
77 {
8 href: '/playground/function-creator',
9 title: 'Function Creator',
10 description: 'Write and execute TypeScript or Python code in a secure environment',
8 href: "/playground/function-creator",
9 title: "Function Creator",
10 description: "Write and execute TypeScript or Python code in a secure environment",
1111 icon: CodeIcon,
1212 },
1313 {
14 href: '/playground/3d-viewer',
15 title: '3D Object Viewer',
16 description: 'Interactive 3D model viewer with orbit controls',
14 href: "/playground/3d-viewer",
15 title: "3D Object Viewer",
16 description: "Interactive 3D model viewer with orbit controls",
1717 icon: Box,
1818 },
1919 {
20 href: '/playground/color-picker',
21 title: 'Color Picker',
22 description: 'Select and preview colors',
20 href: "/playground/color-picker",
21 title: "Color Picker",
22 description: "Select and preview colors",
2323 icon: Palette,
2424 },
2525 ]
@@ -27,7 +27,9 @@ export default function PlaygroundPage() {
2727 return (
2828 <div className="container mx-auto px-4">
2929 <h1 className="text-4xl font-bold mb-2 text-center text-white">Playground</h1>
30 <p className="text-lg text-gray-300 mb-8 text-center">Experiments and tools worth keeping around</p>
30 <p className="text-lg text-gray-300 mb-8 text-center">
31 Experiments and tools worth keeping around
32 </p>
3133
3234 <div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-4">
3335 {tools.map((tool) => (
src/app/projects/[project]/page.tsx +28 −19
@@ -1,25 +1,25 @@
1 import { GitHubProject } from "@/components/GitHubProject";
2 import { Octokit, RequestError } from "octokit";
3 import { projectRepoMap } from "@/lib/projects";
4 import Link from 'next/link';
1 import Link from "next/link"
2 import { Octokit, RequestError } from "octokit"
3 import { GitHubProject } from "@/components/GitHubProject"
4 import { projectRepoMap } from "@/lib/projects"
55
66 export default async function Page({ params }: { params: { project: string } }) {
7 const repoName = projectRepoMap[params.project];
7 const repoName = projectRepoMap[params.project]
88
99 if (!repoName) {
10 return <div>Project not found</div>;
10 return <div>Project not found</div>
1111 }
1212
13 const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
13 const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
1414
1515 try {
16 const [owner, repo] = repoName.split('/');
16 const [owner, repo] = repoName.split("/")
1717 const [repoData, readmeData] = await Promise.all([
1818 octokit.rest.repos.get({ owner, repo }),
19 octokit.rest.repos.getReadme({ owner, repo })
20 ]);
19 octokit.rest.repos.getReadme({ owner, repo }),
20 ])
2121
22 const readme = Buffer.from(readmeData.data.content, 'base64').toString('utf-8');
22 const readme = Buffer.from(readmeData.data.content, "base64").toString("utf-8")
2323
2424 return (
2525 <>
@@ -27,8 +27,17 @@ export default async function Page({ params }: { params: { project: string } })
2727 <div className="container mx-auto px-4">
2828 <Link href="/projects">
2929 <button className="px-4 py-2 rounded-lg backdrop-blur-sm bg-transparent hover:bg-white/10 transition-all duration-300 border border-white/20 hover:border-white/40 text-white/80 hover:text-white flex items-center space-x-2">
30 <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
31 <path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
30 <svg
31 xmlns="http://www.w3.org/2000/svg"
32 className="h-5 w-5"
33 viewBox="0 0 20 20"
34 fill="currentColor"
35 >
36 <path
37 fillRule="evenodd"
38 d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
39 clipRule="evenodd"
40 />
3241 </svg>
3342 <span>Back to Projects</span>
3443 </button>
@@ -39,24 +48,24 @@ export default async function Page({ params }: { params: { project: string } })
3948 <div className="space-y-24">
4049 <GitHubProject
4150 repoName={repoData.data.name}
42 repoDescription={repoData.data.description ?? ''}
51 repoDescription={repoData.data.description ?? ""}
4352 repoUrl={repoData.data.html_url}
4453 stars={repoData.data.stargazers_count}
4554 forks={repoData.data.forks_count}
4655 watchers={repoData.data.watchers_count}
47 language={repoData.data.language ?? ''}
56 language={repoData.data.language ?? ""}
4857 readme={readme}
4958 />
5059 </div>
5160 </div>
5261 </>
53 );
62 )
5463 } catch (error) {
5564 if (error instanceof RequestError) {
56 console.error('Error fetching project data:', error.message, 'Status:', error.status);
65 console.error("Error fetching project data:", error.message, "Status:", error.status)
5766 } else {
58 console.error('Unexpected error:', error);
67 console.error("Unexpected error:", error)
5968 }
60 return <div>Failed to fetch project data</div>;
69 return <div>Failed to fetch project data</div>
6170 }
6271 }
src/app/projects/page.tsx +51 −47
@@ -1,52 +1,56 @@
1 import { projectRepoMap } from "@/lib/projects";
2 import { Card } from "@/components/ui/card";
3 import Link from "next/link";
4 import { Octokit } from "octokit";
1 import Link from "next/link"
2 import { Octokit } from "octokit"
3 import { Card } from "@/components/ui/card"
4 import { projectRepoMap } from "@/lib/projects"
55
66 export default async function Page() {
7 const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
7 const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
88
9 const projectsData = await Promise.all(
10 Object.entries(projectRepoMap).map(async ([key, value]) => {
11 try {
12 const [owner, repo] = value.split('/');
13 const { data } = await octokit.rest.repos.get({ owner, repo });
14 return { key, value, description: data.description };
15 } catch (error) {
16 console.error(`Error fetching data for ${key}:`, error);
17 return { key, value, description: 'A fascinating project' };
18 }
19 })
20 );
9 const projectsData = await Promise.all(
10 Object.entries(projectRepoMap).map(async ([key, value]) => {
11 try {
12 const [owner, repo] = value.split("/")
13 const { data } = await octokit.rest.repos.get({ owner, repo })
14 return { key, value, description: data.description }
15 } catch (error) {
16 console.error(`Error fetching data for ${key}:`, error)
17 return { key, value, description: "A fascinating project" }
18 }
19 })
20 )
2121
22 return (
23 <div className="flex flex-col space-y-8 p-6">
24 <header className="text-center">
25 <h1 className="text-3xl font-bold text-gray-100 mb-4">My Side Projects</h1>
26 <p className="text-lg text-gray-300 max-w-2xl mx-auto">
27 Side projects and experiments. Things I thought should exist, so I built them to see if they could.
28 </p>
29 </header>
30 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
31 {projectsData.map(({ key, value, description }) => (
32 <Card key={key} className="group overflow-hidden rounded-lg backdrop-blur-sm bg-black/10 hover:bg-black/20 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105">
33 <Link href={`/projects/${key}`} className="block p-6">
34 <div className="flex items-center mb-2">
35 <span className="text-2xl mr-2" role="img" aria-label="Project icon">
36 💡
37 </span>
38 <h3 className="text-lg font-semibold text-gray-200 group-hover:text-white transition-colors duration-300">
39 {key}
40 </h3>
41 </div>
42 <p className="text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300 mb-4">
43 {description}
44 </p>
45 <div className="w-12 h-1 bg-blue-500 group-hover:w-full transition-all duration-500 ease-in-out"></div>
46 </Link>
47 </Card>
48 ))}
49 </div>
50 </div>
51 );
22 return (
23 <div className="flex flex-col space-y-8 p-6">
24 <header className="text-center">
25 <h1 className="text-3xl font-bold text-gray-100 mb-4">My Side Projects</h1>
26 <p className="text-lg text-gray-300 max-w-2xl mx-auto">
27 Side projects and experiments. Things I thought should exist, so I built them to see if
28 they could.
29 </p>
30 </header>
31 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
32 {projectsData.map(({ key, value, description }) => (
33 <Card
34 key={key}
35 className="group overflow-hidden rounded-lg backdrop-blur-sm bg-black/10 hover:bg-black/20 transition-all duration-300 shadow-lg hover:shadow-xl transform hover:scale-105"
36 >
37 <Link href={`/projects/${key}`} className="block p-6">
38 <div className="flex items-center mb-2">
39 <span className="text-2xl mr-2" role="img" aria-label="Project icon">
40 💡
41 </span>
42 <h3 className="text-lg font-semibold text-gray-200 group-hover:text-white transition-colors duration-300">
43 {key}
44 </h3>
45 </div>
46 <p className="text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300 mb-4">
47 {description}
48 </p>
49 <div className="w-12 h-1 bg-blue-500 group-hover:w-full transition-all duration-500 ease-in-out"></div>
50 </Link>
51 </Card>
52 ))}
53 </div>
54 </div>
55 )
5256 }
src/app/supersecretcalendar/page.tsx +31 −8
@@ -1,15 +1,38 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
5 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
63 import Cal from "@calcom/embed-react"
4 import { motion } from "framer-motion"
5 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
76
87 const services = [
9 { id: 1, title: 'Personal workflow optimization', description: 'I\'ve been a remote software developer for 5 years. I led the development of a custom Enterprise Resource Planning system for Espo Corporation just prior to the Covid lockdowns, enabling employees to work from home as the business was classified as essential. During the same time I got my Computer Science degree, suffering through e-learning. I\'ve picked up some methods to dealing with the madness. ', name: 'workflow-optimization-consult' },
10 { id: 2, title: 'Custom software development', description: 'AI agents, custom ERP systems, API integrations, infrastructure deployment, data migrations. I\'ve done it all. Trust me, let me do it for you.', name: 'custom-software-development-consult' },
11 { id: 3, title: 'Trusted technical partner', description: 'I have been writing code for over 10 years and have always been quite obssessed. When I was 12 I started mowing lawns, I\'ve done demolition work, I was a dish boy, I even worked at a detail shop. I\'ve worked with different people in plenty of industries. I know how to build software to solve real problems. Bring your problem and leave with a plan.', name: 'technical-partner-consult' },
12 { id: 4, title: 'Super Secret Calendar', description: 'You either know me personally or someone I trust gave you this link. Grab time below.', name: 'super-secret-calendar' },
8 {
9 id: 1,
10 title: "Personal workflow optimization",
11 description:
12 "I've been a remote software developer for 5 years. I led the development of a custom Enterprise Resource Planning system for Espo Corporation just prior to the Covid lockdowns, enabling employees to work from home as the business was classified as essential. During the same time I got my Computer Science degree, suffering through e-learning. I've picked up some methods to dealing with the madness. ",
13 name: "workflow-optimization-consult",
14 },
15 {
16 id: 2,
17 title: "Custom software development",
18 description:
19 "AI agents, custom ERP systems, API integrations, infrastructure deployment, data migrations. I've done it all. Trust me, let me do it for you.",
20 name: "custom-software-development-consult",
21 },
22 {
23 id: 3,
24 title: "Trusted technical partner",
25 description:
26 "I have been writing code for over 10 years and have always been quite obssessed. When I was 12 I started mowing lawns, I've done demolition work, I was a dish boy, I even worked at a detail shop. I've worked with different people in plenty of industries. I know how to build software to solve real problems. Bring your problem and leave with a plan.",
27 name: "technical-partner-consult",
28 },
29 {
30 id: 4,
31 title: "Super Secret Calendar",
32 description:
33 "You either know me personally or someone I trust gave you this link. Grab time below.",
34 name: "super-secret-calendar",
35 },
1336 ]
1437
1538 export default function ServicePage() {
src/app/thanksforbooking/page.tsx +12 −18
@@ -1,10 +1,10 @@
1 "use client";
1 "use client"
22
3 import { motion } from "framer-motion";
4 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5 import Link from "next/link";
6 import { Button } from "@/components/ui/button";
7 import { Home } from "lucide-react";
3 import { motion } from "framer-motion"
4 import { Home } from "lucide-react"
5 import Link from "next/link"
6 import { Button } from "@/components/ui/button"
7 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
88
99 export default function ThanksForBookingPage() {
1010 return (
@@ -28,8 +28,7 @@ export default function ThanksForBookingPage() {
2828 transition={{ delay: 0.3 }}
2929 className="text-xl text-blue-200 text-center"
3030 >
31 I&apos;m looking forward to our conversation and learning more
32 about your project.
31 I&apos;m looking forward to our conversation and learning more about your project.
3332 </motion.div>
3433
3534 <motion.div
@@ -38,9 +37,7 @@ export default function ThanksForBookingPage() {
3837 transition={{ delay: 0.5 }}
3938 className="bg-black/50 backdrop-blur-md border border-blue-500/50 rounded-lg p-6"
4039 >
41 <h2 className="text-xl font-semibold mb-4 text-blue-300">
42 What&apos;s Next?
43 </h2>
40 <h2 className="text-xl font-semibold mb-4 text-blue-300">What&apos;s Next?</h2>
4441 <ul className="space-y-4 text-gray-300">
4542 <li className="flex items-center space-x-3">
4643 <span className="text-blue-400">✉️</span>
@@ -49,15 +46,13 @@ export default function ThanksForBookingPage() {
4946 <li className="flex items-center space-x-3">
5047 <span className="text-blue-400">📝</span>
5148 <span>
52 Feel free to prepare any specific questions or topics
53 you&apos;d like to discuss
49 Feel free to prepare any specific questions or topics you&apos;d like to discuss
5450 </span>
5551 </li>
5652 <li className="flex items-center space-x-3">
5753 <span className="text-blue-400">🎯</span>
5854 <span>
59 I&apos;ll review any information you&apos;ve provided before
60 our meeting
55 I&apos;ll review any information you&apos;ve provided before our meeting
6156 </span>
6257 </li>
6358 </ul>
@@ -70,8 +65,7 @@ export default function ThanksForBookingPage() {
7065 className="text-center"
7166 >
7267 <p className="text-gray-400 mb-6">
73 If you need to make any changes, please don&apos;t hesitate to
74 reach out.
68 If you need to make any changes, please don&apos;t hesitate to reach out.
7569 </p>
7670 <Button
7771 asChild
@@ -88,5 +82,5 @@ export default function ThanksForBookingPage() {
8882 </Card>
8983 </motion.div>
9084 </div>
91 );
85 )
9286 }
src/components/ClientErrorBoundary.tsx added +8
@@ -0,0 +1,8 @@
1 "use client"
2
3 import { type ReactNode } from "react"
4 import { ErrorBoundary } from "./ErrorBoundary"
5
6 export function ClientErrorBoundary({ children }: { children: ReactNode }) {
7 return <ErrorBoundary>{children}</ErrorBoundary>
8 }
src/components/Contributions.tsx +5 −5
@@ -1,8 +1,8 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
5 import WebsiteGallery from './WebsiteGallery'
3 import { motion } from "framer-motion"
4 import type React from "react"
5 import WebsiteGallery from "./WebsiteGallery"
66
77 // Using the same websites from the original portfolio carousel
88 const websites = [
@@ -46,7 +46,7 @@ const websites = [
4646 export const Contributions: React.FC = () => {
4747 return (
4848 <section className="py-12 container mx-auto px-4">
49 <motion.h2
49 <motion.h2
5050 className="text-3xl font-bold mb-8 text-center text-white text-glow"
5151 initial={{ opacity: 0, y: 20 }}
5252 animate={{ opacity: 1, y: 0 }}
src/components/ErrorBoundary.tsx added +106
@@ -0,0 +1,106 @@
1 "use client"
2
3 import { Component, type ErrorInfo, type ReactNode } from "react"
4 import { Button } from "@/components/ui/button"
5 import { Card } from "@/components/ui/card"
6
7 interface Props {
8 children: ReactNode
9 fallback?: ReactNode
10 }
11
12 interface State {
13 hasError: boolean
14 error: Error | null
15 }
16
17 export class ErrorBoundary extends Component<Props, State> {
18 constructor(props: Props) {
19 super(props)
20 this.state = { hasError: false, error: null }
21 }
22
23 static getDerivedStateFromError(error: Error): State {
24 return { hasError: true, error }
25 }
26
27 componentDidCatch(error: Error, errorInfo: ErrorInfo) {
28 // Log to console in development
29 if (process.env.NODE_ENV === "development") {
30 console.error("Error boundary caught:", error, errorInfo)
31 }
32
33 // Send to Sentry in production
34 if (process.env.NODE_ENV === "production" && typeof window !== "undefined") {
35 import("@sentry/nextjs").then((Sentry) => {
36 Sentry.captureException(error, {
37 contexts: {
38 react: {
39 componentStack: errorInfo.componentStack,
40 },
41 },
42 })
43 })
44 }
45 }
46
47 render() {
48 if (this.state.hasError) {
49 if (this.props.fallback) {
50 return this.props.fallback
51 }
52
53 return (
54 <Card className="bg-red-950/20 border-red-500/50 p-6 mx-auto max-w-md">
55 <div className="space-y-4">
56 <div className="flex items-center gap-2">
57 <svg
58 className="w-6 h-6 text-red-500"
59 fill="none"
60 stroke="currentColor"
61 viewBox="0 0 24 24"
62 >
63 <title>Error icon</title>
64 <path
65 strokeLinecap="round"
66 strokeLinejoin="round"
67 strokeWidth={2}
68 d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
69 />
70 </svg>
71 <h2 className="text-xl font-semibold text-red-400">Something went wrong</h2>
72 </div>
73
74 {process.env.NODE_ENV === "development" && this.state.error && (
75 <div className="bg-black/30 p-3 rounded text-xs font-mono text-gray-300 overflow-auto max-h-32">
76 {this.state.error.message}
77 </div>
78 )}
79
80 <p className="text-gray-400 text-sm">
81 The component encountered an error. Please try refreshing the page.
82 </p>
83
84 <div className="flex gap-2">
85 <Button
86 onClick={() => window.location.reload()}
87 className="flex-1"
88 variant="outline"
89 >
90 Refresh Page
91 </Button>
92 <Button
93 onClick={() => this.setState({ hasError: false, error: null })}
94 className="flex-1"
95 >
96 Try Again
97 </Button>
98 </div>
99 </div>
100 </Card>
101 )
102 }
103
104 return this.props.children
105 }
106 }
src/components/FloatingNavigation.tsx +28 −34
@@ -1,19 +1,20 @@
1 'use client'
1 "use client"
22
3 import React, { useState, useEffect } from 'react'
3 import { LogOut, Menu } from "lucide-react"
4 import Link from "next/link"
5 import { useRouter } from "next/navigation"
6 import type React from "react"
7 import { useEffect, useState } from "react"
48 import { Button } from "@/components/ui/button"
59 import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
6 import Link from 'next/link'
7 import { useRouter } from 'next/navigation'
8 import { Menu, LogOut } from 'lucide-react'
9 import { createClient } from '@/utils/supabase/client'
10 import { createClient } from "@/utils/supabase/client"
1011
1112 const navItems = [
12 { href: '/', label: 'Home' },
13 { href: '/playground/function-creator', label: 'Function Creator' },
14 { href: '/playground/3d-viewer', label: '3D Viewer' },
15 { href: '/playground/color-picker', label: 'Color Picker' },
16 { href: '/projects', label: 'Side Projects' },
13 { href: "/", label: "Home" },
14 { href: "/playground/function-creator", label: "Function Creator" },
15 { href: "/playground/3d-viewer", label: "3D Viewer" },
16 { href: "/playground/color-picker", label: "Color Picker" },
17 { href: "/projects", label: "Side Projects" },
1718 ]
1819
1920 export const FloatingNavigation: React.FC = () => {
@@ -24,14 +25,18 @@ export const FloatingNavigation: React.FC = () => {
2425
2526 useEffect(() => {
2627 const checkAuth = async () => {
27 const { data: { user } } = await supabase.auth.getUser()
28 const {
29 data: { user },
30 } = await supabase.auth.getUser()
2831 setUserEmail(user?.email || null)
2932 }
3033
3134 checkAuth()
3235
3336 // Listen for auth changes
34 const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
37 const {
38 data: { subscription },
39 } = supabase.auth.onAuthStateChange((_event, session) => {
3540 setUserEmail(session?.user?.email || null)
3641 })
3742
@@ -41,7 +46,7 @@ export const FloatingNavigation: React.FC = () => {
4146 const handleSignOut = async () => {
4247 await supabase.auth.signOut()
4348 setOpen(false)
44 router.push('/')
49 router.push("/")
4550 }
4651
4752 return (
@@ -66,27 +71,16 @@ export const FloatingNavigation: React.FC = () => {
6671 ))}
6772
6873 {userEmail && (
69 <>
70 <div className="border-t border-gray-700 pt-4 mt-4">
71 <div className="space-y-3">
72 <div className="text-sm text-gray-400">
73 Signed in as:
74 </div>
75 <div className="text-sm text-white font-medium break-all">
76 {userEmail}
77 </div>
78 <Button
79 onClick={handleSignOut}
80 variant="outline"
81 size="sm"
82 className="w-full"
83 >
84 <LogOut className="w-4 h-4 mr-2" />
85 Sign Out
86 </Button>
87 </div>
74 <div className="border-t border-gray-700 pt-4 mt-4">
75 <div className="space-y-3">
76 <div className="text-sm text-gray-400">Signed in as:</div>
77 <div className="text-sm text-white font-medium break-all">{userEmail}</div>
78 <Button onClick={handleSignOut} variant="outline" size="sm" className="w-full">
79 <LogOut className="w-4 h-4 mr-2" />
80 Sign Out
81 </Button>
8882 </div>
89 </>
83 </div>
9084 )}
9185 </nav>
9286 </SheetContent>
src/components/GitHubProject.tsx +19 −19
@@ -1,16 +1,16 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { Card, CardContent } from '@/components/ui/card'
5 import Link from 'next/link'
6 import { FaGithub, FaStar, FaCodeBranch, FaEye, FaCode } from 'react-icons/fa'
7 import { RiGitRepositoryLine } from 'react-icons/ri'
8 import ReactMarkdown from 'react-markdown'
9 import rehypeRaw from 'rehype-raw'
10 import remarkGfm from 'remark-gfm'
11 import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
12 import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'
13 import '@/styles/github-markdown.css'
3 import Link from "next/link"
4 import type React from "react"
5 import { FaCode, FaCodeBranch, FaEye, FaGithub, FaStar } from "react-icons/fa"
6 import { RiGitRepositoryLine } from "react-icons/ri"
7 import ReactMarkdown from "react-markdown"
8 import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
9 import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
10 import rehypeRaw from "rehype-raw"
11 import remarkGfm from "remark-gfm"
12 import { Card, CardContent } from "@/components/ui/card"
13 import "@/styles/github-markdown.css"
1414
1515 interface GitHubProjectProps {
1616 repoName: string
@@ -48,10 +48,10 @@ export const GitHubProject: React.FC<GitHubProjectProps> = ({
4848 <div className="flex flex-col items-center mb-6">
4949 <div className="flex flex-wrap justify-center space-x-4 mb-4">
5050 {[
51 { icon: FaStar, label: 'Stars', value: stars },
52 { icon: FaCodeBranch, label: 'Forks', value: forks },
53 { icon: FaEye, label: 'Watchers', value: watchers },
54 { icon: FaCode, label: 'Language', value: language },
51 { icon: FaStar, label: "Stars", value: stars },
52 { icon: FaCodeBranch, label: "Forks", value: forks },
53 { icon: FaEye, label: "Watchers", value: watchers },
54 { icon: FaCode, label: "Language", value: language },
5555 ].map((item) => (
5656 <div key={item.label} className="flex items-center text-gray-400 mb-2">
5757 <item.icon className="mr-1" />
@@ -79,7 +79,7 @@ export const GitHubProject: React.FC<GitHubProjectProps> = ({
7979 rehypePlugins={[rehypeRaw]}
8080 components={{
8181 code({ node, className, children, ...props }) {
82 const match = /language-(\w+)/.exec(className || '')
82 const match = /language-(\w+)/.exec(className || "")
8383 return match ? (
8484 // @ts-expect-error syntax highlight is fine
8585 <SyntaxHighlighter
@@ -88,14 +88,14 @@ export const GitHubProject: React.FC<GitHubProjectProps> = ({
8888 PreTag="div"
8989 {...props}
9090 >
91 {String(children).replace(/\n$/, '')}
91 {String(children).replace(/\n$/, "")}
9292 </SyntaxHighlighter>
9393 ) : (
9494 <code className={className} {...props}>
9595 {children}
9696 </code>
9797 )
98 }
98 },
9999 }}
100100 >
101101 {readme}
src/components/HeroSection.tsx +14 −10
@@ -1,10 +1,10 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
5 import { Button } from '@/components/ui/button'
6 import Link from 'next/link'
7 import { TypeAnimation } from 'react-type-animation'
3 import { motion } from "framer-motion"
4 import Link from "next/link"
5 import type React from "react"
6 import { TypeAnimation } from "react-type-animation"
7 import { Button } from "@/components/ui/button"
88
99 export const HeroSection: React.FC = () => {
1010 return (
@@ -30,11 +30,11 @@ export const HeroSection: React.FC = () => {
3030 >
3131 <TypeAnimation
3232 sequence={[
33 'First principles thinking',
33 "First principles thinking",
3434 3000,
35 'Built for your domain',
35 "Built for your domain",
3636 3000,
37 'Using AI as a tool, not a talking point',
37 "Using AI as a tool, not a talking point",
3838 3000,
3939 ]}
4040 wrapper="p"
@@ -51,7 +51,11 @@ export const HeroSection: React.FC = () => {
5151 <Button asChild className="bg-blue-600 hover:bg-blue-700 text-white neon-border">
5252 <Link href="#booking">Book a Call ($500)</Link>
5353 </Button>
54 <Button variant="outline" asChild className="border-blue-500 text-blue-500 hover:bg-blue-900 neon-border">
54 <Button
55 variant="outline"
56 asChild
57 className="border-blue-500 text-blue-500 hover:bg-blue-900 neon-border"
58 >
5559 <Link href="/projects">Explore My Work</Link>
5660 </Button>
5761 </motion.div>
src/components/InteractiveBackground.tsx +8 −11
@@ -1,6 +1,7 @@
1 'use client'
1 "use client"
22
3 import React, { useEffect, useRef } from 'react'
3 import type React from "react"
4 import { useEffect, useRef } from "react"
45
56 export const InteractiveBackground: React.FC = () => {
67 const canvasRef = useRef<HTMLCanvasElement>(null)
@@ -9,7 +10,7 @@ export const InteractiveBackground: React.FC = () => {
910 const canvas = canvasRef.current
1011 if (!canvas) return
1112
12 const ctx = canvas.getContext('2d')
13 const ctx = canvas.getContext("2d")
1314 if (!ctx) return
1415
1516 canvas.width = window.innerWidth
@@ -21,7 +22,7 @@ export const InteractiveBackground: React.FC = () => {
2122
2223 for (let i = 0; i < 200; i++) {
2324 const angle = Math.random() * Math.PI * 2
24 const distance = Math.random() * Math.min(canvas.width, canvas.height) / 2
25 const distance = (Math.random() * Math.min(canvas.width, canvas.height)) / 2
2526 particles.push({
2627 x: centerX + Math.cos(angle) * distance,
2728 y: centerY + Math.sin(angle) * distance,
@@ -66,18 +67,14 @@ export const InteractiveBackground: React.FC = () => {
6667 canvas.height = window.innerHeight
6768 }
6869
69 window.addEventListener('resize', handleResize)
70 window.addEventListener("resize", handleResize)
7071
7172 return () => {
72 window.removeEventListener('resize', handleResize)
73 window.removeEventListener("resize", handleResize)
7374 }
7475 }, [])
7576
7677 return (
77 <canvas
78 ref={canvasRef}
79 className="fixed inset-0 pointer-events-none"
80 style={{ zIndex: -1 }}
81 />
78 <canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" style={{ zIndex: -1 }} />
8279 )
8380 }
src/components/Me.tsx +10 −10
@@ -1,11 +1,11 @@
1 "use client";
1 "use client"
22
3 import React from "react";
4 import { motion } from "framer-motion";
5 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6 import Link from "next/link";
7 import Image from "next/image";
8 import { FaGithub, FaLinkedin, FaXTwitter, FaFilePdf } from "react-icons/fa6";
3 import { motion } from "framer-motion"
4 import Image from "next/image"
5 import Link from "next/link"
6 import type React from "react"
7 import { FaFilePdf, FaGithub, FaLinkedin, FaXTwitter } from "react-icons/fa6"
8 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
99
1010 const me = [
1111 {
@@ -18,7 +18,7 @@ const me = [
1818 x: "https://x.com/trevordobbertin",
1919 image: "/trevor.jpg",
2020 },
21 ];
21 ]
2222
2323 export const Me: React.FC = () => {
2424 return (
@@ -98,5 +98,5 @@ export const Me: React.FC = () => {
9898 ))}
9999 </div>
100100 </section>
101 );
102 };
101 )
102 }
src/components/ParallaxStars.tsx +6 −9
@@ -1,13 +1,14 @@
1 'use client'
1 "use client"
22
3 import React, { useEffect, useRef } from 'react'
4 import { motion, useScroll, useTransform } from 'framer-motion'
3 import { motion, useScroll, useTransform } from "framer-motion"
4 import type React from "react"
5 import { useEffect, useRef } from "react"
56
67 export const ParallaxStars: React.FC = () => {
78 const ref = useRef(null)
89 const { scrollYProgress } = useScroll({
910 target: ref,
10 offset: ["start start", "end start"]
11 offset: ["start start", "end start"],
1112 })
1213
1314 const y1 = useTransform(scrollYProgress, [0, 1], ["0%", "50%"])
@@ -27,11 +28,7 @@ export const ParallaxStars: React.FC = () => {
2728 return (
2829 <div ref={ref} className="fixed inset-0 pointer-events-none">
2930 {[y1, y2, y3].map((y, index) => (
30 <motion.div
31 key={index}
32 className="absolute inset-0"
33 style={{ y }}
34 >
31 <motion.div key={index} className="absolute inset-0" style={{ y }}>
3532 {stars.current.map((star, starIndex) => (
3633 <div
3734 key={starIndex}
src/components/PlaygroundTabs.tsx +10 −9
@@ -1,13 +1,14 @@
1 "use client";
1 "use client"
22
3 import React, { useState } from "react";
4 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
5 import { FunctionCreator } from "@/components/playground/FunctionCreator";
6 import { ColorPicker } from "@/components/playground/ColorPicker";
7 import { ObjectViewer } from "@/components/playground/ObjectViewer";
3 import type React from "react"
4 import { useState } from "react"
5 import { ColorPicker } from "@/components/playground/ColorPicker"
6 import { FunctionCreator } from "@/components/playground/FunctionCreator"
7 import { ObjectViewer } from "@/components/playground/ObjectViewer"
8 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
89
910 export const PlaygroundTabs: React.FC = () => {
10 const [activeTab, setActiveTab] = useState("object-viewer");
11 const [activeTab, setActiveTab] = useState("object-viewer")
1112
1213 return (
1314 <Tabs
@@ -54,5 +55,5 @@ export const PlaygroundTabs: React.FC = () => {
5455 <FunctionCreator />
5556 </TabsContent>
5657 </Tabs>
57 );
58 };
58 )
59 }
src/components/ProjectsList.tsx +67 −16
@@ -1,18 +1,53 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
5 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6 import Link from 'next/link'
7 import { Github, ExternalLink } from 'lucide-react'
8 import Image from 'next/image'
3 import { motion } from "framer-motion"
4 import { ExternalLink, Github } from "lucide-react"
5 import Image from "next/image"
6 import Link from "next/link"
7 import type React from "react"
8 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
99
1010 const projects = [
11 { id: 1, title: 'Project Alpha', description: 'A revolutionary AI platform for predictive analytics', github: 'https://github.com/yourusername/project-alpha', demo: 'https://project-alpha-demo.com', image: '/project-alpha.jpg' },
12 { id: 2, title: 'Project Beta', description: 'Next-gen blockchain solution for secure transactions', github: 'https://github.com/yourusername/project-beta', demo: 'https://project-beta-demo.com', image: '/project-beta.jpg' },
13 { id: 3, title: 'Project Gamma', description: 'Quantum computing interface for complex simulations', github: 'https://github.com/yourusername/project-gamma', demo: 'https://project-gamma-demo.com', image: '/project-gamma.jpg' },
14 { id: 4, title: 'Project Delta', description: 'Advanced machine learning algorithms for natural language processing', github: 'https://github.com/yourusername/project-delta', demo: 'https://project-delta-demo.com', image: '/project-delta.jpg' },
15 { id: 5, title: 'Project Epsilon', description: 'Augmented reality toolkit for immersive educational experiences', github: 'https://github.com/yourusername/project-epsilon', demo: 'https://project-epsilon-demo.com', image: '/project-epsilon.jpg' },
11 {
12 id: 1,
13 title: "Project Alpha",
14 description: "A revolutionary AI platform for predictive analytics",
15 github: "https://github.com/yourusername/project-alpha",
16 demo: "https://project-alpha-demo.com",
17 image: "/project-alpha.jpg",
18 },
19 {
20 id: 2,
21 title: "Project Beta",
22 description: "Next-gen blockchain solution for secure transactions",
23 github: "https://github.com/yourusername/project-beta",
24 demo: "https://project-beta-demo.com",
25 image: "/project-beta.jpg",
26 },
27 {
28 id: 3,
29 title: "Project Gamma",
30 description: "Quantum computing interface for complex simulations",
31 github: "https://github.com/yourusername/project-gamma",
32 demo: "https://project-gamma-demo.com",
33 image: "/project-gamma.jpg",
34 },
35 {
36 id: 4,
37 title: "Project Delta",
38 description: "Advanced machine learning algorithms for natural language processing",
39 github: "https://github.com/yourusername/project-delta",
40 demo: "https://project-delta-demo.com",
41 image: "/project-delta.jpg",
42 },
43 {
44 id: 5,
45 title: "Project Epsilon",
46 description: "Augmented reality toolkit for immersive educational experiences",
47 github: "https://github.com/yourusername/project-epsilon",
48 demo: "https://project-epsilon-demo.com",
49 image: "/project-epsilon.jpg",
50 },
1651 ]
1752
1853 export const ProjectsList: React.FC = () => {
@@ -31,15 +66,31 @@ export const ProjectsList: React.FC = () => {
3166 </CardHeader>
3267 <CardContent className="flex-grow flex flex-col">
3368 <div className="relative h-48 mb-4">
34 <Image src={project.image} alt={project.title} layout="fill" objectFit="cover" className="rounded-md" />
69 <Image
70 src={project.image}
71 alt={project.title}
72 layout="fill"
73 objectFit="cover"
74 className="rounded-md"
75 />
3576 </div>
3677 <p className="text-white mb-4">{project.description}</p>
3778 <div className="flex justify-between mt-auto">
38 <Link href={project.github} target="_blank" rel="noopener noreferrer" className="flex items-center text-blue-400 hover:text-blue-300 transition-colors">
79 <Link
80 href={project.github}
81 target="_blank"
82 rel="noopener noreferrer"
83 className="flex items-center text-blue-400 hover:text-blue-300 transition-colors"
84 >
3985 <Github className="mr-2 h-4 w-4" />
4086 GitHub
4187 </Link>
42 <Link href={project.demo} target="_blank" rel="noopener noreferrer" className="flex items-center text-green-400 hover:text-green-300 transition-colors">
88 <Link
89 href={project.demo}
90 target="_blank"
91 rel="noopener noreferrer"
92 className="flex items-center text-green-400 hover:text-green-300 transition-colors"
93 >
4394 <ExternalLink className="mr-2 h-4 w-4" />
4495 Live Demo
4596 </Link>
@@ -50,4 +101,4 @@ export const ProjectsList: React.FC = () => {
50101 ))}
51102 </div>
52103 )
53 }↵
104 }
src/components/ServicesOverview.tsx +11 −6
@@ -1,9 +1,9 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
5 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
63 import Cal from "@calcom/embed-react"
4 import { motion } from "framer-motion"
5 import type React from "react"
6 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
77
88 export const ServicesOverview: React.FC = () => {
99 return (
@@ -17,10 +17,15 @@ export const ServicesOverview: React.FC = () => {
1717 <h2 className="text-3xl font-bold mb-8 text-center text-white">Start Here</h2>
1818 <Card className="bg-black/50 backdrop-blur-md border-blue-500 max-w-3xl mx-auto">
1919 <CardHeader>
20 <CardTitle className="text-blue-300">Got an idea? A problem you can&apos;t stop thinking about?</CardTitle>
20 <CardTitle className="text-blue-300">
21 Got an idea? A problem you can&apos;t stop thinking about?
22 </CardTitle>
2123 </CardHeader>
2224 <CardContent>
23 <p className="text-gray-300 mb-4">Book a consultation. We&apos;ll talk through it, figure out if it&apos;s something we should build together.</p>
25 <p className="text-gray-300 mb-4">
26 Book a consultation. We&apos;ll talk through it, figure out if it&apos;s something we
27 should build together.
28 </p>
2429 <p className="text-blue-300 font-semibold mb-4">$500. 30 minutes. No nonsense.</p>
2530 <div className="relative">
2631 <Cal
src/components/SkillsSection.tsx +10 −10
@@ -1,15 +1,15 @@
1 'use client'
1 "use client"
22
3 import React from 'react'
4 import { motion } from 'framer-motion'
3 import { motion } from "framer-motion"
4 import type React from "react"
55
66 const skills = [
7 { name: 'React', level: 90 },
8 { name: 'TypeScript', level: 85 },
9 { name: 'Node.js', level: 80 },
10 { name: 'GraphQL', level: 75 },
11 { name: 'Python', level: 70 },
12 { name: 'AWS', level: 65 },
7 { name: "React", level: 90 },
8 { name: "TypeScript", level: 85 },
9 { name: "Node.js", level: 80 },
10 { name: "GraphQL", level: 75 },
11 { name: "Python", level: 70 },
12 { name: "AWS", level: 65 },
1313 ]
1414
1515 export const SkillsSection: React.FC = () => {
@@ -40,4 +40,4 @@ export const SkillsSection: React.FC = () => {
4040 </div>
4141 </section>
4242 )
43 }↵
43 }
src/components/SpaceBackground.tsx +76 −54
@@ -1,6 +1,7 @@
1 'use client'
1 "use client"
22
3 import React, { useEffect, useRef } from 'react'
3 import type React from "react"
4 import { useEffect, useRef } from "react"
45
56 export const SpaceBackground: React.FC<{ children: React.ReactNode }> = ({ children }) => {
67 const canvasRef = useRef<HTMLCanvasElement>(null)
@@ -9,15 +10,28 @@ export const SpaceBackground: React.FC<{ children: React.ReactNode }> = ({ child
910 const canvas = canvasRef.current
1011 if (!canvas) return
1112
12 const ctx = canvas.getContext('2d')
13 const ctx = canvas.getContext("2d")
1314 if (!ctx) return
1415
1516 canvas.width = window.innerWidth
1617 canvas.height = window.innerHeight
1718
1819 const stars: { x: number; y: number; size: number; speed: number }[] = []
19 const shootingStars: { x: number; y: number; length: number; speed: number; opacity: number }[] = []
20 const particles: { x: number; y: number; size: number; color: string; speed: number; angle: number }[] = []
20 const shootingStars: {
21 x: number
22 y: number
23 length: number
24 speed: number
25 opacity: number
26 }[] = []
27 const particles: {
28 x: number
29 y: number
30 size: number
31 color: string
32 speed: number
33 angle: number
34 }[] = []
2135
2236 for (let i = 0; i < 200; i++) {
2337 stars.push({
@@ -40,7 +54,18 @@ export const SpaceBackground: React.FC<{ children: React.ReactNode }> = ({ child
4054
4155 function createExplosion(x: number, y: number) {
4256 const particleCount = 50
43 const colors = ['#ffffff', '#ffc0cb', '#e6e6fa', '#dda0dd', '#ff69b4', '#da70d6', '#8a2be2', '#9370db', '#7b68ee', '#4b0082']
57 const colors = [
58 "#ffffff",
59 "#ffc0cb",
60 "#e6e6fa",
61 "#dda0dd",
62 "#ff69b4",
63 "#da70d6",
64 "#8a2be2",
65 "#9370db",
66 "#7b68ee",
67 "#4b0082",
68 ]
4469 for (let i = 0; i < particleCount; i++) {
4570 particles.push({
4671 x,
@@ -48,7 +73,7 @@ export const SpaceBackground: React.FC<{ children: React.ReactNode }> = ({ child
4873 size: Math.random() * 2 + 1,
4974 color: colors[Math.floor(Math.random() * colors.length)],
5075 speed: Math.random() * 3 + 1,
51 angle: Math.random() * Math.PI * 2
76 angle: Math.random() * Math.PI * 2,
5277 })
5378 }
5479 }
@@ -74,62 +99,59 @@ export const SpaceBackground: React.FC<{ children: React.ReactNode }> = ({ child
7499 })
75100 }
76101
77 function animate() {
78 if (!ctx) return
79 if (!canvas) return
102 function animate() {
103 if (!ctx) return
104 if (!canvas) return
80105
81 ctx.clearRect(0, 0, canvas.width, canvas.height)
82
83 // Draw shooting stars
84 shootingStars.forEach((star, index) => {
85 ctx.strokeStyle = `rgba(255, 255, 255, ${star.opacity})`
86 ctx.lineWidth = 2
87 ctx.beginPath()
88 ctx.moveTo(star.x, star.y)
89 ctx.lineTo(star.x + star.length, star.y + star.length)
90 ctx.stroke()
91
92 star.x += star.speed
93 star.y += star.speed
94 star.opacity -= 0.02
95
96 if (star.opacity <= 0 || star.x > canvas.width || star.y > canvas.height) {
97 createExplosion(star.x, star.y)
98 shootingStars.splice(index, 1)
99 }
100 })
101
102 if (Math.random() < 0.01 && shootingStars.length < 5) {
103 shootingStars.push(createShootingStar())
104 }
105
106 // Update and draw particles
107 updateParticles()
108 drawParticles(ctx)
109
110 requestAnimationFrame(animate)
111 }
112
113 animate()
114
115 const handleResize = () => {
116 canvas.width = window.innerWidth
117 canvas.height = window.innerHeight
118 }
106 ctx.clearRect(0, 0, canvas.width, canvas.height)
119107
120 window.addEventListener('resize', handleResize)
108 // Draw shooting stars
109 shootingStars.forEach((star, index) => {
110 ctx.strokeStyle = `rgba(255, 255, 255, ${star.opacity})`
111 ctx.lineWidth = 2
112 ctx.beginPath()
113 ctx.moveTo(star.x, star.y)
114 ctx.lineTo(star.x + star.length, star.y + star.length)
115 ctx.stroke()
116
117 star.x += star.speed
118 star.y += star.speed
119 star.opacity -= 0.02
120
121 if (star.opacity <= 0 || star.x > canvas.width || star.y > canvas.height) {
122 createExplosion(star.x, star.y)
123 shootingStars.splice(index, 1)
124 }
125 })
126
127 if (Math.random() < 0.01 && shootingStars.length < 5) {
128 shootingStars.push(createShootingStar())
129 }
130
131 // Update and draw particles
132 updateParticles()
133 drawParticles(ctx)
134
135 requestAnimationFrame(animate)
136 }
137
138 animate()
139
140 const handleResize = () => {
141 canvas.width = window.innerWidth
142 canvas.height = window.innerHeight
143 }
144
145 window.addEventListener("resize", handleResize)
121146
122147 return () => {
123 window.removeEventListener('resize', handleResize)
148 window.removeEventListener("resize", handleResize)
124149 }
125150 }, [])
126151
127152 return (
128153 <div className="relative min-h-screen overflow-hidden bg-gradient-to-b from-black to-blue-900">
129 <canvas
130 ref={canvasRef}
131 className="absolute inset-0 w-full h-full"
132 />
154 <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
133155 <div className="relative z-10">{children}</div>
134156 </div>
135157 )
src/components/Trevato.tsx +5 −5
@@ -1,8 +1,8 @@
1 'use client'
2 import Image from 'next/image'
3 import Link from 'next/link'
4 import { motion, useScroll } from 'framer-motion'
5 import { useEffect, useState } from 'react'
1 "use client"
2 import { motion, useScroll } from "framer-motion"
3 import Image from "next/image"
4 import Link from "next/link"
5 import { useEffect, useState } from "react"
66
77 export function Trevato() {
88 const { scrollYProgress } = useScroll()
src/components/WebsiteFrame.tsx +28 −21
@@ -1,9 +1,9 @@
1 'use client'
1 "use client"
22
3 import { useState, useEffect, useRef } from 'react'
4 import { Button } from '@/components/ui/button'
5 import { motion } from 'framer-motion'
6 import { ZoomIn, ZoomOut } from 'lucide-react'
3 import { motion } from "framer-motion"
4 import { ZoomIn, ZoomOut } from "lucide-react"
5 import { useEffect, useRef, useState } from "react"
6 import { Button } from "@/components/ui/button"
77
88 interface WebsiteFrameProps {
99 url: string
@@ -48,10 +48,10 @@ export default function WebsiteFrame({
4848 }
4949
5050 updateDimensions()
51 window.addEventListener('resize', updateDimensions)
52 return () => window.removeEventListener('resize', updateDimensions)
51 window.addEventListener("resize", updateDimensions)
52 return () => window.removeEventListener("resize", updateDimensions)
5353 }
54 }, [scale, containerRef])
54 }, [scale])
5555
5656 // Force a re-render after initial mount to fix white screen issue
5757 useEffect(() => {
@@ -90,9 +90,9 @@ export default function WebsiteFrame({
9090 <div
9191 className="scale-container overflow-hidden"
9292 style={{
93 width: '100%',
94 height: '100%',
95 position: 'relative',
93 width: "100%",
94 height: "100%",
95 position: "relative",
9696 }}
9797 >
9898 <iframe
@@ -100,14 +100,14 @@ export default function WebsiteFrame({
100100 key={url}
101101 src={url}
102102 title={title}
103 className={`transition-all duration-500 ${isIframeLoaded ? 'opacity-100' : 'opacity-0'}`}
103 className={`transition-all duration-500 ${isIframeLoaded ? "opacity-100" : "opacity-0"}`}
104104 style={{
105105 width: `${dimensions.width}px`,
106106 height: `${dimensions.height}px`,
107107 border: 0,
108108 transform: `scale(${scale})`,
109 transformOrigin: '0 0',
110 position: 'absolute',
109 transformOrigin: "0 0",
110 position: "absolute",
111111 top: 0,
112112 left: 0,
113113 }}
@@ -118,24 +118,31 @@ export default function WebsiteFrame({
118118 </div>
119119
120120 {showControls && (
121 <motion.div
121 <motion.div
122122 initial={{ opacity: 0, y: 10 }}
123123 animate={{ opacity: 1, y: 0 }}
124124 transition={{ duration: 0.3 }}
125125 className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-2 rounded-full bg-black/70 backdrop-blur px-3 py-1.5 border border-blue-500 animate-glow"
126126 >
127 <Button onClick={handleZoomOut} size="icon" variant="ghost" className="h-8 w-8 text-blue-300 hover:text-blue-100 hover:bg-blue-900/50">
127 <Button
128 onClick={handleZoomOut}
129 size="icon"
130 variant="ghost"
131 className="h-8 w-8 text-blue-300 hover:text-blue-100 hover:bg-blue-900/50"
132 >
128133 <ZoomOut className="h-4 w-4" />
129134 </Button>
130135
131136 <div className="w-24 h-1.5 bg-blue-900/50 rounded-full overflow-hidden">
132 <div
133 className="h-full bg-blue-500"
134 style={{ width: `${scale * 100}%` }}
135 />
137 <div className="h-full bg-blue-500" style={{ width: `${scale * 100}%` }} />
136138 </div>
137139
138 <Button onClick={handleZoomIn} size="icon" variant="ghost" className="h-8 w-8 text-blue-300 hover:text-blue-100 hover:bg-blue-900/50">
140 <Button
141 onClick={handleZoomIn}
142 size="icon"
143 variant="ghost"
144 className="h-8 w-8 text-blue-300 hover:text-blue-100 hover:bg-blue-900/50"
145 >
139146 <ZoomIn className="h-4 w-4" />
140147 </Button>
141148
src/components/WebsiteGallery.tsx +24 −26
@@ -1,11 +1,11 @@
1 'use client'
1 "use client"
22
3 import { useState, useRef, useEffect } from 'react'
4 import { motion, AnimatePresence } from 'framer-motion'
5 import { Card, CardContent } from '@/components/ui/card'
6 import { Button } from '@/components/ui/button'
7 import { ExternalLink, Maximize2, ChevronLeft, ChevronRight, X } from 'lucide-react'
8 import WebsiteFrame from './WebsiteFrame'
3 import { AnimatePresence, motion } from "framer-motion"
4 import { ChevronLeft, ChevronRight, ExternalLink, Maximize2, X } from "lucide-react"
5 import { useEffect, useRef, useState } from "react"
6 import { Button } from "@/components/ui/button"
7 import { Card, CardContent } from "@/components/ui/card"
8 import WebsiteFrame from "./WebsiteFrame"
99
1010 interface Website {
1111 id: string
@@ -31,7 +31,7 @@ function chunkArray<T>(array: T[], size: number): T[][] {
3131 export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
3232 // Responsive chunking based on screen size
3333 const [itemsPerSection, setItemsPerSection] = useState(3)
34
34
3535 useEffect(() => {
3636 const updateSectionSize = () => {
3737 if (window.innerWidth < 640) {
@@ -42,21 +42,21 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
4242 setItemsPerSection(3) // Show 3 items per section on desktop
4343 }
4444 }
45
45
4646 updateSectionSize()
47 window.addEventListener('resize', updateSectionSize)
48 return () => window.removeEventListener('resize', updateSectionSize)
47 window.addEventListener("resize", updateSectionSize)
48 return () => window.removeEventListener("resize", updateSectionSize)
4949 }, [])
50
50
5151 // Group websites into sections based on responsive size
5252 const websiteSections = chunkArray(websites, itemsPerSection)
5353
5454 const [activeSectionIndex, setActiveSectionIndex] = useState(0)
5555 const [expandedView, setExpandedView] = useState<Website | null>(null)
5656 const [isLoading, setIsLoading] = useState<Record<string, boolean>>({})
57 const [touchStart, setTouchStart] = useState<number | null>(null)
58 const [touchEnd, setTouchEnd] = useState<number | null>(null)
59 const [isMobile, setIsMobile] = useState(false)
57 const [_touchStart, _setTouchStart] = useState<number | null>(null)
58 const [_touchEnd, _setTouchEnd] = useState<number | null>(null)
59 const [_isMobile, setIsMobile] = useState(false)
6060 const carouselRef = useRef<HTMLDivElement>(null)
6161
6262 // Check if we're on mobile
@@ -66,10 +66,10 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
6666 }
6767
6868 checkMobile()
69 window.addEventListener('resize', checkMobile)
69 window.addEventListener("resize", checkMobile)
7070
7171 return () => {
72 window.removeEventListener('resize', checkMobile)
72 window.removeEventListener("resize", checkMobile)
7373 }
7474 }, [])
7575
@@ -98,7 +98,7 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
9898 }, 8000)
9999
100100 return () => clearInterval(interval)
101 }, [activeSectionIndex])
101 }, [handleNext])
102102
103103 return (
104104 <div className="w-full">
@@ -189,7 +189,7 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
189189 className="h-8 w-8 opacity-80 hover:opacity-100 bg-blue-900/70 hover:bg-blue-800/70 border border-blue-500 text-blue-300"
190190 onClick={(e) => {
191191 e.stopPropagation()
192 window.open(website.url, '_blank')
192 window.open(website.url, "_blank")
193193 }}
194194 aria-label="Open in new tab"
195195 >
@@ -231,29 +231,27 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
231231 key={index}
232232 className={`w-2.5 h-2.5 rounded-full transition-all ${
233233 index === activeSectionIndex
234 ? 'bg-blue-500 scale-125'
235 : 'bg-blue-900 hover:bg-blue-700'
234 ? "bg-blue-500 scale-125"
235 : "bg-blue-900 hover:bg-blue-700"
236236 }`}
237237 onClick={() => setActiveSectionIndex(index)}
238238 aria-label={`View section ${index + 1}`}
239239 />
240240 ))}
241241 </div>
242
243
244242 </motion.div>
245243
246244 {/* Expanded View Modal */}
247245 <AnimatePresence>
248246 {expandedView && (
249 <motion.div
247 <motion.div
250248 initial={{ opacity: 0 }}
251249 animate={{ opacity: 1 }}
252250 exit={{ opacity: 0 }}
253251 transition={{ duration: 0.3 }}
254252 className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 backdrop-blur-sm"
255253 >
256 <motion.div
254 <motion.div
257255 initial={{ scale: 0.9, opacity: 0 }}
258256 animate={{ scale: 1, opacity: 1 }}
259257 exit={{ scale: 0.9, opacity: 0 }}
@@ -266,7 +264,7 @@ export default function WebsiteGallery({ websites }: WebsiteGalleryProps) {
266264 <Button
267265 size="icon"
268266 variant="ghost"
269 onClick={() => window.open(expandedView.url, '_blank')}
267 onClick={() => window.open(expandedView.url, "_blank")}
270268 aria-label="Open in new tab"
271269 className="text-blue-300 hover:text-blue-100 hover:bg-blue-900/50"
272270 >
src/components/playground/CodeEditor.tsx +7 −5
@@ -1,11 +1,12 @@
1 'use client'
1 "use client"
22
3 import React, { useState } from 'react'
4 import dynamic from 'next/dynamic'
3 import dynamic from "next/dynamic"
4 import type React from "react"
5 import { useState } from "react"
56 import { Card } from "@/components/ui/card"
67
78 const DynamicCodeEditor = dynamic(
8 () => import('@uiw/react-textarea-code-editor').then((mod) => mod.default),
9 () => import("@uiw/react-textarea-code-editor").then((mod) => mod.default),
910 { ssr: false }
1011 )
1112
@@ -25,7 +26,8 @@ export const CodeEditor: React.FC = () => {
2526 style={{
2627 fontSize: 12,
2728 backgroundColor: "transparent",
28 fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
29 fontFamily:
30 "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
2931 }}
3032 className="min-h-[300px] text-white"
3133 />
src/components/playground/ColorPicker.tsx +8 −8
@@ -1,6 +1,7 @@
1 'use client'
1 "use client"
22
3 import React, { useState } from 'react'
3 import type React from "react"
4 import { useState } from "react"
45 import { Card } from "@/components/ui/card"
56 import { Input } from "@/components/ui/input"
67 import { Label } from "@/components/ui/label"
@@ -11,11 +12,10 @@ export const ColorPicker: React.FC = () => {
1112 return (
1213 <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4">
1314 <div className="flex flex-col items-center space-y-4">
14 <div
15 className="w-32 h-32 rounded-full"
16 style={{ backgroundColor: color }}
17 />
18 <Label htmlFor="color-input" className="text-white">Select a color:</Label>
15 <div className="w-32 h-32 rounded-full" style={{ backgroundColor: color }} />
16 <Label htmlFor="color-input" className="text-white">
17 Select a color:
18 </Label>
1919 <Input
2020 id="color-input"
2121 type="color"
@@ -27,4 +27,4 @@ export const ColorPicker: React.FC = () => {
2727 </div>
2828 </Card>
2929 )
30 }↵
30 }
src/components/playground/FunctionCreator.tsx +149 −104
@@ -1,18 +1,19 @@
1 'use client'
1 "use client"
22
3 import React, { useState } from 'react'
4 import dynamic from 'next/dynamic'
5 import { Card } from "@/components/ui/card"
3 import { ClockIcon, CodeIcon, PlayIcon, SettingsIcon, XIcon } from "lucide-react"
4 import dynamic from "next/dynamic"
5 import type React from "react"
6 import { useState } from "react"
67 import { Button } from "@/components/ui/button"
7 import { PlayIcon, ClockIcon, CodeIcon, SettingsIcon, XIcon } from 'lucide-react'
8 import { toast } from '@/hooks/use-toast'
8 import { Card } from "@/components/ui/card"
9 import { toast } from "@/hooks/use-toast"
910
1011 const DynamicCodeEditor = dynamic(
11 () => import('@uiw/react-textarea-code-editor').then((mod) => mod.default),
12 () => import("@uiw/react-textarea-code-editor").then((mod) => mod.default),
1213 { ssr: false }
1314 )
1415
15 type Language = 'typescript' | 'python';
16 type Language = "typescript" | "python"
1617
1718 const defaultCode = {
1819 typescript: `// TypeScript Example
@@ -57,148 +58,175 @@ print(asyncio.run(fetch_data()))
5758
5859 # Access environment variables
5960 import os
60 print(f"Environment: {os.getenv('ENVIRONMENT', 'development')}")`
61 };
61 print(f"Environment: {os.getenv('ENVIRONMENT', 'development')}")`,
62 }
6263
6364 interface ExecutionResult {
64 success: boolean;
65 output?: string;
66 error?: string;
67 exitCode?: number;
65 success: boolean
66 output?: string
67 error?: string
68 exitCode?: number
6869 }
6970
7071 interface EnvVar {
71 key: string;
72 value: string;
72 key: string
73 value: string
7374 }
7475
7576 export const FunctionCreator: React.FC = () => {
76 const [language, setLanguage] = useState<Language>('typescript');
77 const [code, setCode] = useState(defaultCode.typescript);
78 const [isExecuting, setIsExecuting] = useState(false);
79 const [isScheduling, setIsScheduling] = useState(false);
80 const [output, setOutput] = useState<string>('');
81 const [cronExpression, setCronExpression] = useState('0 * * * *'); // Every hour
82 const [showScheduler, setShowScheduler] = useState(false);
83 const [showEnvVars, setShowEnvVars] = useState(false);
84 const [envVars, setEnvVars] = useState<EnvVar[]>([{ key: '', value: '' }]);
77 const [language, setLanguage] = useState<Language>("typescript")
78 const [code, setCode] = useState(defaultCode.typescript)
79 const [isExecuting, setIsExecuting] = useState(false)
80 const [isScheduling, setIsScheduling] = useState(false)
81 const [output, setOutput] = useState<string>("")
82 const [functionName, setFunctionName] = useState("")
83 const [cronExpression, setCronExpression] = useState("0 * * * *") // Every hour
84 const [showScheduler, setShowScheduler] = useState(false)
85 const [showEnvVars, setShowEnvVars] = useState(false)
86 const [envVars, setEnvVars] = useState<EnvVar[]>([{ key: "", value: "" }])
8587
8688 const handleLanguageChange = (newLanguage: Language) => {
87 setLanguage(newLanguage);
88 setCode(defaultCode[newLanguage]);
89 setOutput('');
90 };
89 setLanguage(newLanguage)
90 setCode(defaultCode[newLanguage])
91 setOutput("")
92 }
9193
9294 const addEnvVar = () => {
93 setEnvVars([...envVars, { key: '', value: '' }]);
94 };
95 setEnvVars([...envVars, { key: "", value: "" }])
96 }
9597
9698 const removeEnvVar = (index: number) => {
97 setEnvVars(envVars.filter((_, i) => i !== index));
98 };
99 setEnvVars(envVars.filter((_, i) => i !== index))
100 }
99101
100 const updateEnvVar = (index: number, field: 'key' | 'value', value: string) => {
101 const newEnvVars = [...envVars];
102 newEnvVars[index][field] = value;
103 setEnvVars(newEnvVars);
104 };
102 const updateEnvVar = (index: number, field: "key" | "value", value: string) => {
103 const newEnvVars = [...envVars]
104 newEnvVars[index][field] = value
105 setEnvVars(newEnvVars)
106 }
105107
106108 const getEnvVarsObject = () => {
107 return envVars.reduce((acc, { key, value }) => {
108 if (key.trim()) {
109 acc[key.trim()] = value;
110 }
111 return acc;
112 }, {} as Record<string, string>);
113 };
109 return envVars.reduce(
110 (acc, { key, value }) => {
111 if (key.trim()) {
112 acc[key.trim()] = value
113 }
114 return acc
115 },
116 {} as Record<string, string>
117 )
118 }
114119
115120 const handleRun = async () => {
116 setIsExecuting(true);
117 setOutput('Executing code...');
121 setIsExecuting(true)
122 setOutput("Executing code...")
118123
119124 try {
120 const response = await fetch('/api/sandbox/execute', {
121 method: 'POST',
125 const response = await fetch("/api/sandbox/execute", {
126 method: "POST",
122127 headers: {
123 'Content-Type': 'application/json',
128 "Content-Type": "application/json",
124129 },
125130 body: JSON.stringify({
126131 code,
127132 language,
128133 envVars: getEnvVarsObject(),
129134 }),
130 });
135 })
131136
132 const result: ExecutionResult = await response.json();
137 const result: ExecutionResult = await response.json()
133138
134139 if (result.success) {
135 setOutput(result.output || 'Code executed successfully!');
140 setOutput(result.output || "Code executed successfully!")
136141 toast({
137142 title: "Success",
138143 description: "Code executed successfully",
139 });
144 })
140145 } else {
141 setOutput(`Error (exit code ${result.exitCode}):\n${result.error || 'Unknown error'}`);
146 setOutput(`Error (exit code ${result.exitCode}):\n${result.error || "Unknown error"}`)
142147 toast({
143148 title: "Execution Error",
144149 description: "Check the output panel for details",
145150 variant: "destructive",
146 });
151 })
147152 }
148153 } catch (error) {
149 const errorMessage = error instanceof Error ? error.message : 'Unknown error';
150 setOutput(`Failed to execute code: ${errorMessage}`);
154 const errorMessage = error instanceof Error ? error.message : "Unknown error"
155 setOutput(`Failed to execute code: ${errorMessage}`)
151156 toast({
152157 title: "Error",
153158 description: "Failed to execute code",
154159 variant: "destructive",
155 });
160 })
156161 } finally {
157 setIsExecuting(false);
162 setIsExecuting(false)
158163 }
159 };
164 }
160165
161166 const handleSchedule = async () => {
162 setIsScheduling(true);
167 if (!functionName.trim()) {
168 toast({
169 title: "Name Required",
170 description: "Please provide a name for this scheduled function",
171 variant: "destructive",
172 })
173 return
174 }
175
176 setIsScheduling(true)
163177
164178 try {
165 const response = await fetch('/api/sandbox/schedule', {
166 method: 'POST',
179 // Convert envVars array to object
180 const envVarsObject = envVars.reduce(
181 (acc, { key, value }) => {
182 if (key.trim()) {
183 acc[key] = value
184 }
185 return acc
186 },
187 {} as Record<string, string>
188 )
189
190 const response = await fetch("/api/sandbox/schedule", {
191 method: "POST",
167192 headers: {
168 'Content-Type': 'application/json',
193 "Content-Type": "application/json",
169194 },
170195 body: JSON.stringify({
196 name: functionName,
171197 code,
172198 language,
173199 cronExpression,
200 envVars: envVarsObject,
174201 }),
175 });
202 })
176203
177 const result = await response.json();
204 const result = await response.json()
178205
179206 if (result.success) {
180207 toast({
181 title: "Scheduled",
182 description: `Function scheduled with cron: ${cronExpression}`,
183 });
184 setShowScheduler(false);
208 title: "Scheduled Successfully",
209 description: `${functionName} will run ${cronExpression}`,
210 })
211 setShowScheduler(false)
212 setFunctionName("")
185213 } else {
186214 toast({
187215 title: "Scheduling Error",
188216 description: result.error || "Failed to schedule function",
189217 variant: "destructive",
190 });
218 })
191219 }
192 } catch (error) {
220 } catch (_error) {
193221 toast({
194222 title: "Error",
195223 description: "Failed to schedule function",
196224 variant: "destructive",
197 });
225 })
198226 } finally {
199 setIsScheduling(false);
227 setIsScheduling(false)
200228 }
201 };
229 }
202230
203231 return (
204232 <div className="space-y-4">
@@ -206,18 +234,18 @@ export const FunctionCreator: React.FC = () => {
206234 <div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
207235 <div className="flex flex-wrap gap-2">
208236 <Button
209 variant={language === 'typescript' ? 'default' : 'outline'}
237 variant={language === "typescript" ? "default" : "outline"}
210238 size="sm"
211 onClick={() => handleLanguageChange('typescript')}
239 onClick={() => handleLanguageChange("typescript")}
212240 className="transition-all duration-200"
213241 >
214242 <CodeIcon className="w-4 h-4 mr-2" />
215243 TypeScript
216244 </Button>
217245 <Button
218 variant={language === 'python' ? 'default' : 'outline'}
246 variant={language === "python" ? "default" : "outline"}
219247 size="sm"
220 onClick={() => handleLanguageChange('python')}
248 onClick={() => handleLanguageChange("python")}
221249 className="transition-all duration-200"
222250 >
223251 <CodeIcon className="w-4 h-4 mr-2" />
@@ -233,7 +261,7 @@ export const FunctionCreator: React.FC = () => {
233261 size="sm"
234262 >
235263 <PlayIcon className="w-4 h-4 mr-2" />
236 {isExecuting ? 'Running...' : 'Run'}
264 {isExecuting ? "Running..." : "Run"}
237265 </Button>
238266 <Button
239267 onClick={() => setShowEnvVars(!showEnvVars)}
@@ -268,14 +296,14 @@ export const FunctionCreator: React.FC = () => {
268296 <input
269297 type="text"
270298 value={envVar.key}
271 onChange={(e) => updateEnvVar(index, 'key', e.target.value)}
299 onChange={(e) => updateEnvVar(index, "key", e.target.value)}
272300 placeholder="KEY"
273301 className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
274302 />
275303 <input
276304 type="text"
277305 value={envVar.value}
278 onChange={(e) => updateEnvVar(index, 'value', e.target.value)}
306 onChange={(e) => updateEnvVar(index, "value", e.target.value)}
279307 placeholder="value"
280308 className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
281309 />
@@ -295,12 +323,7 @@ export const FunctionCreator: React.FC = () => {
295323 ))}
296324 </div>
297325 <div className="flex flex-col sm:flex-row gap-2">
298 <Button
299 onClick={addEnvVar}
300 variant="outline"
301 size="sm"
302 className="w-full sm:w-auto"
303 >
326 <Button onClick={addEnvVar} variant="outline" size="sm" className="w-full sm:w-auto">
304327 Add Variable
305328 </Button>
306329 <Button
@@ -321,6 +344,20 @@ export const FunctionCreator: React.FC = () => {
321344 <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4 animate-in slide-in-from-top-2 duration-200">
322345 <div className="space-y-3">
323346 <h3 className="text-lg font-semibold text-white">Schedule Function</h3>
347
348 <div className="space-y-2">
349 <label className="text-sm text-gray-300">
350 Function Name
351 </label>
352 <input
353 type="text"
354 value={functionName}
355 onChange={(e) => setFunctionName(e.target.value)}
356 placeholder="My Daily Task"
357 className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
358 />
359 </div>
360
324361 <div className="space-y-2">
325362 <label className="text-sm text-gray-300">
326363 Cron Expression
@@ -336,10 +373,20 @@ export const FunctionCreator: React.FC = () => {
336373 <div className="text-xs text-gray-400 space-y-1">
337374 <p>Examples:</p>
338375 <ul className="list-disc list-inside space-y-0.5">
339 <li><code className="bg-gray-800 px-1 rounded">0 * * * *</code> - Every hour</li>
340 <li><code className="bg-gray-800 px-1 rounded">*/15 * * * *</code> - Every 15 minutes</li>
341 <li><code className="bg-gray-800 px-1 rounded">0 0 * * *</code> - Daily at midnight</li>
342 <li><code className="bg-gray-800 px-1 rounded">0 9 * * 1</code> - Every Monday at 9 AM</li>
376 <li>
377 <code className="bg-gray-800 px-1 rounded">0 * * * *</code> - Every hour
378 </li>
379 <li>
380 <code className="bg-gray-800 px-1 rounded">*/15 * * * *</code> - Every 15
381 minutes
382 </li>
383 <li>
384 <code className="bg-gray-800 px-1 rounded">0 0 * * *</code> - Daily at midnight
385 </li>
386 <li>
387 <code className="bg-gray-800 px-1 rounded">0 9 * * 1</code> - Every Monday at 9
388 AM
389 </li>
343390 </ul>
344391 </div>
345392 </div>
@@ -350,13 +397,9 @@ export const FunctionCreator: React.FC = () => {
350397 className="bg-blue-600 hover:bg-blue-700"
351398 size="sm"
352399 >
353 {isScheduling ? 'Scheduling...' : 'Schedule Function'}
400 {isScheduling ? "Scheduling..." : "Schedule Function"}
354401 </Button>
355 <Button
356 onClick={() => setShowScheduler(false)}
357 variant="outline"
358 size="sm"
359 >
402 <Button onClick={() => setShowScheduler(false)} variant="outline" size="sm">
360403 Cancel
361404 </Button>
362405 </div>
@@ -368,15 +411,16 @@ export const FunctionCreator: React.FC = () => {
368411 <Card className="bg-black/50 backdrop-blur-md border-blue-500 p-4 overflow-hidden">
369412 <DynamicCodeEditor
370413 value={code}
371 language={language === 'typescript' ? 'tsx' : 'py'}
372 placeholder={`Enter ${language === 'typescript' ? 'TypeScript' : 'Python'} code...`}
414 language={language === "typescript" ? "tsx" : "py"}
415 placeholder={`Enter ${language === "typescript" ? "TypeScript" : "Python"} code...`}
373416 onChange={(evn) => setCode(evn.target.value)}
374417 padding={15}
375418 style={{
376419 fontSize: 14,
377420 backgroundColor: "transparent",
378 fontFamily: 'ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace',
379 minHeight: '300px',
421 fontFamily:
422 "ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
423 minHeight: "300px",
380424 }}
381425 className="text-white rounded overflow-auto"
382426 />
@@ -402,7 +446,8 @@ export const FunctionCreator: React.FC = () => {
402446 <p className="flex items-start gap-2">
403447 <span className="text-blue-400">ℹ️</span>
404448 <span>
405 Your code runs in an isolated, secure environment. Each execution is ephemeral and cannot access external resources without explicit configuration.
449 Your code runs in an isolated, secure environment. Each execution is ephemeral and
450 cannot access external resources without explicit configuration.
406451 </span>
407452 </p>
408453 </div>
src/components/playground/ObjectViewer.tsx +14 −7
@@ -1,9 +1,10 @@
1 'use client'
1 "use client"
22
3 import React, { Suspense, useRef } from 'react'
4 import { Canvas, useFrame, useLoader } from '@react-three/fiber'
5 import { OrbitControls, PerspectiveCamera, Environment } from '@react-three/drei'
6 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
3 import { Environment, OrbitControls, PerspectiveCamera } from "@react-three/drei"
4 import { Canvas, useLoader } from "@react-three/fiber"
5 import type React from "react"
6 import { Suspense, useRef } from "react"
7 import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"
78 import { Card } from "@/components/ui/card"
89
910 const LoadingSpinner: React.FC = () => (
@@ -14,7 +15,7 @@ const LoadingSpinner: React.FC = () => (
1415
1516 export const ObjectViewer: React.FC = () => {
1617 const ChicagoModel = () => {
17 const gltf = useLoader(GLTFLoader, '/chicago.glb')
18 const gltf = useLoader(GLTFLoader, "/chicago.glb")
1819 const modelRef = useRef()
1920
2021 return <primitive object={gltf.scene} ref={modelRef} scale={[0.01, 0.01, 0.01]} />
@@ -31,7 +32,13 @@ export const ObjectViewer: React.FC = () => {
3132 <PerspectiveCamera makeDefault position={[0, 50, 100]} />
3233 <Environment preset="city" background={false} />
3334 <ChicagoModel />
34 <OrbitControls target={[0, 0, 0]} maxPolarAngle={Math.PI / 2} enableZoom={true} minDistance={1} maxDistance={1000} />
35 <OrbitControls
36 target={[0, 0, 0]}
37 maxPolarAngle={Math.PI / 2}
38 enableZoom={true}
39 minDistance={1}
40 maxDistance={1000}
41 />
3542 </Canvas>
3643 </Suspense>
3744 </div>
src/components/ui/avatar.tsx +2 −5
@@ -1,7 +1,7 @@
11 "use client"
22
3 import * as React from "react"
43 import * as AvatarPrimitive from "@radix-ui/react-avatar"
4 import * as React from "react"
55
66 import { cn } from "@/lib/utils"
77
@@ -11,10 +11,7 @@ const Avatar = React.forwardRef<
1111 >(({ className, ...props }, ref) => (
1212 <AvatarPrimitive.Root
1313 ref={ref}
14 className={cn(
15 "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
16 className
17 )}
14 className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
1815 {...props}
1916 />
2017 ))
src/components/ui/button.tsx +5 −12
@@ -1,6 +1,6 @@
1 import * as React from "react"
21 import { Slot } from "@radix-ui/react-slot"
32 import { cva, type VariantProps } from "class-variance-authority"
3 import * as React from "react"
44
55 import { cn } from "@/lib/utils"
66
@@ -9,14 +9,11 @@ const buttonVariants = cva(
99 {
1010 variants: {
1111 variant: {
12 default:
13 "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 destructive:
15 "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
12 default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13 destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
1614 outline:
1715 "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 secondary:
19 "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
16 secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
2017 ghost: "hover:bg-accent hover:text-accent-foreground",
2118 link: "text-primary underline-offset-4 hover:underline",
2219 },
@@ -44,11 +41,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
4441 ({ className, variant, size, asChild = false, ...props }, ref) => {
4542 const Comp = asChild ? Slot : "button"
4643 return (
47 <Comp
48 className={cn(buttonVariants({ variant, size, className }))}
49 ref={ref}
50 {...props}
51 />
44 <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
5245 )
5346 }
5447 )
src/components/ui/card.tsx +34 −54
@@ -2,75 +2,55 @@ import * as React from "react"
22
33 import { cn } from "@/lib/utils"
44
5 const Card = React.forwardRef<
6 HTMLDivElement,
7 React.HTMLAttributes<HTMLDivElement>
8 >(({ className, ...props }, ref) => (
9 <div
10 ref={ref}
11 className={cn(
12 "rounded-xl border bg-card text-card-foreground shadow",
13 className
14 )}
15 {...props}
16 />
17 ))
5 const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6 ({ className, ...props }, ref) => (
7 <div
8 ref={ref}
9 className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
10 {...props}
11 />
12 )
13 )
1814 Card.displayName = "Card"
1915
20 const CardHeader = React.forwardRef<
21 HTMLDivElement,
22 React.HTMLAttributes<HTMLDivElement>
23 >(({ className, ...props }, ref) => (
24 <div
25 ref={ref}
26 className={cn("flex flex-col space-y-1.5 p-6", className)}
27 {...props}
28 />
29 ))
16 const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17 ({ className, ...props }, ref) => (
18 <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
19 )
20 )
3021 CardHeader.displayName = "CardHeader"
3122
32 const CardTitle = React.forwardRef<
33 HTMLParagraphElement,
34 React.HTMLAttributes<HTMLHeadingElement>
35 >(({ className, ...props }, ref) => (
36 <h3
37 ref={ref}
38 className={cn("font-semibold leading-none tracking-tight", className)}
39 {...props}
40 />
41 ))
23 const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
24 ({ className, ...props }, ref) => (
25 <h3
26 ref={ref}
27 className={cn("font-semibold leading-none tracking-tight", className)}
28 {...props}
29 />
30 )
31 )
4232 CardTitle.displayName = "CardTitle"
4333
4434 const CardDescription = React.forwardRef<
4535 HTMLParagraphElement,
4636 React.HTMLAttributes<HTMLParagraphElement>
4737 >(({ className, ...props }, ref) => (
48 <p
49 ref={ref}
50 className={cn("text-sm text-muted-foreground", className)}
51 {...props}
52 />
38 <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
5339 ))
5440 CardDescription.displayName = "CardDescription"
5541
56 const CardContent = React.forwardRef<
57 HTMLDivElement,
58 React.HTMLAttributes<HTMLDivElement>
59 >(({ className, ...props }, ref) => (
60 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61 ))
42 const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
43 ({ className, ...props }, ref) => (
44 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
45 )
46 )
6247 CardContent.displayName = "CardContent"
6348
64 const CardFooter = React.forwardRef<
65 HTMLDivElement,
66 React.HTMLAttributes<HTMLDivElement>
67 >(({ className, ...props }, ref) => (
68 <div
69 ref={ref}
70 className={cn("flex items-center p-6 pt-0", className)}
71 {...props}
72 />
73 ))
49 const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
50 ({ className, ...props }, ref) => (
51 <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
52 )
53 )
7454 CardFooter.displayName = "CardFooter"
7555
7656 export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/dialog.tsx +6 −24
@@ -1,8 +1,8 @@
11 "use client"
22
3 import * as React from "react"
43 import * as DialogPrimitive from "@radix-ui/react-dialog"
54 import { Cross2Icon } from "@radix-ui/react-icons"
5 import * as React from "react"
66
77 import { cn } from "@/lib/utils"
88
@@ -53,29 +53,14 @@ const DialogContent = React.forwardRef<
5353 ))
5454 DialogContent.displayName = DialogPrimitive.Content.displayName
5555
56 const DialogHeader = ({
57 className,
58 ...props
59 }: React.HTMLAttributes<HTMLDivElement>) => (
60 <div
61 className={cn(
62 "flex flex-col space-y-1.5 text-center sm:text-left",
63 className
64 )}
65 {...props}
66 />
56 const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
57 <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
6758 )
6859 DialogHeader.displayName = "DialogHeader"
6960
70 const DialogFooter = ({
71 className,
72 ...props
73 }: React.HTMLAttributes<HTMLDivElement>) => (
61 const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
7462 <div
75 className={cn(
76 "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77 className
78 )}
63 className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
7964 {...props}
8065 />
8166 )
@@ -87,10 +72,7 @@ const DialogTitle = React.forwardRef<
8772 >(({ className, ...props }, ref) => (
8873 <DialogPrimitive.Title
8974 ref={ref}
90 className={cn(
91 "text-lg font-semibold leading-none tracking-tight",
92 className
93 )}
75 className={cn("text-lg font-semibold leading-none tracking-tight", className)}
9476 {...props}
9577 />
9678 ))
src/components/ui/input.tsx +1 −2
@@ -2,8 +2,7 @@ import * as React from "react"
22
33 import { cn } from "@/lib/utils"
44
5 export interface InputProps
6 extends React.InputHTMLAttributes<HTMLInputElement> {}
5 export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
76
87 const Input = React.forwardRef<HTMLInputElement, InputProps>(
98 ({ className, type, ...props }, ref) => {
src/components/ui/label.tsx +3 −8
@@ -1,8 +1,8 @@
11 "use client"
22
3 import * as React from "react"
43 import * as LabelPrimitive from "@radix-ui/react-label"
54 import { cva, type VariantProps } from "class-variance-authority"
5 import * as React from "react"
66
77 import { cn } from "@/lib/utils"
88
@@ -12,14 +12,9 @@ const labelVariants = cva(
1212
1313 const Label = React.forwardRef<
1414 React.ElementRef<typeof LabelPrimitive.Root>,
15 React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
16 VariantProps<typeof labelVariants>
15 React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
1716 >(({ className, ...props }, ref) => (
18 <LabelPrimitive.Root
19 ref={ref}
20 className={cn(labelVariants(), className)}
21 {...props}
22 />
17 <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
2318 ))
2419 Label.displayName = LabelPrimitive.Root.displayName
2520
src/components/ui/sheet.tsx +6 −25
@@ -1,9 +1,9 @@
11 "use client"
22
3 import * as React from "react"
43 import * as SheetPrimitive from "@radix-ui/react-dialog"
54 import { Cross2Icon } from "@radix-ui/react-icons"
65 import { cva, type VariantProps } from "class-variance-authority"
6 import * as React from "react"
77
88 import { cn } from "@/lib/utils"
99
@@ -59,11 +59,7 @@ const SheetContent = React.forwardRef<
5959 >(({ side = "right", className, children, ...props }, ref) => (
6060 <SheetPortal>
6161 <SheetOverlay />
62 <SheetPrimitive.Content
63 ref={ref}
64 className={cn(sheetVariants({ side }), className)}
65 {...props}
66 >
62 <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
6763 <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
6864 <Cross2Icon className="h-4 w-4" />
6965 <span className="sr-only">Close</span>
@@ -74,29 +70,14 @@ const SheetContent = React.forwardRef<
7470 ))
7571 SheetContent.displayName = SheetPrimitive.Content.displayName
7672
77 const SheetHeader = ({
78 className,
79 ...props
80 }: React.HTMLAttributes<HTMLDivElement>) => (
81 <div
82 className={cn(
83 "flex flex-col space-y-2 text-center sm:text-left",
84 className
85 )}
86 {...props}
87 />
73 const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
74 <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
8875 )
8976 SheetHeader.displayName = "SheetHeader"
9077
91 const SheetFooter = ({
92 className,
93 ...props
94 }: React.HTMLAttributes<HTMLDivElement>) => (
78 const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
9579 <div
96 className={cn(
97 "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
98 className
99 )}
80 className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
10081 {...props}
10182 />
10283 )
src/components/ui/spinner.tsx added +30
@@ -0,0 +1,30 @@
1 import { type HTMLAttributes } from "react"
2 import { cn } from "@/lib/utils"
3
4 interface SpinnerProps extends HTMLAttributes<HTMLDivElement> {
5 size?: "sm" | "md" | "lg"
6 }
7
8 export function Spinner({ size = "md", className, ...props }: SpinnerProps) {
9 const sizeClasses = {
10 sm: "w-4 h-4 border-2",
11 md: "w-8 h-8 border-3",
12 lg: "w-12 h-12 border-4",
13 }
14
15 return (
16 <div
17 className={cn(
18 "inline-block animate-spin rounded-full border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]",
19 sizeClasses[size],
20 className
21 )}
22 role="status"
23 {...props}
24 >
25 <span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">
26 Loading...
27 </span>
28 </div>
29 )
30 }
src/components/ui/tabs.tsx +1 −1
@@ -1,7 +1,7 @@
11 "use client"
22
3 import * as React from "react"
43 import * as TabsPrimitive from "@radix-ui/react-tabs"
4 import * as React from "react"
55
66 import { cn } from "@/lib/utils"
77
src/components/ui/textarea.tsx +1 −2
@@ -2,8 +2,7 @@ import * as React from "react"
22
33 import { cn } from "@/lib/utils"
44
5 export interface TextareaProps
6 extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
5 export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
76
87 const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
98 ({ className, ...props }, ref) => {
src/components/ui/toast.tsx +2 −3
@@ -1,9 +1,9 @@
11 "use client"
22
3 import * as React from "react"
43 import { Cross2Icon } from "@radix-ui/react-icons"
54 import * as ToastPrimitives from "@radix-ui/react-toast"
65 import { cva, type VariantProps } from "class-variance-authority"
6 import * as React from "react"
77
88 import { cn } from "@/lib/utils"
99
@@ -42,8 +42,7 @@ const toastVariants = cva(
4242
4343 const Toast = React.forwardRef<
4444 React.ElementRef<typeof ToastPrimitives.Root>,
45 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
46 VariantProps<typeof toastVariants>
45 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
4746 >(({ className, variant, ...props }, ref) => {
4847 return (
4948 <ToastPrimitives.Root
src/components/ui/toaster.tsx +17 −15
@@ -1,6 +1,5 @@
11 "use client"
22
3 import { useToast } from "@/hooks/use-toast"
43 import {
54 Toast,
65 ToastClose,
@@ -9,26 +8,29 @@ import {
98 ToastTitle,
109 ToastViewport,
1110 } from "@/components/ui/toast"
11 import { useToast } from "@/hooks/use-toast"
1212
1313 export function Toaster() {
1414 const { toasts } = useToast()
1515
1616 return (
1717 <ToastProvider>
18 {toasts.map(function ({ id, title, description, action, ...props }) {
19 return (
20 <Toast key={id} {...props} className="bg-black/50 backdrop-blur-md border-blue-500 hover:border-blue-300 transition-colors">
21 <div className="grid gap-1">
22 {title && <ToastTitle className="text-blue-300">{title}</ToastTitle>}
23 {description && (
24 <ToastDescription className="text-gray-300">{description}</ToastDescription>
25 )}
26 </div>
27 {action}
28 <ToastClose className="text-blue-300 hover:text-blue-100" />
29 </Toast>
30 )
31 })}
18 {toasts.map(({ id, title, description, action, ...props }) => (
19 <Toast
20 key={id}
21 {...props}
22 className="bg-black/50 backdrop-blur-md border-blue-500 hover:border-blue-300 transition-colors"
23 >
24 <div className="grid gap-1">
25 {title && <ToastTitle className="text-blue-300">{title}</ToastTitle>}
26 {description && (
27 <ToastDescription className="text-gray-300">{description}</ToastDescription>
28 )}
29 </div>
30 {action}
31 <ToastClose className="text-blue-300 hover:text-blue-100" />
32 </Toast>
33 ))}
3234 <ToastViewport />
3335 </ToastProvider>
3436 )
src/components/ui/tooltip.tsx +1 −1
@@ -1,7 +1,7 @@
11 "use client"
22
3 import * as React from "react"
43 import * as TooltipPrimitive from "@radix-ui/react-tooltip"
4 import * as React from "react"
55
66 import { cn } from "@/lib/utils"
77
src/hooks/use-toast.ts +3 −8
@@ -3,10 +3,7 @@
33 // Inspired by react-hot-toast library
44 import * as React from "react"
55
6 import type {
7 ToastActionElement,
8 ToastProps,
9 } from "@/components/ui/toast"
6 import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
107
118 const TOAST_LIMIT = 1
129 const TOAST_REMOVE_DELAY = 1000000
@@ -85,9 +82,7 @@ export const reducer = (state: State, action: Action): State => {
8582 case "UPDATE_TOAST":
8683 return {
8784 ...state,
88 toasts: state.toasts.map((t) =>
89 t.id === action.toast.id ? { ...t, ...action.toast } : t
90 ),
85 toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
9186 }
9287
9388 case "DISMISS_TOAST": {
@@ -182,7 +177,7 @@ function useToast() {
182177 listeners.splice(index, 1)
183178 }
184179 }
185 }, [state])
180 }, [])
186181
187182 return {
188183 ...state,
src/lib/logger.ts added +49
@@ -0,0 +1,49 @@
1 import pino from "pino"
2
3 const isDevelopment = process.env.NODE_ENV === "development"
4 const isProduction = process.env.NODE_ENV === "production"
5
6 // Create logger with environment-appropriate config
7 export const logger = pino({
8 level: process.env.LOG_LEVEL || (isDevelopment ? "debug" : "info"),
9
10 // Pretty print in development, structured JSON in production
11 ...(isDevelopment && {
12 transport: {
13 target: "pino-pretty",
14 options: {
15 colorize: true,
16 translateTime: "HH:MM:ss",
17 ignore: "pid,hostname",
18 },
19 },
20 }),
21
22 // Base fields for all logs
23 base: {
24 env: process.env.NODE_ENV,
25 },
26
27 // Redact sensitive fields
28 redact: {
29 paths: ["req.headers.authorization", "req.headers.cookie", "*.password", "*.token"],
30 remove: true,
31 },
32
33 // Serialize errors properly
34 serializers: {
35 err: pino.stdSerializers.err,
36 req: pino.stdSerializers.req,
37 res: pino.stdSerializers.res,
38 },
39 })
40
41 // Helper for API route logging
42 export function createRouteLogger(routeName: string) {
43 return logger.child({ route: routeName })
44 }
45
46 // Helper for scheduled function logging
47 export function createSchedulerLogger(functionId: string) {
48 return logger.child({ scheduler: true, functionId })
49 }
src/lib/projects.ts +6 −6
@@ -1,7 +1,7 @@
11 export const projectRepoMap: Record<string, string> = {
2 "qr_io": "qr-io/qr.io",
3 "csgomlbot": "trevato/csgomlbot",
4 "issue_completer": "trevato/issue_completer",
5 "csv_semantic_search": "trevato/csv_semantic_search",
6 "autonomous_plane": "trevato/autonomous_plane"
7 };
2 qr_io: "qr-io/qr.io",
3 csgomlbot: "trevato/csgomlbot",
4 issue_completer: "trevato/issue_completer",
5 csv_semantic_search: "trevato/csv_semantic_search",
6 autonomous_plane: "trevato/autonomous_plane",
7 }
src/lib/utils.ts +1 −1
@@ -1,4 +1,4 @@
1 import { clsx, type ClassValue } from "clsx"
1 import { type ClassValue, clsx } from "clsx"
22 import { twMerge } from "tailwind-merge"
33
44 export function cn(...inputs: ClassValue[]) {
src/styles/github-markdown.css +34 −11
@@ -1,32 +1,53 @@
11 .markdown-body {
2 font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
2 font-family:
3 -apple-system,
4 BlinkMacSystemFont,
5 Segoe UI,
6 Helvetica,
7 Arial,
8 sans-serif,
9 Apple Color Emoji,
10 Segoe UI Emoji;
311 font-size: 16px;
412 line-height: 1.5;
513 word-wrap: break-word;
614 }
715
8 .markdown-body h1, .markdown-body h2 {
9 padding-bottom: .3em;
16 .markdown-body h1,
17 .markdown-body h2 {
18 padding-bottom: 0.3em;
1019 border-bottom: 1px solid #21262d;
1120 }
1221
13 .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
22 .markdown-body h1,
23 .markdown-body h2,
24 .markdown-body h3,
25 .markdown-body h4,
26 .markdown-body h5,
27 .markdown-body h6 {
1428 margin-top: 24px;
1529 margin-bottom: 16px;
1630 font-weight: 600;
1731 line-height: 1.25;
1832 }
1933
20 .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details {
34 .markdown-body p,
35 .markdown-body blockquote,
36 .markdown-body ul,
37 .markdown-body ol,
38 .markdown-body dl,
39 .markdown-body table,
40 .markdown-body pre,
41 .markdown-body details {
2142 margin-top: 0;
2243 margin-bottom: 16px;
2344 }
2445
2546 .markdown-body code {
26 padding: .2em .4em;
47 padding: 0.2em 0.4em;
2748 margin: 0;
2849 font-size: 85%;
29 background-color: rgba(110,118,129,0.4);
50 background-color: rgba(110, 118, 129, 0.4);
3051 border-radius: 6px;
3152 }
3253
@@ -52,10 +73,11 @@
5273 .markdown-body blockquote {
5374 padding: 0 1em;
5475 color: #8b949e;
55 border-left: .25em solid #30363d;
76 border-left: 0.25em solid #30363d;
5677 }
5778
58 .markdown-body ul, .markdown-body ol {
79 .markdown-body ul,
80 .markdown-body ol {
5981 padding-left: 2em;
6082 }
6183
@@ -67,7 +89,8 @@
6789 border-collapse: collapse;
6890 }
6991
70 .markdown-body table th, .markdown-body table td {
92 .markdown-body table th,
93 .markdown-body table td {
7194 padding: 6px 13px;
7295 border: 1px solid #30363d;
7396 }
@@ -88,7 +111,7 @@
88111 }
89112
90113 .markdown-body hr {
91 height: .25em;
114 height: 0.25em;
92115 padding: 0;
93116 margin: 24px 0;
94117 background-color: #30363d;
src/utils/supabase/client.ts +1 −1
@@ -1,4 +1,4 @@
1 import { createBrowserClient } from '@supabase/ssr'
1 import { createBrowserClient } from "@supabase/ssr"
22
33 export function createClient() {
44 return createBrowserClient(
src/utils/supabase/middleware.ts +7 −8
@@ -1,5 +1,5 @@
1 import { createServerClient } from '@supabase/ssr'
2 import { NextResponse, type NextRequest } from 'next/server'
1 import { createServerClient } from "@supabase/ssr"
2 import { type NextRequest, NextResponse } from "next/server"
33
44 export async function updateSession(request: NextRequest) {
55 let supabaseResponse = NextResponse.next({
@@ -28,15 +28,14 @@ export async function updateSession(request: NextRequest) {
2828 )
2929
3030 // Get the user session
31 const { data: { user } } = await supabase.auth.getUser()
31 const {
32 data: { user },
33 } = await supabase.auth.getUser()
3234
3335 // Protect API routes that require authentication
34 if (request.nextUrl.pathname.startsWith('/api/sandbox/')) {
36 if (request.nextUrl.pathname.startsWith("/api/sandbox/")) {
3537 if (!user) {
36 return NextResponse.json(
37 { error: 'Authentication required' },
38 { status: 401 }
39 )
38 return NextResponse.json({ error: "Authentication required" }, { status: 401 })
4039 }
4140 }
4241
src/utils/supabase/server.ts +10 −10
@@ -1,9 +1,9 @@
11 "use server"
2 import { createServerClient, type CookieOptions } from "@supabase/ssr";
3 import { cookies } from "next/headers";
2 import { type CookieOptions, createServerClient } from "@supabase/ssr"
3 import { cookies } from "next/headers"
44
5 export async function createClient<Database>() {
6 const cookieStore = cookies();
5 export async function createClient<_Database>() {
6 const cookieStore = cookies()
77
88 // Create a server's supabase client with newly configured cookie,
99 // which could be used to maintain user's session
@@ -13,12 +13,12 @@ export async function createClient<Database>() {
1313 {
1414 cookies: {
1515 get(name: string) {
16 return cookieStore.get(name)?.value;
16 return cookieStore.get(name)?.value
1717 },
1818 set(name: string, value: string, options: CookieOptions) {
1919 try {
20 cookieStore.set({ name, value, ...options });
21 } catch (error) {
20 cookieStore.set({ name, value, ...options })
21 } catch (_error) {
2222 // The `set` method was called from a Server Component.
2323 // This can be ignored if you have middleware refreshing
2424 // user sessions.
@@ -26,8 +26,8 @@ export async function createClient<Database>() {
2626 },
2727 remove(name: string, options: CookieOptions) {
2828 try {
29 cookieStore.set({ name, value: "", ...options });
30 } catch (error) {
29 cookieStore.set({ name, value: "", ...options })
30 } catch (_error) {
3131 // The `delete` method was called from a Server Component.
3232 // This can be ignored if you have middleware refreshing
3333 // user sessions.
@@ -35,5 +35,5 @@ export async function createClient<Database>() {
3535 },
3636 },
3737 }
38 );
38 )
3939 }
supabase/functions/execute-scheduled/index.ts added +185
@@ -0,0 +1,185 @@
1 import { serve } from "https://deno.land/[email protected]/http/server.ts"
2 import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
3
4 const corsHeaders = {
5 "Access-Control-Allow-Origin": "*",
6 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
7 }
8
9 serve(async (req) => {
10 // Handle CORS preflight
11 if (req.method === "OPTIONS") {
12 return new Response("ok", { headers: corsHeaders })
13 }
14
15 try {
16 // Verify authorization
17 const authHeader = req.headers.get("authorization")
18 const expectedAuth = `Bearer ${Deno.env.get("CRON_SECRET")}`
19
20 if (authHeader !== expectedAuth) {
21 return new Response(JSON.stringify({ error: "Unauthorized" }), {
22 status: 401,
23 headers: { ...corsHeaders, "Content-Type": "application/json" },
24 })
25 }
26
27 // Create Supabase client with service role (automatic in Edge Functions)
28 const supabaseClient = createClient(
29 Deno.env.get("SUPABASE_URL") ?? "",
30 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
31 {
32 auth: {
33 autoRefreshToken: false,
34 persistSession: false,
35 },
36 }
37 )
38
39 // Fetch all functions that are due to run
40 const now = new Date().toISOString()
41 const { data: dueFunctions, error: fetchError } = await supabaseClient
42 .from("scheduled_functions")
43 .select("*")
44 .eq("enabled", true)
45 .lte("next_run_at", now)
46 .limit(50) // Process max 50 functions per minute
47
48 if (fetchError) {
49 throw new Error(`Failed to fetch due functions: ${fetchError.message}`)
50 }
51
52 if (!dueFunctions || dueFunctions.length === 0) {
53 return new Response(
54 JSON.stringify({
55 success: true,
56 message: "No functions due to run",
57 processed: 0,
58 }),
59 {
60 headers: { ...corsHeaders, "Content-Type": "application/json" },
61 }
62 )
63 }
64
65 // Process each function
66 const results = await Promise.allSettled(
67 dueFunctions.map((func) => executeFunction(func, supabaseClient))
68 )
69
70 const successful = results.filter((r) => r.status === "fulfilled").length
71 const failed = results.filter((r) => r.status === "rejected").length
72
73 return new Response(
74 JSON.stringify({
75 success: true,
76 message: `Processed ${dueFunctions.length} functions`,
77 processed: dueFunctions.length,
78 successful,
79 failed,
80 }),
81 {
82 headers: { ...corsHeaders, "Content-Type": "application/json" },
83 }
84 )
85 } catch (error) {
86 console.error("Edge function error:", error)
87 return new Response(
88 JSON.stringify({
89 error: "Execution failed",
90 details: error instanceof Error ? error.message : "Unknown error",
91 }),
92 {
93 status: 500,
94 headers: { ...corsHeaders, "Content-Type": "application/json" },
95 }
96 )
97 }
98 })
99
100 async function executeFunction(func: any, supabase: any) {
101 const startTime = Date.now()
102 let status: "success" | "error" | "timeout" = "error"
103 let output: string | null = null
104 let error: string | null = null
105
106 try {
107 // Execute the function via sandbox API
108 const appUrl = Deno.env.get("NEXT_PUBLIC_APP_URL") || "https://trevato.dev"
109 const response = await fetch(`${appUrl}/api/sandbox/execute`, {
110 method: "POST",
111 headers: {
112 "Content-Type": "application/json",
113 },
114 body: JSON.stringify({
115 code: func.code,
116 language: func.language,
117 envVars: func.env_vars || {},
118 }),
119 signal: AbortSignal.timeout(5 * 60 * 1000), // 5 minute timeout
120 })
121
122 const result = await response.json()
123
124 if (response.ok && result.success) {
125 status = "success"
126 output = result.output || result.result || "Execution completed"
127 } else {
128 status = "error"
129 error = result.error || result.details || "Execution failed"
130 }
131 } catch (err) {
132 if (err instanceof Error) {
133 if (err.name === "TimeoutError" || err.name === "AbortError") {
134 status = "timeout"
135 error = "Execution timed out after 5 minutes"
136 } else {
137 status = "error"
138 error = err.message
139 }
140 } else {
141 status = "error"
142 error = "Unknown execution error"
143 }
144 }
145
146 const duration = Date.now() - startTime
147
148 // Calculate next run time using cron-parser
149 let nextRun: string
150 try {
151 // Use dynamic import for cron-parser in Deno
152 const { parseExpression } = await import("npm:[email protected]")
153 const interval = parseExpression(func.cron_expression)
154 nextRun = interval.next().toDate().toISOString()
155 } catch (_err) {
156 // If cron parsing fails, disable the function
157 await supabase
158 .from("scheduled_functions")
159 .update({ enabled: false })
160 .eq("id", func.id)
161
162 throw new Error("Invalid cron expression, function disabled")
163 }
164
165 // Update function with next run time
166 await supabase
167 .from("scheduled_functions")
168 .update({
169 last_run_at: new Date().toISOString(),
170 next_run_at: nextRun,
171 execution_count: func.execution_count + 1,
172 })
173 .eq("id", func.id)
174
175 // Log execution history
176 await supabase.from("execution_history").insert({
177 function_id: func.id,
178 status,
179 output: output?.substring(0, 10000), // Limit output size
180 error: error?.substring(0, 1000), // Limit error size
181 duration_ms: duration,
182 })
183
184 return { functionId: func.id, status, duration }
185 }
supabase/migrations/20251123_create_scheduled_functions.sql added +150
@@ -0,0 +1,150 @@
1 -- Create scheduled_functions table
2 create table if not exists public.scheduled_functions (
3 id uuid primary key default gen_random_uuid(),
4 user_id uuid references auth.users(id) on delete cascade not null,
5 name text not null,
6 code text not null,
7 language text not null check (language in ('typescript', 'python')),
8 cron_expression text not null,
9 env_vars jsonb default '{}'::jsonb,
10 enabled boolean default true,
11 created_at timestamp with time zone default now(),
12 updated_at timestamp with time zone default now(),
13 last_run_at timestamp with time zone,
14 next_run_at timestamp with time zone,
15 execution_count integer default 0,
16
17 constraint name_not_empty check (char_length(trim(name)) > 0),
18 constraint code_not_empty check (char_length(trim(code)) > 0),
19 constraint valid_cron check (cron_expression ~ '^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$')
20 );
21
22 -- Create execution_history table
23 create table if not exists public.execution_history (
24 id uuid primary key default gen_random_uuid(),
25 function_id uuid references public.scheduled_functions(id) on delete cascade not null,
26 executed_at timestamp with time zone default now(),
27 status text not null check (status in ('success', 'error', 'timeout')),
28 output text,
29 error text,
30 duration_ms integer,
31
32 constraint valid_duration check (duration_ms >= 0)
33 );
34
35 -- Create indexes for performance
36 create index if not exists idx_scheduled_functions_user_id on public.scheduled_functions(user_id);
37 create index if not exists idx_scheduled_functions_next_run on public.scheduled_functions(next_run_at) where enabled = true;
38 create index if not exists idx_execution_history_function_id on public.execution_history(function_id);
39 create index if not exists idx_execution_history_executed_at on public.execution_history(executed_at desc);
40
41 -- Enable Row Level Security
42 alter table public.scheduled_functions enable row level security;
43 alter table public.execution_history enable row level security;
44
45 -- RLS Policies for scheduled_functions
46 create policy "Users can view their own scheduled functions"
47 on public.scheduled_functions for select
48 using (auth.uid() = user_id);
49
50 create policy "Users can create their own scheduled functions"
51 on public.scheduled_functions for insert
52 with check (auth.uid() = user_id);
53
54 create policy "Users can update their own scheduled functions"
55 on public.scheduled_functions for update
56 using (auth.uid() = user_id);
57
58 create policy "Users can delete their own scheduled functions"
59 on public.scheduled_functions for delete
60 using (auth.uid() = user_id);
61
62 -- RLS Policies for execution_history (read through scheduled_functions ownership)
63 create policy "Users can view execution history for their functions"
64 on public.execution_history for select
65 using (
66 exists (
67 select 1 from public.scheduled_functions
68 where scheduled_functions.id = execution_history.function_id
69 and scheduled_functions.user_id = auth.uid()
70 )
71 );
72
73 -- Function to update updated_at timestamp
74 create or replace function public.handle_updated_at()
75 returns trigger as $$
76 begin
77 new.updated_at = now();
78 return new;
79 end;
80 $$ language plpgsql;
81
82 -- Trigger to automatically update updated_at
83 create trigger set_updated_at
84 before update on public.scheduled_functions
85 for each row
86 execute function public.handle_updated_at();
87
88 -- Function to calculate next run time from cron expression
89 create or replace function public.calculate_next_run(cron_expr text, from_time timestamp with time zone default now())
90 returns timestamp with time zone as $$
91 declare
92 parts text[];
93 minute_part text;
94 hour_part text;
95 next_run timestamp with time zone;
96 current_minute int;
97 current_hour int;
98 target_minute int;
99 target_hour int;
100 begin
101 -- Parse cron expression (minute hour day month weekday)
102 parts := string_to_array(cron_expr, ' ');
103 minute_part := parts[1];
104 hour_part := parts[2];
105
106 -- Start from the next minute
107 next_run := date_trunc('minute', from_time) + interval '1 minute';
108
109 -- Simple implementation: only handle minute and hour
110 -- For full cron support, you'd use a library like pg_cron
111
112 -- If specific minute is set (not * or */n)
113 if minute_part ~ '^\d+$' then
114 target_minute := minute_part::int;
115 current_minute := extract(minute from next_run)::int;
116
117 if current_minute > target_minute then
118 -- Move to next hour
119 next_run := next_run + interval '1 hour';
120 end if;
121
122 next_run := date_trunc('hour', next_run) + (target_minute || ' minutes')::interval;
123 end if;
124
125 -- If specific hour is set (not * or */n)
126 if hour_part ~ '^\d+$' then
127 target_hour := hour_part::int;
128 current_hour := extract(hour from next_run)::int;
129
130 if current_hour > target_hour or (current_hour = target_hour and minute_part ~ '^\d+$' and extract(minute from next_run)::int >= minute_part::int) then
131 -- Move to next day
132 next_run := next_run + interval '1 day';
133 end if;
134
135 next_run := date_trunc('day', next_run) + (target_hour || ' hours')::interval;
136
137 if minute_part ~ '^\d+$' then
138 next_run := next_run + (minute_part::int || ' minutes')::interval;
139 end if;
140 end if;
141
142 return next_run;
143 end;
144 $$ language plpgsql immutable;
145
146 -- Grant necessary permissions
147 grant usage on schema public to anon, authenticated;
148 grant all on public.scheduled_functions to authenticated;
149 grant all on public.execution_history to authenticated;
150 grant execute on function public.calculate_next_run to authenticated;
tailwind.config.ts +54 −54
@@ -1,63 +1,63 @@
1 import type { Config } from "tailwindcss";
1 import type { Config } from "tailwindcss"
22
33 const config: Config = {
4 darkMode: ["class"],
5 content: [
4 darkMode: ["class"],
5 content: [
66 "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
77 "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
88 "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
99 ],
1010 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 },
54 borderRadius: {
55 lg: 'var(--radius)',
56 md: 'calc(var(--radius) - 2px)',
57 sm: 'calc(var(--radius) - 4px)'
58 }
59 }
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 },
54 borderRadius: {
55 lg: "var(--radius)",
56 md: "calc(var(--radius) - 2px)",
57 sm: "calc(var(--radius) - 4px)",
58 },
59 },
6060 },
6161 plugins: [require("tailwindcss-animate")],
62 };
63 export default config;
62 }
63 export default config
test-results/.last-run.json +2 −4
@@ -1,6 +1,4 @@
11 {
22 "status": "failed",
3 "failedTests": [
4 "632d6c1a91c2a57dc030-1a0dbb6af9ef5c74ef9a"
5 ]
6 }↵
3 "failedTests": ["632d6c1a91c2a57dc030-1a0dbb6af9ef5c74ef9a"]
4 }
tsconfig.json +4 −17
@@ -1,10 +1,6 @@
11 {
22 "compilerOptions": {
3 "lib": [
4 "dom",
5 "dom.iterable",
6 "esnext"
7 ],
3 "lib": ["dom", "dom.iterable", "esnext"],
84 "allowJs": true,
95 "skipLibCheck": true,
106 "strict": true,
@@ -22,19 +18,10 @@
2218 }
2319 ],
2420 "paths": {
25 "@/*": [
26 "./src/*"
27 ]
21 "@/*": ["./src/*"]
2822 },
2923 "target": "ES2017"
3024 },
31 "include": [
32 "next-env.d.ts",
33 "**/*.ts",
34 "**/*.tsx",
35 ".next/types/**/*.ts"
36 ],
37 "exclude": [
38 "node_modules"
39 ]
25 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 "exclude": ["node_modules", "supabase/functions"]
4027 }
vercel.json added +8
@@ -0,0 +1,8 @@
1 {
2 "crons": [
3 {
4 "path": "/api/cron/execute-scheduled",
5 "schedule": "* * * * *"
6 }
7 ]
8 }
vitest.config.ts added +29
@@ -0,0 +1,29 @@
1 import path from "node:path"
2 import react from "@vitejs/plugin-react"
3 import { defineConfig } from "vitest/config"
4
5 export default defineConfig({
6 plugins: [react()],
7 test: {
8 environment: "jsdom",
9 globals: true,
10 setupFiles: ["./vitest.setup.ts"],
11 css: true,
12 coverage: {
13 provider: "v8",
14 reporter: ["text", "json", "html"],
15 exclude: [
16 "node_modules/",
17 "vitest.setup.ts",
18 "**/*.config.{js,ts}",
19 "**/types.ts",
20 "**/*.d.ts",
21 ],
22 },
23 },
24 resolve: {
25 alias: {
26 "@": path.resolve(__dirname, "./src"),
27 },
28 },
29 })
vitest.setup.ts added +1
@@ -0,0 +1,1 @@
1 import "@testing-library/jest-dom"

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs