eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'feature/dashboard' into develop

+3457 -1000
+12 -1
.env.local.example
··· 1 1 NEXT_PUBLIC_APP_URL=http://localhost:3000 2 2 3 + # Prelaunch feature flag. 4 + # When `true`, hide elements intended to ship only after launch. 5 + NEXT_PUBLIC_PRELAUNCH=false 6 + 7 + 3 8 # Stripe keys 4 9 # https://dashboard.stripe.com/apikeys 5 10 NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_12345 ··· 15 20 SUPABASE_SERVICE_ROLE_KEY=somekey 16 21 17 22 # https://dashboard.stripe.com/ 18 - NEXT_PUBLIC_STRIPE_PRICE_ID=price_randomhash 23 + NEXT_PUBLIC_STRIPE_PRICE_ID=price_randomhash 24 + 25 + # Optional: plan-specific Stripe Price IDs (recommended). Keys match package names. 26 + # If provided, the UI will show amounts from Stripe and checkout will use the correct Price per plan. 27 + NEXT_PUBLIC_STRIPE_PRICE_PERSONAL_ID=price_randomhash_personal 28 + NEXT_PUBLIC_STRIPE_PRICE_COMMUNITY_ID=price_randomhash_community 29 + NEXT_PUBLIC_STRIPE_PRICE_BUSINESS_ID=price_randomhash_business
+158
PDS-INTEGRATION-NOTES.md
··· 1 + ## PDS integration overview 2 + 3 + ### End-to-end flow 4 + 5 + - **Stripe checkout** 6 + - User selects a plan on the pricing page. 7 + - Plan context is passed via URL params (`auto_checkout`, `pds_plan`, `pds_disksize_gb`, etc.) through `/signup` or `/login` to `/dashboard`. 8 + - `DashboardClient` calls `createSubscriptionCheckout(priceId, options)` with: 9 + - `username` (optional, normalized) 10 + - `hostname` (optional, cleaned; falls back to `<username>.eny.k8s.frx.pub`) 11 + - `disksizeGb` (derived from plan or override) 12 + - `createSubscriptionCheckout` encodes this into Stripe Checkout metadata: 13 + - `user_id`, `user_email` 14 + - `pds_username` 15 + - `pds_hostname_base` 16 + - `pds_disksize_gb` 17 + 18 + - **Stripe webhook → provisioning** 19 + - Stripe sends `checkout.session.completed` to `/api/webhooks` (Vercel URL, optionally with `x-vercel-protection-bypass` query param). 20 + - The webhook: 21 + - Verifies the event using `STRIPE_WEBHOOK_SECRET`. 22 + - Stores `user_id ↔ stripe_customer_id` in `subscriptions` (minimal mapping). 23 + - Derives provisioning parameters: 24 + - `pds_username` (normalized from metadata or `user_email`) 25 + - `pds_hostname_base` (metadata or `<username>.eny.k8s.frx.pub`) 26 + - `disksizeGb` (metadata or `"10"`). 27 + - Calls `provisionPdsForUser` with: 28 + - `userId`, `userEmail` 29 + - `pdsUsername`, `pdsHostnameBase`, `disksizeGb`. 30 + 31 + - **`provisionPdsForUser`** 32 + - Uses Supabase admin client (`NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`) to read/write `pds_services`. 33 + - Idempotency: 34 + - If `pds_services` already has a row with a non-null `pds_service_id`, it skips redeploy. 35 + - If `status` is **not** in `{ "deploy_failed", "deploy_succeeded_no_id" }`, it also skips redeploy. 36 + - Otherwise it proceeds to deploy again. 37 + - Calls `POST https://k8s-pds.frx.pub/api/v1/deploy` with Bearer **`PDS_API_TOKEN`** and JSON body: 38 + 39 + ```json 40 + { 41 + "username": "<pdsUsername>", 42 + "password": "<generated base64url>", 43 + "email": "<userEmail>", 44 + "hostname": "<pdsHostnameBase>", 45 + "disksize": <number in GiB> 46 + } 47 + ``` 48 + 49 + - Backend currently expects `hostname` to be a bare FQDN (no scheme, no path). 50 + 51 + - Expects a 2xx response with JSON containing an id somewhere. Current backend returns: 52 + 53 + ```json 54 + { 55 + "...": "...", 56 + "serviceId": 800 57 + } 58 + ``` 59 + 60 + - The code extracts `maybeServiceId` from (in order): 61 + - `service_id` 62 + - `serviceId` 63 + - `id` 64 + - `service.id` 65 + - `data.id` 66 + - `data.serviceId` 67 + - It upserts into `pds_services`: 68 + - `user_id` 69 + - `pds_service_id` (number or `null`) 70 + - `hostname` 71 + - `status`: 72 + - `"provisioning"` when `pds_service_id` is set. 73 + - `"deploy_succeeded_no_id"` when deploy succeeded but no id was found. 74 + - `"deploy_failed"` on deploy error. 75 + 76 + ### Database schema (`pds_services`) 77 + 78 + ```sql 79 + CREATE TABLE IF NOT EXISTS pds_services ( 80 + user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, 81 + pds_service_id BIGINT, 82 + hostname TEXT, 83 + status TEXT NOT NULL DEFAULT 'provisioning', 84 + created_at TIMESTAMPTZ DEFAULT NOW(), 85 + updated_at TIMESTAMPTZ DEFAULT NOW() 86 + ); 87 + 88 + ALTER TABLE pds_services ENABLE ROW LEVEL SECURITY; 89 + 90 + DROP POLICY IF EXISTS "Users can view own pds services" ON pds_services; 91 + CREATE POLICY "Users can view own pds services" 92 + ON pds_services 93 + FOR SELECT 94 + USING (auth.uid() = user_id); 95 + 96 + CREATE OR REPLACE FUNCTION update_pds_services_updated_at_column() 97 + RETURNS TRIGGER AS $$ 98 + BEGIN 99 + NEW.updated_at = NOW(); 100 + RETURN NEW; 101 + END; 102 + $$ language 'plpgsql'; 103 + 104 + DROP TRIGGER IF EXISTS update_pds_services_updated_at ON pds_services; 105 + CREATE TRIGGER update_pds_services_updated_at 106 + BEFORE UPDATE ON pds_services 107 + FOR EACH ROW 108 + EXECUTE FUNCTION update_pds_services_updated_at_column(); 109 + ``` 110 + 111 + - One PDS per `user_id` (PK on `user_id`). 112 + - `status` values used today: `"provisioning"`, `"deploy_failed"`, `"deploy_succeeded_no_id"`. 113 + 114 + ### PDS service fetch (`app/api/pds/service/route.ts`) 115 + 116 + - Authenticates the current user with the normal Supabase client. 117 + - Reads `pds_services` row for `user_id`: 118 + - If no row or `pds_service_id` is `null`, returns `404` with a simple message. 119 + - Builds `GET` URL to PDS API: 120 + 121 + ```text 122 + GET https://k8s-pds.frx.pub/api/v1/service/{pds_service_id} 123 + ``` 124 + 125 + - Sends: 126 + - `Accept: application/json` 127 + - `X-Requested-With: XMLHttpRequest` 128 + - `Authorization: Bearer PDS_API_TOKEN` 129 + - If `content-type` is JSON: 130 + - Parses body. 131 + - Handles double-encoded JSON strings by `JSON.parse` when possible. 132 + - On non-2xx, returns `502` with `{ error, status, body }` for debugging. 133 + - On 2xx, returns the JSON as-is to the dashboard. 134 + - If non-JSON, returns `502` with `{ error, status, contentType, bodyPreview }`. 135 + 136 + ### Dashboard UI 137 + 138 + - `Usage summary` section uses `ServiceDetailsClient mode="stats"` and is fed from `/api/pds/service` (or mock when `PDS_USE_MOCK=true`). 139 + - Details section uses `ServiceDetailsClient mode="details"`: 140 + - Shows `id`, `service`, `namespace`, `state`, `kubeconfig_id`. 141 + - Shows `encrypted_config` fields (hostname, email settings, storage size) with `adminPassword` masked. 142 + - Shows `install_cmd` and timestamps. 143 + 144 + ### Env vars (server vs client) 145 + 146 + - **Server-only (secret)**: 147 + - `SUPABASE_SERVICE_ROLE_KEY` 148 + - `STRIPE_SECRET_KEY` 149 + - `STRIPE_WEBHOOK_SECRET` 150 + - `PDS_API_TOKEN` 151 + 152 + - **Client-safe (`NEXT_PUBLIC_*`)**: 153 + - `NEXT_PUBLIC_SUPABASE_URL` 154 + - `NEXT_PUBLIC_SUPABASE_ANON_KEY` 155 + - `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` 156 + - `NEXT_PUBLIC_STRIPE_PRICE_ID` 157 + 158 + These are configured in Vercel for **all environments** (or at least Preview/Production). Webhook and PDS routes only use the server-only keys.
+7 -1
README.md
··· 64 64 STRIPE_WEBHOOK_SECRET=your_webhook_secret 65 65 NEXT_PUBLIC_STRIPE_PRICE_ID=your_stripe_price_id 66 66 67 + # Optional (recommended): plan-specific Price IDs for checkout + pricing display. 68 + # If set, the UI will show the real Stripe amounts for each plan. 69 + NEXT_PUBLIC_STRIPE_PRICE_PERSONAL_ID=your_stripe_price_id_personal 70 + NEXT_PUBLIC_STRIPE_PRICE_COMMUNITY_ID=your_stripe_price_id_community 71 + NEXT_PUBLIC_STRIPE_PRICE_BUSINESS_ID=your_stripe_price_id_business 72 + 67 73 # App URL (for redirects) 68 74 NEXT_PUBLIC_APP_URL=http://localhost:3000 69 75 ``` 70 76 71 77 Get your Supabase keys from your project settings → API. 72 78 Get your Stripe keys from your Stripe dashboard. 73 - Create a subscription product and price in Stripe, then use the price ID for `NEXT_PUBLIC_STRIPE_PRICE_ID`. 79 + Create 3 subscription prices in Stripe (Personal / Community / Business). Set the resulting Price IDs in `NEXT_PUBLIC_STRIPE_PRICE_PERSONAL_ID`, `NEXT_PUBLIC_STRIPE_PRICE_COMMUNITY_ID`, and `NEXT_PUBLIC_STRIPE_PRICE_BUSINESS_ID`. 74 80 75 81 3. Start the development server: 76 82
+4 -2
app/actions/auth.ts
··· 26 26 return { error: error.message }; 27 27 } 28 28 29 + const next = (formData.get("next") as string) || "/dashboard"; 29 30 revalidatePath("/", "layout"); 30 - redirect("/dashboard"); 31 + redirect(next); 31 32 } 32 33 33 34 export async function signIn(formData: FormData) { ··· 47 48 return { error: error.message }; 48 49 } 49 50 51 + const next = (formData.get("next") as string) || "/dashboard"; 50 52 revalidatePath("/", "layout"); 51 - redirect("/dashboard"); 53 + redirect(next); 52 54 } 53 55 54 56 export async function signOut() {
+22 -18
app/actions/components/ui/badge.tsx
··· 1 1 import * as React from "react" 2 2 import { cva, type VariantProps } from "class-variance-authority" 3 - import { Slot } from "radix-ui" 3 + import { Slot } from "@radix-ui/react-slot" 4 4 5 5 import { cn } from "@/actions/lib/utils" 6 6 ··· 27 27 } 28 28 ) 29 29 30 - function Badge({ 31 - className, 32 - variant = "default", 33 - asChild = false, 34 - ...props 35 - }: React.ComponentProps<"span"> & 36 - VariantProps<typeof badgeVariants> & { asChild?: boolean }) { 37 - const Comp = asChild ? Slot.Root : "span" 30 + type BadgeProps = React.HTMLAttributes<HTMLSpanElement> & 31 + VariantProps<typeof badgeVariants> & { 32 + asChild?: boolean 33 + } 38 34 39 - return ( 40 - <Comp 41 - data-slot="badge" 42 - data-variant={variant} 43 - className={cn(badgeVariants({ variant }), className)} 44 - {...props} 45 - /> 46 - ) 47 - } 35 + const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>( 36 + ({ className, variant = "default", asChild = false, ...props }, ref) => { 37 + const Comp = asChild ? Slot : "span" 38 + 39 + return ( 40 + <Comp 41 + data-slot="badge" 42 + data-variant={variant} 43 + className={cn(badgeVariants({ variant }), className)} 44 + ref={ref} 45 + {...props} 46 + /> 47 + ) 48 + } 49 + ) 50 + 51 + Badge.displayName = "Badge" 48 52 49 53 export { Badge, badgeVariants }
+23 -22
app/actions/components/ui/button.tsx
··· 1 1 import * as React from "react" 2 2 import { cva, type VariantProps } from "class-variance-authority" 3 - import { Slot } from "radix-ui" 3 + import { Slot } from "@radix-ui/react-slot" 4 4 5 5 import { cn } from "@/actions/lib/utils" 6 6 7 7 const buttonVariants = cva( 8 - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 8 + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none cursor-pointer focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 9 9 { 10 10 variants: { 11 11 variant: { 12 - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", 12 + default: "bg-primary text-primary-foreground", 13 13 outline: 14 14 "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", 15 15 secondary: ··· 41 41 } 42 42 ) 43 43 44 - function Button({ 45 - className, 46 - variant = "default", 47 - size = "default", 48 - asChild = false, 49 - ...props 50 - }: React.ComponentProps<"button"> & 44 + type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & 51 45 VariantProps<typeof buttonVariants> & { 52 46 asChild?: boolean 53 - }) { 54 - const Comp = asChild ? Slot.Root : "button" 47 + } 48 + 49 + const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 50 + ({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { 51 + const Comp = asChild ? Slot : "button" 52 + 53 + return ( 54 + <Comp 55 + data-slot="button" 56 + data-variant={variant} 57 + data-size={size} 58 + className={cn(buttonVariants({ variant, size, className }))} 59 + ref={ref} 60 + {...props} 61 + /> 62 + ) 63 + } 64 + ) 55 65 56 - return ( 57 - <Comp 58 - data-slot="button" 59 - data-variant={variant} 60 - data-size={size} 61 - className={cn(buttonVariants({ variant, size, className }))} 62 - {...props} 63 - /> 64 - ) 65 - } 66 + Button.displayName = "Button" 66 67 67 68 export { Button, buttonVariants }
+19 -19
app/actions/components/ui/card.tsx
··· 1 - import * as React from "react" 1 + import * as React from "react"; 2 2 3 - import { cn } from "@/actions/lib/utils" 3 + import { cn } from "@/actions/lib/utils"; 4 4 5 5 function Card({ 6 6 className, ··· 12 12 data-slot="card" 13 13 data-size={size} 14 14 className={cn( 15 - "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", 16 - className 15 + "group/card flex flex-col gap-4 overflow-hidden rounded-xl border border-white/10 bg-white/5 py-4 text-sm text-white shadow-[0_0_40px_rgba(15,23,42,0.85)] backdrop-blur-xl has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", 16 + className, 17 17 )} 18 18 {...props} 19 19 /> 20 - ) 20 + ); 21 21 } 22 22 23 23 function CardHeader({ className, ...props }: React.ComponentProps<"div">) { ··· 26 26 data-slot="card-header" 27 27 className={cn( 28 28 "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3", 29 - className 29 + className, 30 30 )} 31 31 {...props} 32 32 /> 33 - ) 33 + ); 34 34 } 35 35 36 36 function CardTitle({ className, ...props }: React.ComponentProps<"div">) { ··· 38 38 <div 39 39 data-slot="card-title" 40 40 className={cn( 41 - "text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", 42 - className 41 + "text-base leading-snug font-semibold text-white group-data-[size=sm]/card:text-sm", 42 + className, 43 43 )} 44 44 {...props} 45 45 /> 46 - ) 46 + ); 47 47 } 48 48 49 49 function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 50 50 return ( 51 51 <div 52 52 data-slot="card-description" 53 - className={cn("text-sm text-muted-foreground", className)} 53 + className={cn("text-sm text-white/70", className)} 54 54 {...props} 55 55 /> 56 - ) 56 + ); 57 57 } 58 58 59 59 function CardAction({ className, ...props }: React.ComponentProps<"div">) { ··· 62 62 data-slot="card-action" 63 63 className={cn( 64 64 "col-start-2 row-span-2 row-start-1 self-start justify-self-end", 65 - className 65 + className, 66 66 )} 67 67 {...props} 68 68 /> 69 - ) 69 + ); 70 70 } 71 71 72 72 function CardContent({ className, ...props }: React.ComponentProps<"div">) { ··· 76 76 className={cn("px-4 group-data-[size=sm]/card:px-3", className)} 77 77 {...props} 78 78 /> 79 - ) 79 + ); 80 80 } 81 81 82 82 function CardFooter({ className, ...props }: React.ComponentProps<"div">) { ··· 84 84 <div 85 85 data-slot="card-footer" 86 86 className={cn( 87 - "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3", 88 - className 87 + "flex items-center rounded-b-xl border-t border-white/10 bg-white/5 p-4 text-white group-data-[size=sm]/card:p-3", 88 + className, 89 89 )} 90 90 {...props} 91 91 /> 92 - ) 92 + ); 93 93 } 94 94 95 95 export { ··· 100 100 CardAction, 101 101 CardDescription, 102 102 CardContent, 103 - } 103 + };
+122 -28
app/actions/subscription.ts
··· 6 6 import { headers } from "next/headers"; 7 7 import type { Stripe } from "stripe"; 8 8 9 + function normalizeSlug(value: string) { 10 + return value 11 + .toLowerCase() 12 + .replace(/[^a-z0-9-]/g, "-") 13 + .replace(/^-+/, "") 14 + .replace(/-+$/, "") 15 + .slice(0, 63); 16 + } 17 + 18 + type PdsCheckoutOptions = { 19 + username?: string; 20 + hostname?: string; 21 + disksizeGb?: number; 22 + }; 23 + 9 24 /** 10 25 * Get user's Stripe customer ID from database (minimal storage) 11 26 */ ··· 46 61 limit: 10, 47 62 }); 48 63 49 - // Find active or trialing subscription 64 + // Find active or trialing subscription (still counts as subscribed even if cancel_at_period_end) 50 65 const activeSubscription = subscriptions.data.find( 51 66 (sub) => 52 - (sub.status === "active" || sub.status === "trialing") && 53 - !sub.cancel_at_period_end 67 + sub.status === "active" || sub.status === "trialing" 54 68 ); 55 69 56 70 return activeSubscription || null; ··· 61 75 } 62 76 63 77 /** 64 - * Get subscription status for UI (always from Stripe) 78 + * Get latest subscription for UI (shows canceled history too) 65 79 */ 66 80 export async function getSubscriptionStatus() { 67 - const subscription = await getActiveSubscription(); 81 + const customerId = await getStripeCustomerId(); 82 + if (!customerId) { 83 + return { 84 + subscribed: false, 85 + subscription: null, 86 + }; 87 + } 88 + 89 + try { 90 + const subscriptions = await stripe.subscriptions.list({ 91 + customer: customerId, 92 + status: "all", 93 + limit: 10, 94 + }); 95 + 96 + if (!subscriptions.data.length) { 97 + return { 98 + subscribed: false, 99 + subscription: null, 100 + }; 101 + } 102 + 103 + // Pick the most recently created subscription 104 + const latest = subscriptions.data.reduce<Stripe.Subscription | null>( 105 + (acc, sub) => { 106 + if (!acc) return sub; 107 + return sub.created > acc.created ? sub : acc; 108 + }, 109 + null 110 + ); 111 + 112 + if (!latest) { 113 + return { 114 + subscribed: false, 115 + subscription: null, 116 + }; 117 + } 68 118 69 - return { 70 - subscribed: !!subscription, 71 - subscription: subscription 72 - ? { 73 - status: subscription.status, 74 - cancel_at_period_end: subscription.cancel_at_period_end, 75 - current_period_end: new Date( 76 - subscription.current_period_end * 1000 77 - ).toISOString(), 78 - current_period_start: new Date( 79 - subscription.current_period_start * 1000 80 - ).toISOString(), 81 - } 82 - : null, 83 - }; 119 + const isCurrentlySubscribed = 120 + (latest.status === "active" || latest.status === "trialing") && 121 + latest.cancel_at_period_end === false; 122 + 123 + return { 124 + subscribed: isCurrentlySubscribed, 125 + subscription: { 126 + status: latest.status, 127 + cancel_at_period_end: latest.cancel_at_period_end, 128 + current_period_end: new Date( 129 + latest.current_period_end * 1000 130 + ).toISOString(), 131 + current_period_start: new Date( 132 + latest.current_period_start * 1000 133 + ).toISOString(), 134 + }, 135 + }; 136 + } catch (error) { 137 + console.error("Error fetching subscription status from Stripe:", error); 138 + return { 139 + subscribed: false, 140 + subscription: null, 141 + }; 142 + } 84 143 } 85 144 86 145 /** ··· 101 160 /** 102 161 * Create checkout session for new subscription 103 162 */ 104 - export async function createSubscriptionCheckout(priceId: string) { 163 + export async function createSubscriptionCheckout( 164 + priceId: string, 165 + options?: PdsCheckoutOptions, 166 + ) { 105 167 const supabase = await createClient(); 106 168 const { 107 169 data: { user }, ··· 124 186 customerId = customer.id; 125 187 126 188 // Store only customer ID in database (minimal) 127 - await supabase.from("subscriptions").upsert({ 128 - user_id: user.id, 129 - stripe_customer_id: customerId, 130 - }); 189 + const { data: existingSub } = await supabase 190 + .from("subscriptions") 191 + .select("id") 192 + .eq("user_id", user.id) 193 + .limit(1) 194 + .maybeSingle(); 195 + 196 + if (!existingSub) { 197 + const { error } = await supabase.from("subscriptions").insert({ 198 + user_id: user.id, 199 + stripe_customer_id: customerId, 200 + }); 201 + if (error) throw error; 202 + } 131 203 } 132 204 133 205 const headersList = await headers(); ··· 140 212 "http://localhost:3000"; 141 213 142 214 const checkoutSession = await stripe.checkout.sessions.create({ 215 + // Used later in the Stripe webhook to provision the user's PDS 216 + // with user-selected settings. 217 + metadata: (() => { 218 + const fallbackUsername = normalizeSlug(user.email!.split("@")[0] || "pds"); 219 + const pdsUsername = normalizeSlug(options?.username || fallbackUsername); 220 + const pdsDisksizeGb = Number(options?.disksizeGb); 221 + const normalizedDisksize = 222 + Number.isFinite(pdsDisksizeGb) && pdsDisksizeGb > 0 223 + ? String(Math.floor(pdsDisksizeGb)) 224 + : "10"; 225 + 226 + const requestedHostname = (options?.hostname || "").trim(); 227 + const cleanedHostname = requestedHostname 228 + .replace(/^https?:\/\//i, "") 229 + .replace(/\/.*$/, ""); 230 + const pdsHostnameBase = cleanedHostname || `${pdsUsername}.eny.k8s.frx.pub`; 231 + 232 + return { 233 + user_id: user.id, 234 + user_email: user.email!, 235 + pds_username: pdsUsername, 236 + pds_disksize_gb: normalizedDisksize, 237 + pds_hostname_base: pdsHostnameBase, 238 + }; 239 + })(), 143 240 customer: customerId, 144 241 mode: "subscription", 145 242 payment_method_types: ["card"], ··· 151 248 ], 152 249 success_url: `${origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`, 153 250 cancel_url: `${origin}/dashboard`, 154 - metadata: { 155 - user_id: user.id, 156 - }, 157 251 }); 158 252 159 253 return { url: checkoutSession.url };
+106
app/api/pds/atproto/create-account/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { createClient } from "@/lib/supabase/server"; 4 + 5 + import { 6 + getPdsBaseUrlFromService, 7 + getPdsServiceForCurrentUser, 8 + } from "../helpers"; 9 + 10 + export async function POST(req: Request) { 11 + try { 12 + const body = (await req.json()) as { 13 + email?: string; 14 + handle: string; 15 + password: string; 16 + inviteCode: string; 17 + }; 18 + 19 + if (!body?.handle || !body?.password || !body?.inviteCode) { 20 + return NextResponse.json( 21 + { message: "Missing required fields: handle, password, inviteCode" }, 22 + { status: 400 }, 23 + ); 24 + } 25 + 26 + const { service, pdsServiceId } = await getPdsServiceForCurrentUser(); 27 + 28 + const requiredServiceIdRaw = process.env.NEXT_PUBLIC_PDS_TEST_SERVICE_ID; 29 + if (requiredServiceIdRaw) { 30 + const requiredServiceId = Number(requiredServiceIdRaw); 31 + if (pdsServiceId !== requiredServiceId) { 32 + return NextResponse.json( 33 + { 34 + message: `PDS service id mismatch: expected ${requiredServiceId}, got ${pdsServiceId}`, 35 + }, 36 + { status: 409 }, 37 + ); 38 + } 39 + } 40 + 41 + // Ensure we always have `https://...` for fetch 42 + const pdsBaseUrl = getPdsBaseUrlFromService(service); 43 + 44 + let emailToUse = body.email; 45 + if (!emailToUse) { 46 + // Reuse the user's own Supabase email for testing, but add a +alias suffix 47 + // to avoid collisions if the backend enforces uniqueness. 48 + const supabase = await createClient(); 49 + const { 50 + data: { user }, 51 + } = await supabase.auth.getUser(); 52 + 53 + if (!user?.email) { 54 + return NextResponse.json( 55 + { message: "Missing email (neither request body nor Supabase user email found)" }, 56 + { status: 400 }, 57 + ); 58 + } 59 + 60 + const baseEmail = user.email; 61 + const [local, domain] = baseEmail.split("@"); 62 + const alias = `${local}+atproto-test-${Date.now()}`; 63 + emailToUse = `${alias}@${domain}`; 64 + } 65 + 66 + const res = await fetch( 67 + `${pdsBaseUrl}/xrpc/com.atproto.server.createAccount`, 68 + { 69 + method: "POST", 70 + headers: { 71 + "Content-Type": "application/json", 72 + }, 73 + body: JSON.stringify({ 74 + email: emailToUse, 75 + handle: body.handle, 76 + password: body.password, 77 + inviteCode: body.inviteCode, 78 + }), 79 + }, 80 + ); 81 + 82 + const contentType = res.headers.get("content-type") || ""; 83 + const payload = contentType.includes("application/json") 84 + ? await res.json() 85 + : await res.text().catch(() => ""); 86 + 87 + if (!res.ok) { 88 + return NextResponse.json( 89 + { message: "Failed to create account", status: res.status, payload }, 90 + { status: 502 }, 91 + ); 92 + } 93 + 94 + // Return the email that we used, so the UI mirrors the real admin workflow. 95 + if (payload && typeof payload === "object") { 96 + return NextResponse.json({ ...(payload as any), emailUsed: emailToUse }); 97 + } 98 + 99 + return NextResponse.json({ payload, emailUsed: emailToUse }); 100 + } catch (error) { 101 + const message = error instanceof Error ? error.message : "Unknown error"; 102 + const status = (error as any)?.status ?? 500; 103 + return NextResponse.json({ message }, { status }); 104 + } 105 + } 106 +
+72
app/api/pds/atproto/create-session/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { 4 + getPdsBaseUrlFromService, 5 + getPdsServiceForCurrentUser, 6 + } from "../helpers"; 7 + 8 + export async function POST(req: Request) { 9 + try { 10 + const body = (await req.json()) as { 11 + identifier: string; 12 + password: string; 13 + }; 14 + 15 + if (!body?.identifier || !body?.password) { 16 + return NextResponse.json( 17 + { message: "Missing required fields: identifier, password" }, 18 + { status: 400 }, 19 + ); 20 + } 21 + 22 + const { service, pdsServiceId } = await getPdsServiceForCurrentUser(); 23 + 24 + const requiredServiceIdRaw = process.env.NEXT_PUBLIC_PDS_TEST_SERVICE_ID; 25 + if (requiredServiceIdRaw) { 26 + const requiredServiceId = Number(requiredServiceIdRaw); 27 + if (pdsServiceId !== requiredServiceId) { 28 + return NextResponse.json( 29 + { 30 + message: `PDS service id mismatch: expected ${requiredServiceId}, got ${pdsServiceId}`, 31 + }, 32 + { status: 409 }, 33 + ); 34 + } 35 + } 36 + 37 + const pdsBaseUrl = getPdsBaseUrlFromService(service); 38 + 39 + const res = await fetch( 40 + `${pdsBaseUrl}/xrpc/com.atproto.server.createSession`, 41 + { 42 + method: "POST", 43 + headers: { 44 + "Content-Type": "application/json", 45 + }, 46 + body: JSON.stringify({ 47 + identifier: body.identifier, 48 + password: body.password, 49 + }), 50 + }, 51 + ); 52 + 53 + const contentType = res.headers.get("content-type") || ""; 54 + const payload = contentType.includes("application/json") 55 + ? await res.json() 56 + : await res.text().catch(() => ""); 57 + 58 + if (!res.ok) { 59 + return NextResponse.json( 60 + { message: "Failed to create session", status: res.status, payload }, 61 + { status: 502 }, 62 + ); 63 + } 64 + 65 + return NextResponse.json(payload); 66 + } catch (error) { 67 + const message = error instanceof Error ? error.message : "Unknown error"; 68 + const status = (error as any)?.status ?? 500; 69 + return NextResponse.json({ message }, { status }); 70 + } 71 + } 72 +
+105
app/api/pds/atproto/helpers.ts
··· 1 + import { createClient } from "@/lib/supabase/server"; 2 + 3 + const PDS_API_BASE_URL = "https://k8s-pds.frx.pub/api/v1"; 4 + 5 + function parseMaybeDoubleEncodedJson(input: unknown) { 6 + if (typeof input === "string") { 7 + try { 8 + return JSON.parse(input); 9 + } catch { 10 + return input; 11 + } 12 + } 13 + return input; 14 + } 15 + 16 + export async function getPdsServiceForCurrentUser(): Promise<{ 17 + pdsServiceId: number; 18 + service: any; 19 + }> { 20 + const supabase = await createClient(); 21 + const { 22 + data: { user }, 23 + } = await supabase.auth.getUser(); 24 + 25 + if (!user) { 26 + return Promise.reject(Object.assign(new Error("Unauthorized"), { status: 401 })); 27 + } 28 + 29 + const { data: pdsServiceRow } = await supabase 30 + .from("pds_services") 31 + .select("pds_service_id") 32 + .eq("user_id", user.id) 33 + .maybeSingle(); 34 + 35 + const forcedServiceIdRaw = 36 + process.env.PDS_FORCE_SERVICE_ID === "true" 37 + ? process.env.PDS_TEST_SERVICE_ID 38 + : undefined; 39 + const forcedServiceId = forcedServiceIdRaw ? Number(forcedServiceIdRaw) : null; 40 + 41 + const pdsServiceId = (forcedServiceId !== null && Number.isFinite(forcedServiceId) 42 + ? forcedServiceId 43 + : pdsServiceRow?.pds_service_id) as number | null | undefined; 44 + 45 + if (!pdsServiceId) { 46 + return Promise.reject( 47 + Object.assign(new Error("No provisioned PDS found for this user"), { status: 404 }), 48 + ); 49 + } 50 + 51 + const apiToken = process.env.PDS_API_TOKEN; 52 + if (!apiToken) { 53 + return Promise.reject( 54 + Object.assign(new Error("Missing PDS_API_TOKEN env var"), { status: 500 }), 55 + ); 56 + } 57 + 58 + const res = await fetch(`${PDS_API_BASE_URL}/service/${pdsServiceId}`, { 59 + cache: "no-store", 60 + headers: { 61 + Accept: "application/json", 62 + "X-Requested-With": "XMLHttpRequest", 63 + Authorization: `Bearer ${apiToken}`, 64 + }, 65 + }); 66 + 67 + const contentType = res.headers.get("content-type") || ""; 68 + if (!res.ok) { 69 + let body: unknown = null; 70 + try { 71 + body = await res.json(); 72 + } catch { 73 + body = await res.text().catch(() => ""); 74 + } 75 + return Promise.reject( 76 + Object.assign( 77 + new Error(`Upstream PDS service fetch failed (${res.status})`), 78 + { status: 502, body: parseMaybeDoubleEncodedJson(body), contentType }, 79 + ), 80 + ); 81 + } 82 + 83 + if (!contentType.includes("application/json")) { 84 + return Promise.reject( 85 + Object.assign(new Error("Upstream did not return JSON"), { status: 502 }), 86 + ); 87 + } 88 + 89 + const dataRaw: unknown = await res.json(); 90 + const service = parseMaybeDoubleEncodedJson(dataRaw); 91 + 92 + return { pdsServiceId: Number(pdsServiceId), service }; 93 + } 94 + 95 + export function getPdsBaseUrlFromService(service: any): string { 96 + const raw = service?.encrypted_config?.hostname as string | undefined; 97 + if (!raw) { 98 + throw new Error("Missing PDS host"); 99 + } 100 + 101 + // Backend might return either `https://host` or just `host`. 102 + const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; 103 + return withScheme.replace(/\/+$/, ""); 104 + } 105 +
+82
app/api/pds/atproto/invite/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { createHash } from "crypto"; 4 + 5 + import { getPdsBaseUrlFromService, getPdsServiceForCurrentUser } from "../helpers"; 6 + 7 + function toBasicAuth(user: string, pass: string) { 8 + return `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}`; 9 + } 10 + 11 + export async function POST(req: Request) { 12 + try { 13 + const { useCount } = (await req.json()) as { useCount?: number }; 14 + 15 + const { service, pdsServiceId } = await getPdsServiceForCurrentUser(); 16 + const requiredServiceIdRaw = process.env.NEXT_PUBLIC_PDS_TEST_SERVICE_ID; 17 + if (requiredServiceIdRaw) { 18 + const requiredServiceId = Number(requiredServiceIdRaw); 19 + if (pdsServiceId !== requiredServiceId) { 20 + return NextResponse.json( 21 + { 22 + message: `PDS service id mismatch: expected ${requiredServiceId}, got ${pdsServiceId}`, 23 + }, 24 + { status: 409 }, 25 + ); 26 + } 27 + } 28 + 29 + const adminPassword = service?.encrypted_config?.adminPassword as 30 + | string 31 + | undefined; 32 + 33 + if (!service?.encrypted_config || !adminPassword) { 34 + return NextResponse.json( 35 + { message: "Missing PDS host/admin credentials" }, 36 + { status: 500 }, 37 + ); 38 + } 39 + 40 + const trimmedAdminPassword = String(adminPassword).trim(); 41 + const adminPasswordHashPrefix = createHash("sha256") 42 + .update(trimmedAdminPassword) 43 + .digest("hex") 44 + .slice(0, 10); 45 + 46 + // PDS scripts use `admin:${PDS_ADMIN_PASSWORD}` 47 + const authHeader = toBasicAuth("admin", trimmedAdminPassword); 48 + 49 + const pdsBaseUrl = getPdsBaseUrlFromService(service); 50 + 51 + const res = await fetch( 52 + `${pdsBaseUrl}/xrpc/com.atproto.server.createInviteCode`, 53 + { 54 + method: "POST", 55 + headers: { 56 + "Content-Type": "application/json", 57 + Authorization: authHeader, 58 + }, 59 + body: JSON.stringify({ useCount: useCount ?? 1 }), 60 + }, 61 + ); 62 + 63 + const contentType = res.headers.get("content-type") || ""; 64 + const payload = contentType.includes("application/json") 65 + ? await res.json() 66 + : await res.text().catch(() => ""); 67 + 68 + if (!res.ok) { 69 + return NextResponse.json( 70 + { message: "Failed to create invite", status: res.status, payload }, 71 + { status: 502 }, 72 + ); 73 + } 74 + 75 + return NextResponse.json(payload); 76 + } catch (error) { 77 + const message = error instanceof Error ? error.message : "Unknown error"; 78 + const status = (error as any)?.status ?? 500; 79 + return NextResponse.json({ message }, { status }); 80 + } 81 + } 82 +
+219
app/api/pds/service/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { createClient } from "@/lib/supabase/server"; 4 + 5 + const PDS_API_BASE_URL = "https://k8s-pds.frx.pub/api/v1"; 6 + 7 + function getMockService() { 8 + const now = new Date(); 9 + const iso = (d: Date) => d.toISOString(); 10 + 11 + const storageAllocatedBytes = 10 * 1024 ** 3; // 10 GiB 12 + const storageUsedBytes = Math.floor(2.8 * 1024 ** 3); // ~2.8 GiB 13 + const bandwidthLimitBytesPerMonth = 100 * 1024 ** 3; // 100 GiB 14 + const bandwidthUsedBytesThisMonth = Math.floor(14.7 * 1024 ** 3); // ~14.7 GiB 15 + const cpuUsagePercent = 23; 16 + const ramUsagePercent = 41; 17 + const userSlotsUsed = 1; 18 + const userSlotsTotal = 10; 19 + 20 + const storageUsedDailyLast30d = Array.from({ length: 30 }).map((_, i) => { 21 + const d = new Date(now); 22 + d.setUTCDate(d.getUTCDate() - (29 - i)); 23 + const base = Math.floor(1.9 * 1024 ** 3); 24 + const growth = Math.floor(i * (34 * 1024 ** 2)); // ~34 MiB/day 25 + const noise = Math.floor(((i % 5) - 2) * (6 * 1024 ** 2)); 26 + return { date: iso(d).slice(0, 10), usedBytes: base + growth + noise }; 27 + }); 28 + 29 + const requestsPerHourLast24h = Array.from({ length: 24 }).map((_, i) => { 30 + const d = new Date(now); 31 + d.setUTCHours(d.getUTCHours() - (23 - i), 0, 0, 0); 32 + const wave = 260 + Math.floor(180 * Math.sin((i / 24) * Math.PI * 2)); 33 + const jitter = (i % 3) * 17; 34 + return { hour: iso(d), count: Math.max(40, wave + jitter) }; 35 + }); 36 + 37 + const failedRequestsLast24h = 42; 38 + const successfulRequestsLast24h = requestsPerHourLast24h.reduce( 39 + (sum, p) => sum + p.count, 40 + 0, 41 + ); 42 + 43 + return { 44 + id: 1, 45 + name: "test1-pds", 46 + service: "bluesky-pds", 47 + namespace: "kd0186-test1-pds", 48 + encrypted_config: { 49 + hostname: "test1.eny.space", 50 + adminPassword: "zoidberg", 51 + emailSmtpUrl: "smtps://max@mustermann.de:s3cr3t@smtp.mustermann.de:465/", 52 + pdsEmailFromAddress: "test1@example.com", 53 + dataStorage: { 54 + size: "10Gi", 55 + }, 56 + }, 57 + install_cmd: 58 + "export KUBECONFIG={kubeconfig}\n" + 59 + "helm repo add nerkho https://charts.nerkho.ch\n" + 60 + "helm repo update\n" + 61 + "helm install bluesky-pds nerkho/bluesky-pds --namespace {namespace} -f {values}\n" + 62 + 'export KUBECONFIG=""', 63 + state: 0, 64 + kubeconfig_id: 1, 65 + created_at: "2026-03-17T15:05:40.000000Z", 66 + updated_at: "2026-03-17T15:05:40.000000Z", 67 + stats: { 68 + cpuUsagePercent, 69 + ramUsagePercent, 70 + storageUsedBytes, 71 + storageAllocatedBytes, 72 + storageObjectsCount: 12345, 73 + storageUsedDailyLast30d, 74 + bandwidthUsedBytesThisMonth, 75 + bandwidthLimitBytesPerMonth, 76 + requestsLast24h: successfulRequestsLast24h, 77 + requestsPerHourLast24h, 78 + activeUsers: 3, 79 + uniqueUsersLast30d: 27, 80 + userSlotsUsed, 81 + userSlotsTotal, 82 + uptimeSeconds: 987654, 83 + lastBackupAt: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(), 84 + failedRequestsLast24h, 85 + successfulRequestsLast24h, 86 + }, 87 + }; 88 + } 89 + 90 + export async function GET() { 91 + try { 92 + const useMock = process.env.PDS_USE_MOCK === "true"; 93 + if (useMock) { 94 + return NextResponse.json(getMockService()); 95 + } 96 + 97 + const apiToken = process.env.PDS_API_TOKEN; 98 + 99 + if (!apiToken) { 100 + return NextResponse.json( 101 + { 102 + error: 103 + "Missing PDS_API_TOKEN env variable for authenticating with PDS API", 104 + }, 105 + { status: 500 }, 106 + ); 107 + } 108 + 109 + const supabase = await createClient(); 110 + const { 111 + data: { user }, 112 + } = await supabase.auth.getUser(); 113 + 114 + if (!user) { 115 + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }); 116 + } 117 + 118 + const { data: pdsServiceRow } = await supabase 119 + .from("pds_services") 120 + .select("pds_service_id") 121 + .eq("user_id", user.id) 122 + .maybeSingle(); 123 + 124 + const forcedServiceIdRaw = process.env.PDS_FORCE_SERVICE_ID === "true" 125 + ? process.env.PDS_TEST_SERVICE_ID 126 + : undefined; 127 + const forcedServiceId = forcedServiceIdRaw ? Number(forcedServiceIdRaw) : null; 128 + 129 + const pdsServiceId = (forcedServiceId !== null && Number.isFinite(forcedServiceId) 130 + ? forcedServiceId 131 + : pdsServiceRow?.pds_service_id) as number | null | undefined; 132 + 133 + if (!pdsServiceId) { 134 + return NextResponse.json( 135 + { message: "No provisioned PDS found for this user yet" }, 136 + { status: 404 }, 137 + ); 138 + } 139 + 140 + const pdsServiceUrl = `${PDS_API_BASE_URL}/service/${pdsServiceId}`; 141 + 142 + const res = await fetch(pdsServiceUrl, { 143 + // Ensure this runs server-side only and is not cached aggressively 144 + cache: "no-store", 145 + headers: { 146 + Accept: "application/json", 147 + "X-Requested-With": "XMLHttpRequest", 148 + Authorization: `Bearer ${apiToken}`, 149 + }, 150 + }); 151 + 152 + const contentType = res.headers.get("content-type") || ""; 153 + 154 + // Try to parse JSON if it looks like JSON, otherwise fall back to text 155 + if (contentType.includes("application/json")) { 156 + let data: unknown = await res.json(); 157 + 158 + // Some backends double-encode JSON as a string; handle that gracefully. 159 + if (typeof data === "string") { 160 + try { 161 + data = JSON.parse(data); 162 + } catch { 163 + // keep as string if it isn't valid JSON 164 + } 165 + } 166 + 167 + if (!res.ok) { 168 + return NextResponse.json( 169 + { 170 + error: "Upstream request failed", 171 + status: res.status, 172 + body: data, 173 + }, 174 + { status: 502 }, 175 + ); 176 + } 177 + 178 + // Redact sensitive secrets before sending to the browser. 179 + if (data && typeof data === "object") { 180 + const d: any = data; 181 + if (d.encrypted_config && typeof d.encrypted_config === "object") { 182 + if ("adminPassword" in d.encrypted_config) { 183 + d.encrypted_config.adminPassword = "redacted"; 184 + } 185 + if ("jwtSecret" in d.encrypted_config) { 186 + d.encrypted_config.jwtSecret = "redacted"; 187 + } 188 + if ("plcRotationKey" in d.encrypted_config) { 189 + d.encrypted_config.plcRotationKey = "redacted"; 190 + } 191 + } 192 + } 193 + 194 + return NextResponse.json(data); 195 + } 196 + 197 + const bodyText = await res.text(); 198 + 199 + // Return diagnostics so we can see what the upstream is sending 200 + return NextResponse.json( 201 + { 202 + error: "Upstream did not return JSON", 203 + status: res.status, 204 + contentType, 205 + bodyPreview: bodyText.slice(0, 500), 206 + }, 207 + { status: 502 }, 208 + ); 209 + } catch (error) { 210 + console.error("Error proxying PDS service request", error); 211 + return NextResponse.json( 212 + { 213 + error: "Failed to reach PDS service endpoint", 214 + detail: error instanceof Error ? error.message : String(error), 215 + }, 216 + { status: 500 }, 217 + ); 218 + } 219 + }
+204 -10
app/api/webhooks/route.ts
··· 2 2 3 3 import { NextResponse } from "next/server"; 4 4 5 + import { randomBytes } from "crypto"; 6 + 5 7 import { stripe } from "@/lib/stripe"; 6 8 import { createAdminClient } from "@/lib/supabase/admin"; 7 9 10 + const PDS_API_BASE_URL = "https://k8s-pds.frx.pub/api/v1"; 11 + 12 + function normalizeSlug(value: string) { 13 + return value 14 + .toLowerCase() 15 + .replace(/[^a-z0-9-]/g, "-") 16 + .replace(/^-+/, "") 17 + .replace(/-+$/, "") 18 + .slice(0, 63); 19 + } 20 + 21 + function normalizeDeployHostname(raw: string) { 22 + let h = raw.trim(); 23 + h = h.replace(/^https?:\/\//i, ""); 24 + h = h.replace(/\/.*$/, ""); 25 + return h.replace(/\/$/, ""); 26 + } 27 + 28 + function isValidFqdn(host: string) { 29 + if (!host || host.length > 253) return false; 30 + if (host.endsWith(".")) return false; 31 + const parts = host.split("."); 32 + if (parts.length < 2) return false; 33 + return parts.every((label) => { 34 + if (!label || label.length > 63) return false; 35 + if (!/^[a-z0-9-]+$/i.test(label)) return false; 36 + if (label.startsWith("-") || label.endsWith("-")) return false; 37 + return true; 38 + }); 39 + } 40 + 41 + async function provisionPdsForUser({ 42 + userId, 43 + userEmail, 44 + pdsUsername, 45 + pdsHostnameBase, 46 + disksizeGb, 47 + }: { 48 + userId: string; 49 + userEmail: string; 50 + pdsUsername: string; 51 + pdsHostnameBase: string; 52 + disksizeGb: string; 53 + }) { 54 + const apiToken = process.env.PDS_API_TOKEN; 55 + if (!apiToken) { 56 + throw new Error("Missing PDS_API_TOKEN env var"); 57 + } 58 + 59 + const password = randomBytes(16).toString("base64url"); 60 + const hostname = normalizeDeployHostname(pdsHostnameBase); 61 + if (!isValidFqdn(hostname)) { 62 + throw new Error( 63 + `Invalid hostname for deploy after normalization: "${hostname}" (raw="${pdsHostnameBase}")`, 64 + ); 65 + } 66 + 67 + const disksizeParsed = Number(disksizeGb); 68 + if (!Number.isFinite(disksizeParsed) || disksizeParsed <= 0) { 69 + throw new Error( 70 + `Invalid pds_disksize_gb metadata value: "${disksizeGb}". Expected a positive number.`, 71 + ); 72 + } 73 + const disksize = Math.floor(disksizeParsed); 74 + 75 + const supabase = createAdminClient(); 76 + 77 + // Idempotency: if we already have a service_id stored, don't redeploy 78 + const { data: existing } = await supabase 79 + .from("pds_services") 80 + .select("pds_service_id,status") 81 + .eq("user_id", userId) 82 + .maybeSingle(); 83 + 84 + if (existing) { 85 + if (existing.pds_service_id) { 86 + return { skipped: true, pds_service_id: existing.pds_service_id }; 87 + } 88 + // Retry deploy for known retryable states where id may be missing. 89 + // Keep skipping for everything else to avoid duplicate provisioning. 90 + const retryableStatuses = new Set([ 91 + "deploy_failed", 92 + "deploy_succeeded_no_id", 93 + ]); 94 + if (existing.status && !retryableStatuses.has(existing.status)) { 95 + return { skipped: true, pds_service_id: null }; 96 + } 97 + } 98 + 99 + const deployRes = await fetch(`${PDS_API_BASE_URL}/deploy`, { 100 + method: "POST", 101 + headers: { 102 + Accept: "application/json", 103 + "Content-Type": "application/json", 104 + Authorization: `Bearer ${apiToken}`, 105 + }, 106 + body: JSON.stringify({ 107 + username: pdsUsername, 108 + password, 109 + email: userEmail, 110 + hostname, 111 + disksize, 112 + }), 113 + }); 114 + 115 + const deployContentType = deployRes.headers.get("content-type") || ""; 116 + const deployBody = deployContentType.includes("application/json") 117 + ? await deployRes.json() 118 + : await deployRes.text(); 119 + 120 + if (!deployRes.ok) { 121 + // Persist failure status for easier debugging 122 + await supabase.from("pds_services").upsert({ 123 + user_id: userId, 124 + hostname, 125 + status: "deploy_failed", 126 + }); 127 + throw new Error( 128 + `PDS deploy failed (${deployRes.status}) for hostname "${hostname}": ${ 129 + typeof deployBody === "string" ? deployBody : JSON.stringify(deployBody) 130 + }`, 131 + ); 132 + } 133 + 134 + const maybeServiceId = 135 + (typeof deployBody === "object" && deployBody !== null 136 + ? ((deployBody as any).service_id ?? 137 + (deployBody as any).serviceId ?? 138 + (deployBody as any).id ?? 139 + (deployBody as any).service?.id ?? 140 + (deployBody as any).data?.id ?? 141 + (deployBody as any).data?.serviceId) 142 + : undefined) ?? null; 143 + 144 + const pds_service_id = 145 + typeof maybeServiceId === "string" || typeof maybeServiceId === "number" 146 + ? Number(maybeServiceId) 147 + : null; 148 + 149 + await supabase.from("pds_services").upsert({ 150 + user_id: userId, 151 + pds_service_id, 152 + hostname, 153 + status: pds_service_id ? "provisioning" : "deploy_succeeded_no_id", 154 + }); 155 + 156 + return { skipped: false, pds_service_id }; 157 + } 158 + 8 159 export async function POST(req: Request) { 9 160 let event: Stripe.Event; 10 161 ··· 12 163 event = stripe.webhooks.constructEvent( 13 164 await (await req.blob()).text(), 14 165 req.headers.get("stripe-signature") as string, 15 - process.env.STRIPE_WEBHOOK_SECRET as string 166 + process.env.STRIPE_WEBHOOK_SECRET as string, 16 167 ); 17 168 } catch (err) { 18 169 const errorMessage = err instanceof Error ? err.message : "Unknown error"; 19 170 console.log(`❌ Webhook Error: ${errorMessage}`); 20 171 return NextResponse.json( 21 172 { message: `Webhook Error: ${errorMessage}` }, 22 - { status: 400 } 173 + { status: 400 }, 23 174 ); 24 175 } 25 176 ··· 33 184 34 185 if (session.mode === "subscription" && session.customer) { 35 186 const userId = session.metadata?.user_id; 187 + const userEmail = session.metadata?.user_email; 36 188 const customerId = 37 189 typeof session.customer === "string" 38 190 ? session.customer 39 191 : session.customer.id; 40 192 41 193 if (userId && customerId) { 42 - // Only store user_id -> stripe_customer_id mapping (minimal) 43 - const { error } = await supabase.from("subscriptions").upsert({ 44 - user_id: userId, 45 - stripe_customer_id: customerId, 46 - }); 194 + // Store user_id -> stripe_customer_id mapping (minimal) 195 + // Avoid creating duplicate rows if the webhook is delivered more than once. 196 + const { data: existingSub } = await supabase 197 + .from("subscriptions") 198 + .select("id") 199 + .eq("user_id", userId) 200 + .limit(1) 201 + .maybeSingle(); 202 + 203 + if (!existingSub) { 204 + const { error } = await supabase 205 + .from("subscriptions") 206 + .insert({ user_id: userId, stripe_customer_id: customerId }); 207 + if (error) console.error("Error inserting customer ID:", error); 208 + else console.log(`✅ Customer ID stored for user ${userId}`); 209 + } else { 210 + const { error } = await supabase 211 + .from("subscriptions") 212 + .update({ stripe_customer_id: customerId }) 213 + .eq("id", existingSub.id); 214 + if (error) console.error("Error updating customer ID:", error); 215 + } 216 + 217 + // Next step: provision the user's PDS 218 + if (userEmail) { 219 + const fallbackUsername = normalizeSlug( 220 + userEmail.split("@")[0] || "pds", 221 + ); 222 + const pdsUsername = normalizeSlug( 223 + session.metadata?.pds_username || fallbackUsername, 224 + ); 225 + const pdsHostnameBase = 226 + session.metadata?.pds_hostname_base || 227 + `${pdsUsername}.eny.k8s.frx.pub`; 228 + const disksizeGb = session.metadata?.pds_disksize_gb || "10"; 47 229 48 - if (error) { 49 - console.error("Error storing customer ID:", error); 230 + try { 231 + console.log(`✅ Provisioning PDS for user ${userId}...`); 232 + await provisionPdsForUser({ 233 + userId, 234 + userEmail, 235 + pdsUsername, 236 + pdsHostnameBase, 237 + disksizeGb, 238 + }); 239 + } catch (e) { 240 + console.error(`❌ Provisioning PDS failed for ${userId}:`, e); 241 + } 50 242 } else { 51 - console.log(`✅ Customer ID stored for user ${userId}`); 243 + console.warn( 244 + `⚠️ Missing user_email metadata in checkout session for user ${userId}.`, 245 + ); 52 246 } 53 247 } 54 248 }
+7 -7
app/components/cta/cta-section.tsx
··· 6 6 7 7 export function CTASection() { 8 8 return ( 9 - <section className="relative w-full bg-neutral-950 px-4 py-16 sm:px-6 sm:py-20"> 9 + <section className="relative w-full px-4 py-16 sm:px-6 sm:py-20"> 10 10 <div className="mx-auto flex max-w-5xl flex-col items-center gap-6 text-center sm:gap-7"> 11 11 <div className="flex items-center gap-2 text-white/80"> 12 12 <Image ··· 24 24 as="h2" 25 25 className="text-2xl font-semibold tracking-tight text-white sm:text-3xl md:text-4xl" 26 26 > 27 - Try eny.space for Free - No Strings Attached. 27 + Explore your PDS like a real space, not just an API. 28 28 </Heading> 29 29 <Paragraph className="max-w-2xl text-sm text-white/70 sm:text-base"> 30 - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do 31 - eiusmod tempor incididunt ut labore et dolore magna aliqua. Deploy 32 - your first app or website in just a few minutes. 30 + Spin up a managed PDS in a few clicks and explore it through a clean, 31 + browser-based UI. See your posts, files and collections instead of raw 32 + JSON—then upgrade to dedicated hosting when you're ready. 33 33 </Paragraph> 34 34 35 35 <div className="mt-2 flex flex-wrap items-center justify-center gap-3"> ··· 38 38 className="border border-white/30 bg-transparent px-6 text-xs font-semibold uppercase tracking-wide text-white hover:border-white hover:bg-white/10" 39 39 endIcon={<ArrowRightIcon className="size-4" aria-hidden />} 40 40 > 41 - Placeholder 41 + Open PDS UI demo 42 42 </ButtonLink> 43 43 <ButtonLink 44 44 href="/signup" 45 - className="px-6 text-xs font-semibold uppercase tracking-wide text-neutral-950 shadow-[0_0_40px_rgba(190,242,100,0.45)] bg-amber-400 hover:bg-amber-300" 45 + className="px-6 text-xs font-semibold uppercase tracking-wide text-neutral-950 shadow-[0_0_40px_rgba(232,121,249,0.45)] bg-fuchsia-400 hover:bg-fuchsia-300" 46 46 endIcon={<ArrowRightIcon className="size-4" aria-hidden />} 47 47 > 48 48 Start free trial
+19 -18
app/components/faq/faq-section.tsx
··· 13 13 14 14 const FAQ_ITEMS: FaqItem[] = [ 15 15 { 16 - id: "web3-vs-traditional", 17 - question: "Why is your Web3 hosting better than traditional hosting?", 16 + id: "what-is-pds", 17 + question: "What is a Personal Data Server (PDS)?", 18 18 answer: 19 - "Unlike traditional hosting, eny.space offers decentralized infrastructure designed for uptime, security, and scalability. Your projects are backed by blockchain-based guarantees, reducing single points of failure and giving you the resilience you need to grow.", 19 + "A PDS is the place where your data and identity for the AT Protocol live. Instead of being locked into one platform, your posts, media and profile are stored on a server you control - and eny.space makes running that server manageable through a simple dashboard.", 20 20 }, 21 21 { 22 - id: "choose-plan", 23 - question: "How do I know which pricing plan is right for me?", 22 + id: "do-i-need-own-server", 23 + question: "Do I need to understand Kubernetes, Docker or cloud hosting?", 24 24 answer: 25 - "Start with the plan that matches your expected storage and traffic. You can upgrade at any time without downtime, and our team can help you right-size based on your current and projected usage.", 25 + "No. The whole point of eny.space is managed PDS hosting: you click to create a PDS and we take care of the underlying infrastructure. You can still bring your own domain and adjust settings, but you never have to touch kubectl or obscure cloud dashboards.", 26 26 }, 27 27 { 28 - id: "secure-platform", 29 - question: "What makes your platform secure for hosting my Web3 project?", 28 + id: "pds-browser-free", 29 + question: "Is the PDS browser UI free to use?", 30 30 answer: 31 - "We combine audited smart contract infrastructure with strong network isolation, encryption in transit and at rest, and continuous monitoring. This layered approach helps protect your data and on-chain assets from common attack vectors.", 31 + "The PDS explorer UI is designed to be freely accessible for browsing public data on your PDS - similar to how you might explore content on Bluesky today. Managed hosting, dedicated resources and custom domains sit on top as paid features when you want your own isolated space.", 32 32 }, 33 33 { 34 - id: "switch-plans", 35 - question: "Can I switch plans later if my needs change?", 34 + id: "billing-and-payments", 35 + question: "How do billing and payments work for eny.space?", 36 36 answer: 37 - "Yes. You can move between plans at any time. Billing is prorated, and your deployments stay online during the switch so you can scale up or down without interruptions.", 37 + "We integrate with modern payment providers so you can subscribe in a few clicks. Behind the scenes, we handle invoices, taxes and payouts for you, so you only see a clear monthly charge for your plan instead of having to reconcile every PDS user manually.", 38 38 }, 39 39 { 40 - id: "time-to-deploy", 41 - question: "How soon can I deploy my Web3 project on eny.space?", 40 + id: "who-is-it-for", 41 + question: "Who is eny.space built for?", 42 42 answer: 43 - "Most teams deploy in minutes. Connect your wallet or Git repository, choose a plan, and follow the guided setup. Our onboarding flow is optimized so you can go from zero to live as quickly as possible.", 43 + "eny.space is aimed at AT Protocol and Bluesky power users, indie developers and communities who want their own PDS without becoming infrastructure engineers. If you care about owning your data and having a clear UI to manage it, you are our target audience.", 44 44 }, 45 45 ]; 46 46 ··· 50 50 return ( 51 51 <section 52 52 id="faq" 53 - className="relative w-full bg-neutral-950 px-4 py-20 sm:px-6 sm:py-24" 53 + className="relative w-full px-4 py-20 sm:px-6 sm:py-24" 54 54 > 55 55 <div className="mx-auto max-w-4xl text-center"> 56 56 <Heading ··· 60 60 Frequently Asked Questions 61 61 </Heading> 62 62 <Paragraph className="mt-4 text-sm text-white/70 sm:text-base"> 63 - All the details you need about the product and billing. If you can't 64 - find what you're looking for, feel free to{" "} 63 + Answers to the most common questions about managed PDS hosting and the 64 + eny.space PDS browser. If you can't find what you're looking for, feel 65 + free to{" "} 65 66 <span className="font-semibold text-white"> 66 67 reach out to our friendly team 67 68 </span>
+1 -1
app/components/features/feature-card.tsx
··· 22 22 className, 23 23 )} 24 24 > 25 - <div className="mb-1 inline-flex size-10 items-center justify-center rounded-xl bg-amber-400/15 text-amber-300 shadow-[0_0_30px_rgba(190,242,100,0.6)]"> 25 + <div className="mb-1 inline-flex size-10 items-center justify-center rounded-xl bg-fuchsia-400/15 text-fuchsia-300 shadow-[0_0_30px_rgba(232,121,249,0.6)]"> 26 26 {icon} 27 27 </div> 28 28 <Heading
+15 -20
app/components/features/features-section.tsx
··· 12 12 13 13 const FEATURES = [ 14 14 { 15 - title: "Example Feature One.", 15 + title: "One‑click managed PDS.", 16 16 description: 17 - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus efficitur urna a augue pulvinar, vitae facilisis massa dictum.", 17 + "Create a Personal Data Server directly from the eny.space UI—no Kubernetes clusters, Docker images or cloud consoles to wire up. Choose your settings, attach a domain and go live in minutes.", 18 18 icon: <NetworkIcon className="size-5" aria-hidden />, 19 19 }, 20 20 { 21 - title: "Sample Multi-Network.", 21 + title: "Dashboard‑first administration.", 22 22 description: 23 - "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti.", 23 + "Invite users to your PDS, manage access and roles, and handle tasks you would normally script on the command line from a clear web dashboard.", 24 24 icon: <CloudIcon className="size-5" aria-hidden />, 25 25 }, 26 26 { 27 - title: "Lorem Scalability.", 27 + title: "Live resource insights.", 28 28 description: 29 - "Suspendisse vitae dictum lectus. Ut bibendum, leo eu tempor ullamcorper, eros ex euismod nunc, nec cursus nisi ex non purus.", 29 + "Track active users, storage, CPU and RAM as intuitive percentages so you instantly see how loaded your server is, instead of digging through logs.", 30 30 icon: <ScaleIcon className="size-5" aria-hidden />, 31 31 }, 32 32 { 33 - title: "Secure Example.", 33 + title: "AT Protocol‑native access.", 34 34 description: 35 - "Etiam egestas, erat sit amet dictum cursus, sapien quam gravida ex, nec imperdiet libero sem at sapien faucibus consequat.", 35 + "Built for AT Protocol and Bluesky power users who arrive with an email and handle, not yet another social login. Your PDS becomes your social cloud space.", 36 36 icon: <ShieldCheckIcon className="size-5" aria-hidden />, 37 37 }, 38 - // { 39 - // title: "Instant Launchpad.", 40 - // description: 41 - // "Quisque congue elit eu velit maximus auctor. Nullam ac mauris quam. Nullam eget erat convallis, consequat purus non, cursus enim.", 42 - // icon: <RocketIcon className="size-5" aria-hidden />, 43 - // }, 44 38 { 45 - title: "Performance Demo.", 39 + title: "PDS explorer UI.", 46 40 description: 47 - "Praesent iaculis urna non eros pretium, in posuere lectus cursus. Sed facilisis facilisis ex, ac molestie tellus posuere vitae.", 41 + "Browse everything on your PDS like a file explorer—posts, images, PDFs and collections. See what’s stored, which apps use it and treat your PDS as a real, visual space.", 48 42 icon: <ZapIcon className="size-5" aria-hidden />, 49 43 }, 50 44 ]; ··· 53 47 return ( 54 48 <section 55 49 id="features" 56 - className="relative w-full bg-neutral-950 px-4 py-16 sm:px-6 sm:py-20" 50 + className="relative w-full px-4 py-16 sm:px-6 sm:py-20" 57 51 > 58 52 <div className="mx-auto max-w-5xl text-center"> 59 53 <Heading 60 54 as="h2" 61 55 className="text-2xl font-semibold tracking-tight text-white sm:text-3xl md:text-4xl" 62 56 > 63 - Lorem Ipsum Dolor Sit Amet - Fast, Reliable, Easy. 57 + Managed PDS hosting for AT Protocol power users. 64 58 </Heading> 65 59 <Paragraph className="mt-4 text-sm text-white/70 sm:text-base"> 66 - Pellentesque habitant morbi tristique senectus et netus et malesuada 67 - fames ac turpis egestas. Proin facilisis nec erat eu molestie. 60 + Create, monitor and manage your Personal Data Server from a single 61 + dashboard. eny.space automates the infrastructure so you can focus on 62 + your data, apps and community. 68 63 </Paragraph> 69 64 </div> 70 65
+1 -1
app/components/footer/footer.tsx
··· 6 6 const year = new Date().getFullYear(); 7 7 8 8 return ( 9 - <footer className="w-full border-t border-white/10 bg-neutral-950"> 9 + <footer className="w-full border-t border-white/10 bg-slate-950/90"> 10 10 <div className="mx-auto flex max-w-7xl flex-col gap-6 px-4 py-8 sm:flex-row sm:items-center sm:justify-between sm:px-6"> 11 11 <div className="flex items-center gap-3"> 12 12 <Image
+1 -1
app/components/heading.tsx
··· 9 9 } 10 10 11 11 export function Heading({ as: Tag = "h1", children, className }: HeadingProps) { 12 - return <Tag className={cn(className)}>{children}</Tag>; 12 + return <Tag className={cn("font-heading", className)}>{children}</Tag>; 13 13 }
+185 -28
app/components/hero/hero-background.tsx
··· 1 + /* eslint-disable tailwindcss/no-custom-classname */ 2 + "use client"; 3 + 1 4 import Image from "next/image"; 5 + import { motion, useScroll, useTransform } from "framer-motion"; 2 6 3 - export function HeroBackground() { 7 + export function HeroBackground({ 8 + showPlanets = true, 9 + }: { 10 + showPlanets?: boolean; 11 + }) { 12 + const { scrollYProgress } = useScroll(); 13 + 14 + // Parallax ratios (approximate 10%, 30%, 80%) 15 + const deepStarsY = useTransform(scrollYProgress, [0, 1], ["0%", "-10%"]); 16 + const midOrbsY = useTransform(scrollYProgress, [0, 1], ["0%", "-30%"]); 17 + const planetY = useTransform( 18 + scrollYProgress, 19 + [0, 0.6, 1], 20 + ["0%", "-48%", "-64%"], 21 + ); 22 + 23 + // "Camera angle" milestone – planets grow and shift between 0.4–0.6 24 + const planetScale = useTransform( 25 + scrollYProgress, 26 + [0, 0.4, 0.6, 1], 27 + [1, 1, 2, 2.4], 28 + ); 29 + const planetX = useTransform( 30 + scrollYProgress, 31 + [0, 0.4, 0.6, 1], 32 + ["0%", "0%", "-10%", "-14%"], 33 + ); 34 + const planetRotate = useTransform( 35 + scrollYProgress, 36 + [0, 0.5, 1], 37 + ["-16deg", "0deg", "16deg"], 38 + ); 39 + 4 40 return ( 5 - <div 6 - className="absolute inset-0 -z-10 bg-neutral-950" 7 - aria-hidden 8 - > 9 - {/* Gradient from dark top to warm orange-yellow bottom */} 41 + <> 42 + {/* Base fixed gradient background */} 10 43 <div 11 - className="absolute inset-0 opacity-90" 12 - style={{ 13 - background: 14 - "linear-gradient(180deg, transparent 0%, transparent 45%, oklch(0.75 0.12 75 / 0.4) 85%, oklch(0.8 0.14 85 / 0.6) 100%)", 15 - }} 16 - /> 17 - {/* Subtle grain overlay */} 18 - <div 19 - className="absolute inset-0 opacity-[0.03] mix-blend-overlay" 20 - style={{ 21 - backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`, 22 - }} 23 - /> 24 - {/* Large decorative logo - subtle, upper center */} 25 - <div className="absolute left-1/2 top-[20%] -translate-x-1/2 opacity-20"> 26 - <Image 27 - src="/logo.svg" 28 - alt="" 29 - width={480} 30 - height={480} 31 - className="shrink-0" 44 + className="pointer-events-none fixed inset-0 -z-20 bg-slate-950" 45 + aria-hidden 46 + > 47 + <div 48 + className="absolute inset-0 opacity-80" 49 + style={{ 50 + background: 51 + "radial-gradient(circle at 20% -10%, oklch(0.78 0.15 260 / 0.45) 0, transparent 55%)", 52 + }} 53 + /> 54 + <div 55 + className="absolute inset-0 opacity-[0.035] mix-blend-overlay" 56 + style={{ 57 + backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.9'/%3E%3C/svg%3E")`, 58 + }} 32 59 /> 33 60 </div> 34 - </div> 61 + 62 + {/* LAYER 1 – Deep tiny stars */} 63 + <motion.div 64 + className="pointer-events-none fixed inset-0 -z-10" 65 + style={{ y: deepStarsY }} 66 + aria-hidden 67 + > 68 + <svg 69 + className="h-full w-full" 70 + viewBox="0 0 1440 900" 71 + preserveAspectRatio="xMidYMid slice" 72 + > 73 + <defs> 74 + <radialGradient id="starGradient" r="1"> 75 + <stop offset="0%" stopColor="white" stopOpacity="0.9" /> 76 + <stop offset="100%" stopColor="white" stopOpacity="0" /> 77 + </radialGradient> 78 + </defs> 79 + 80 + {Array.from({ length: 90 }).map((_, i) => { 81 + const radius = Math.random() * 1.3 + 0.4; 82 + const blur = i % 7 === 0 ? 1.5 : 0; 83 + return ( 84 + <circle 85 + key={i} 86 + cx={Math.random() * 1440} 87 + cy={Math.random() * 900} 88 + r={radius} 89 + fill="url(#starGradient)" 90 + opacity={0.5 + Math.random() * 0.4} 91 + style={{ 92 + filter: blur ? `blur(${blur}px)` : undefined, 93 + }} 94 + /> 95 + ); 96 + })} 97 + </svg> 98 + </motion.div> 99 + 100 + {/* LAYER 2 – Mid orbs / energy fields */} 101 + <motion.div 102 + className="pointer-events-none fixed inset-0 -z-5" 103 + style={{ y: midOrbsY }} 104 + aria-hidden 105 + > 106 + <div className="pointer-events-none fixed inset-0 -z-5 [mask-image:linear-gradient(to_bottom,transparent,black_14%,black_86%,transparent)] [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_14%,black_86%,transparent)]"> 107 + <div className="absolute left-[12%] top-[18%] h-40 w-40 rounded-full bg-sky-400/10 blur-3xl shadow-[0_0_120px_rgba(56,189,248,0.55)]" /> 108 + <div className="absolute right-[10%] top-[35%] h-56 w-56 rounded-full bg-fuchsia-400/15 blur-3xl shadow-[0_0_160px_rgba(244,114,182,0.65)]" /> 109 + <div className="absolute left-[30%] bottom-[20%] h-48 w-64 rounded-[999px] bg-indigo-400/10 blur-[80px] shadow-[0_0_180px_rgba(129,140,248,0.6)]" /> 110 + <div className="absolute right-[26%] bottom-[8%] h-32 w-32 rounded-full bg-amber-300/15 blur-3xl shadow-[0_0_130px_rgba(252,211,77,0.7)]" /> 111 + 112 + {/* Soft vignette */} 113 + <div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0,transparent_55%,rgba(0,0,0,0.55)_100%)]" /> 114 + </div> 115 + </motion.div> 116 + 117 + {showPlanets && ( 118 + <> 119 + {/* LAYER 3 – Foreground planet (Jupiter image) */} 120 + <motion.div 121 + className="pointer-events-none fixed inset-0 -z-[1]" 122 + style={{ 123 + y: planetY, 124 + scale: planetScale, 125 + x: planetX, 126 + rotate: planetRotate, 127 + }} 128 + aria-hidden 129 + > 130 + <div className="absolute bottom-[-22%] right-[2%] h-[360px] w-[360px] overflow-hidden rounded-full opacity-80"> 131 + <Image 132 + src="/jupiter2.png" 133 + alt="Gas giant planet" 134 + fill 135 + priority 136 + className="object-cover object-center" 137 + /> 138 + </div> 139 + </motion.div> 140 + 141 + {/* LAYER 3b – Secondary planets (Earth, Mars, Venus, Neptune images) */} 142 + <motion.div 143 + className="pointer-events-none fixed inset-0 -z-[1]" 144 + style={{ 145 + y: planetY, 146 + scale: planetScale, 147 + x: planetX, 148 + rotate: planetRotate, 149 + }} 150 + aria-hidden 151 + > 152 + <div className="absolute bottom-[10%] left-[6%] h-[220px] w-[220px] overflow-hidden rounded-full"> 153 + <Image 154 + src="/earth.png" 155 + alt="Blue planet" 156 + fill 157 + priority={false} 158 + className="object-cover object-center" 159 + /> 160 + </div> 161 + <div className="absolute top-[54%] right-[30%] h-[120px] w-[120px] overflow-hidden rounded-full opacity-80"> 162 + <Image 163 + src="/mars.png" 164 + alt="Red planet" 165 + fill 166 + priority={false} 167 + className="object-cover object-center" 168 + /> 169 + </div> 170 + <div className="absolute top-[50%] right-[20%] h-[70px] w-[100px] overflow-hidden rounded-full opacity-80"> 171 + <Image 172 + src="/venus.png" 173 + alt="Yellow planet" 174 + fill 175 + priority={false} 176 + className="object-cover object-center" 177 + /> 178 + </div> 179 + <div className="absolute top-[30%] left-[35%] h-[60px] w-[60px] overflow-hidden rounded-full opacity-80"> 180 + <Image 181 + src="/neptune.png" 182 + alt="Blue planet" 183 + fill 184 + priority={false} 185 + className="object-cover object-center" 186 + /> 187 + </div> 188 + </motion.div> 189 + </> 190 + )} 191 + </> 35 192 ); 36 193 }
+5 -13
app/components/hero/hero-left.tsx
··· 8 8 <div className="flex max-w-xl flex-col gap-6"> 9 9 <Heading 10 10 as="h1" 11 - className="text-4xl leading-tight tracking-tight text-white sm:text-5xl md:text-6xl" 11 + className="font-heading text-4xl leading-tight tracking-tight text-white sm:text-5xl md:text-6xl" 12 12 > 13 - Lorem ipsum dolor sit amet 13 + Managed PDS hosting, launching soon. 14 14 </Heading> 15 15 <Paragraph className="text-lg text-white/90 sm:text-xl"> 16 - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod 17 - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 18 - veniam. 16 + eny.space will let you run your own Personal Data Server with a real UI. 17 + For now, sign up and we&apos;ll notify you when packages go live. 19 18 </Paragraph> 20 19 <div className="flex flex-wrap gap-3"> 21 20 <ButtonLink ··· 23 22 className="border border-white/80 bg-transparent uppercase tracking-wide text-white hover:bg-white/10 hover:border-white focus-visible:ring-white/50" 24 23 endIcon={<ArrowUpRightIcon className="size-4" aria-hidden />} 25 24 > 26 - Button 1 27 - </ButtonLink> 28 - <ButtonLink 29 - href="/dashboard" 30 - className="border border-white/80 bg-transparent uppercase tracking-wide text-white hover:bg-white/10 hover:border-white focus-visible:ring-white/50" 31 - endIcon={<ArrowUpRightIcon className="size-4" aria-hidden />} 32 - > 33 - Button 2 25 + Get started 34 26 </ButtonLink> 35 27 </div> 36 28 </div>
+14 -2
app/components/hero/hero.tsx
··· 1 - import { HeroBackground } from "./hero-background"; 1 + import Image from "next/image"; 2 2 import { HeroLeft } from "./hero-left"; 3 3 import { HeroRight } from "./hero-right"; 4 4 5 5 export function Hero() { 6 6 return ( 7 7 <section className="relative flex min-h-[calc(100vh-3.5rem)] w-full flex-col justify-end overflow-hidden px-4 pb-12 pt-16 sm:px-6 sm:pt-24"> 8 - <HeroBackground /> 8 + {/* Centered hero logo, moved higher with -mt-24 (negative margin top) */} 9 + <div 10 + className="pointer-events-none absolute inset-0 flex items-center justify-center opacity-15 -mt-24" 11 + style={{ zIndex: 1 }} 12 + > 13 + <Image 14 + src="/logo.svg" 15 + alt="eny.space logo" 16 + width={520} 17 + height={520} 18 + className="shrink-0" 19 + /> 20 + </div> 9 21 <div className="mx-auto grid w-full max-w-7xl gap-12 md:grid-cols-[1fr_auto] md:items-center md:gap-16"> 10 22 <HeroLeft /> 11 23 <HeroRight />
+6 -17
app/components/logo-bar/logo-bar.tsx
··· 22 22 23 23 function LogoBarItem({ 24 24 item, 25 - index, 26 25 }: { 27 26 item: (typeof PLACEHOLDER_LINKS)[number]; 28 - index: number; 29 27 }) { 30 28 const Icon = item.icon; 31 29 return ( ··· 43 41 44 42 function LogoBarScroll() { 45 43 return ( 46 - <div className="relative w-full overflow-hidden py-8"> 47 - <div 48 - className="pointer-events-none absolute inset-y-0 left-0 w-24 bg-gradient-to-r from-neutral-950 to-transparent z-10" 49 - aria-hidden="true" 50 - /> 51 - <div 52 - className="pointer-events-none absolute inset-y-0 right-0 w-24 bg-gradient-to-l from-neutral-950 to-transparent z-10" 53 - aria-hidden="true" 54 - /> 44 + <div className="relative w-full overflow-hidden py-8 [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]"> 55 45 <div className="flex w-max animate-marquee items-center [transform:translateZ(0)]"> 56 46 {["a", "b"].map((blockId) => ( 57 47 <div ··· 59 49 className="flex shrink-0 items-center gap-16 whitespace-nowrap" 60 50 > 61 51 {PLACEHOLDER_LINKS.slice(0, 6).map((item, i) => ( 62 - <LogoBarItem key={`${blockId}-${i}`} item={item} index={i} /> 52 + <LogoBarItem key={`${blockId}-${i}`} item={item} /> 63 53 ))} 64 54 <div className="flex shrink-0 items-center"> 65 55 <LogoBarItem 66 56 key={`${blockId}-6`} 67 57 item={PLACEHOLDER_LINKS[6]} 68 - index={6} 69 58 /> 70 59 <span className="w-16 shrink-0" aria-hidden /> 71 60 </div> ··· 78 67 79 68 export function LogoBar() { 80 69 return ( 81 - <section className="w-full bg-neutral-950"> 70 + <section className="w-full"> 82 71 <div className="mx-auto max-w-7xl px-4 py-10 sm:px-6"> 83 72 <Paragraph className="mx-auto max-w-2xl text-center text-base text-white/60"> 84 - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do 85 - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 86 - minim veniam, quis nostrud exercitation. 73 + Built for AT Protocol and Bluesky power users who want to see and 74 + manage their Personal Data Server through a modern interface instead 75 + of raw APIs and command‑line tools. 87 76 </Paragraph> 88 77 <LogoBarScroll /> 89 78 </div>
+138 -82
app/components/pricing/pricing-section.tsx
··· 7 7 import { ButtonLink } from "@/components/button-link"; 8 8 import { Heading } from "@/components/heading"; 9 9 import { Paragraph } from "@/components/paragraph"; 10 + import { prelaunch } from "@/lib/prelaunch"; 11 + import { getStripePlanAmounts, type PlanKey } from "@/lib/stripe-plans"; 10 12 11 13 type PricingPlan = { 14 + key: string; 12 15 name: string; 13 16 price: string; 14 17 period: string; 15 18 badge?: string; 16 19 description: string; 17 20 highlight?: boolean; 21 + pdsDiskSizeGb: number; 18 22 features: string[]; 23 + launchOnly?: boolean; 19 24 }; 20 25 21 26 const PLANS: PricingPlan[] = [ 22 27 { 23 - name: "Starter plan", 28 + key: "personal", 29 + name: "Personal", 24 30 price: "$19", 25 31 period: "per month", 26 32 description: "Perfect for small projects.", 33 + pdsDiskSizeGb: 10, 27 34 features: [ 28 35 "1 GB storage", 29 36 "5 app deployments", ··· 32 39 ], 33 40 }, 34 41 { 35 - name: "Growth plan", 42 + key: "community", 43 + name: "Community", 36 44 price: "$49", 37 45 period: "per month", 38 46 badge: "Popular", 39 47 description: "Scale without limits.", 40 48 highlight: true, 49 + pdsDiskSizeGb: 50, 41 50 features: [ 42 51 "10 GB storage", 43 52 "Unlimited app deployments", ··· 46 55 ], 47 56 }, 48 57 { 49 - name: "Pro plan", 58 + key: "business", 59 + name: "Business", 50 60 price: "$99", 51 61 period: "per month", 52 62 description: "Enterprise‑level performance.", 63 + pdsDiskSizeGb: 200, 64 + launchOnly: true, 53 65 features: [ 54 66 "Unlimited storage", 55 67 "Custom domain support", ··· 60 72 }, 61 73 ]; 62 74 63 - export function PricingSection() { 75 + function formatStripePrice(unitAmount: number | null, currency: string | null) { 76 + if (!unitAmount || !currency) return null; 77 + 78 + const normalized = currency.toLowerCase(); 79 + const zeroDecimalCurrencies = new Set([ 80 + "jpy", 81 + "krw", 82 + "clp", 83 + "vnd", 84 + "idr", 85 + "huf", 86 + "pyg", 87 + ]); 88 + const decimals = zeroDecimalCurrencies.has(normalized) ? 0 : 2; 89 + const major = unitAmount / 10 ** decimals; 90 + 91 + try { 92 + return new Intl.NumberFormat(undefined, { 93 + style: "currency", 94 + currency: normalized.toUpperCase(), 95 + maximumFractionDigits: decimals, 96 + }).format(major); 97 + } catch { 98 + return null; 99 + } 100 + } 101 + 102 + export async function PricingSection() { 103 + // Hide the pricing block entirely during prelaunch mode. 104 + // This keeps the "prelaunch vs launch" behavior controlled by one global flag. 105 + if (prelaunch) return null; 106 + 107 + const stripeAmounts = await getStripePlanAmounts(); 108 + 64 109 return ( 65 110 <section 66 111 id="pricing" 67 - className="relative w-full bg-neutral-950 px-4 py-20 sm:px-6 sm:py-24" 112 + className="relative w-full px-4 py-20 sm:px-6 sm:py-24" 68 113 > 69 114 <div className="mx-auto max-w-5xl text-center"> 70 115 <Heading ··· 80 125 </div> 81 126 82 127 <div className="mx-auto mt-12 grid max-w-6xl gap-6 md:grid-cols-3"> 83 - {PLANS.map((plan) => ( 84 - <Card 85 - key={plan.name} 86 - className={[ 87 - "flex h-full flex-col justify-between rounded-3xl border-none bg-gradient-to-b from-neutral-800/90 to-neutral-900/90 p-6 text-white shadow-xl/30", 88 - plan.highlight 89 - ? "relative bg-gradient-to-b from-amber-400/90 via-amber-400/80 to-amber-500/90 text-neutral-950 shadow-[0_0_40px_rgba(190,242,100,0.4)]" 90 - : "", 91 - ] 92 - .filter(Boolean) 93 - .join(" ")} 94 - > 95 - <CardHeader className="flex flex-col gap-2 px-0"> 96 - <div className="flex items-center justify-between gap-3"> 97 - <CardTitle className="text-sm font-semibold uppercase tracking-wide"> 98 - {plan.name} 99 - </CardTitle> 100 - {plan.badge ? ( 101 - <span className="rounded-full bg-neutral-900/80 px-3 py-1 text-xs font-medium uppercase tracking-wide text-amber-300 md:text-[11px]"> 102 - {plan.badge} 103 - </span> 104 - ) : null} 105 - </div> 106 - <div className="mt-4 flex items-baseline gap-2"> 107 - <span className="text-4xl font-semibold sm:text-5xl"> 108 - {plan.price} 109 - </span> 110 - <span className="text-sm font-medium opacity-80"> 111 - {plan.period} 112 - </span> 113 - </div> 114 - <Paragraph 115 - className={[ 116 - "mt-3 text-sm", 117 - plan.highlight ? "text-neutral-900/80" : "text-white/70", 118 - ].join(" ")} 119 - > 120 - {plan.description} 121 - </Paragraph> 122 - </CardHeader> 128 + {PLANS.map((plan) => 129 + (() => { 130 + const params = new URLSearchParams({ 131 + auto_checkout: "1", 132 + pds_plan: plan.key, 133 + pds_disksize_gb: String(plan.pdsDiskSizeGb), 134 + }); 135 + const signupHref = `/signup?${params.toString()}`; 123 136 124 - <CardContent className="mt-6 flex flex-1 flex-col gap-6 px-0"> 125 - <ButtonLink 126 - href="/signup" 137 + return ( 138 + <Card 139 + key={plan.name} 127 140 className={[ 128 - "w-full rounded-full px-4 py-3 text-center text-sm font-semibold uppercase tracking-wide transition", 141 + "flex h-full flex-col justify-between rounded-3xl border-none bg-gradient-to-b from-slate-900/65 to-slate-950/75 p-6 text-white shadow-xl/30", 129 142 plan.highlight 130 - ? "bg-neutral-950 text-amber-300 hover:bg-neutral-900" 131 - : "bg-white text-neutral-950 hover:bg-neutral-200", 132 - ].join(" ")} 143 + ? "relative bg-gradient-to-b from-fuchsia-500/65 via-fuchsia-500/55 to-fuchsia-600/70 text-white shadow-[0_0_36px_rgba(232,121,249,0.3)]" 144 + : "", 145 + ] 146 + .filter(Boolean) 147 + .join(" ")} 133 148 > 134 - Get started 135 - </ButtonLink> 149 + <CardHeader className="flex flex-col gap-2 px-0"> 150 + <div className="flex items-center justify-between gap-3"> 151 + <CardTitle className="text-sm font-semibold uppercase tracking-wide text-white"> 152 + {plan.name} 153 + </CardTitle> 154 + {plan.badge ? ( 155 + <span className="absolute right-[7%] rounded-full bg-neutral-900/80 px-3 py-1 text-xs font-medium uppercase tracking-wide text-fuchsia-300 md:text-[11px]"> 156 + {plan.badge} 157 + </span> 158 + ) : null} 159 + </div> 160 + <div className="mt-4 flex items-baseline gap-2"> 161 + <span className="text-4xl font-semibold sm:text-5xl text-white"> 162 + {(() => { 163 + const key = plan.key as PlanKey; 164 + const fromStripe = formatStripePrice( 165 + stripeAmounts[key]?.unitAmount ?? null, 166 + stripeAmounts[key]?.currency ?? null, 167 + ); 168 + return fromStripe ?? plan.price; 169 + })()} 170 + </span> 171 + <span className="text-sm font-medium opacity-80 text-white"> 172 + {plan.period} 173 + </span> 174 + </div> 175 + <Paragraph 176 + className={["mt-3 text-sm text-white/70"].join(" ")} 177 + > 178 + {plan.description} 179 + </Paragraph> 180 + </CardHeader> 136 181 137 - <div className="pt-2 text-left"> 138 - <Paragraph 139 - className={[ 140 - "text-xs font-semibold uppercase tracking-[0.18em]", 141 - plan.highlight ? "text-neutral-900/70" : "text-white/60", 142 - ].join(" ")} 143 - > 144 - Key features on {plan.name.split(" ")[0]} 145 - </Paragraph> 146 - <ul className="mt-4 space-y-2 text-sm"> 147 - {plan.features.map((feature) => ( 148 - <li 149 - key={feature} 182 + <CardContent className="mt-6 flex flex-1 flex-col gap-6 px-0"> 183 + <ButtonLink 184 + href={signupHref} 185 + className={[ 186 + "w-full rounded-full px-4 py-3 text-center text-sm font-semibold uppercase tracking-wide transition", 187 + plan.highlight 188 + ? "bg-neutral-950 text-fuchsia-200 hover:bg-neutral-900" 189 + : "bg-white text-neutral-950 hover:bg-neutral-200", 190 + ].join(" ")} 191 + > 192 + Get started 193 + </ButtonLink> 194 + 195 + <div className="pt-2 text-left"> 196 + <Paragraph 150 197 className={[ 151 - "flex items-start gap-2", 152 - plan.highlight 153 - ? "text-neutral-900/80" 154 - : "text-white/75", 198 + "text-xs font-semibold uppercase tracking-[0.18em] text-white/60", 155 199 ].join(" ")} 156 200 > 157 - <div className="inline-flex items-center gap-2"> 158 - <span className="inline-block size-1.5 rounded-full bg-amber-400" /> 159 - <span>{feature}</span> 160 - </div> 161 - </li> 162 - ))} 163 - </ul> 164 - </div> 165 - </CardContent> 166 - </Card> 167 - ))} 201 + Key features on {plan.name.split(" ")[0]} 202 + </Paragraph> 203 + <ul className="mt-4 space-y-2 text-sm"> 204 + {plan.features.map((feature) => ( 205 + <li 206 + key={feature} 207 + className={[ 208 + "flex items-start gap-2 text-white/75", 209 + ].join(" ")} 210 + > 211 + <div className="inline-flex items-center gap-2"> 212 + <span className="inline-block size-1.5 rounded-full bg-fuchsia-400" /> 213 + <span>{feature}</span> 214 + </div> 215 + </li> 216 + ))} 217 + </ul> 218 + </div> 219 + </CardContent> 220 + </Card> 221 + ); 222 + })(), 223 + )} 168 224 </div> 169 225 </section> 170 226 );
+12
app/components/site-background.tsx
··· 1 + "use client"; 2 + 3 + import { usePathname } from "next/navigation"; 4 + import { HeroBackground } from "@/components/hero"; 5 + 6 + export function SiteBackground() { 7 + const pathname = usePathname(); 8 + const isLanding = pathname === "/"; 9 + 10 + return <HeroBackground showPlanets={isLanding} />; 11 + } 12 +
+44
app/components/site-header/mobile-menu.tsx
··· 1 + import Link from "next/link"; 2 + import type { User } from "@supabase/supabase-js"; 3 + 4 + import { signOut } from "@/actions/auth"; 5 + 6 + interface MobileMenuProps { 7 + user: User | null; 8 + open: boolean; 9 + onClose: () => void; 10 + } 11 + 12 + const mobileLinkClass = 13 + "block text-left text-base font-medium text-white/90 hover:text-white"; 14 + 15 + export function MobileMenu({ user, open, onClose }: MobileMenuProps) { 16 + if (!open) return null; 17 + 18 + return ( 19 + <div className="fixed right-3 top-14 z-40 max-h-[calc(100vh-3.5rem)] w-1/3 overflow-y-auto rounded-xl border border-fuchsia-300/30 bg-slate-900/75 px-4 pb-4 pt-3 shadow-[0_0_30px_rgba(232,121,249,0.2)] backdrop-blur-md md:hidden"> 20 + <nav className="flex flex-col gap-2"> 21 + {/* navigation intentionally hidden; CTA-only header */} 22 + </nav> 23 + 24 + <div className="mt-3 flex flex-col gap-2"> 25 + {user ? ( 26 + <form action={signOut}> 27 + <button type="submit" className={mobileLinkClass}> 28 + Sign out 29 + </button> 30 + </form> 31 + ) : ( 32 + <> 33 + <Link href="/login" onClick={onClose} className={mobileLinkClass}> 34 + Login 35 + </Link> 36 + <Link href="/signup" onClick={onClose} className={mobileLinkClass}> 37 + Get started 38 + </Link> 39 + </> 40 + )} 41 + </div> 42 + </div> 43 + ); 44 + }
+103 -119
app/components/site-header/site-header.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 3 + import { useState, useEffect } from "react"; 4 4 import Link from "next/link"; 5 5 import Image from "next/image"; 6 6 import { signOut } from "@/actions/auth"; 7 7 import { Button } from "@/actions/components/ui/button"; 8 8 import { ArrowUpRightIcon, MenuIcon, XIcon } from "lucide-react"; 9 9 import type { User } from "@supabase/supabase-js"; 10 - 11 - const navLinkClass = 12 - "inline-flex h-9 items-center justify-center rounded-full bg-transparent px-4 py-2 text-xs font-medium uppercase tracking-wide text-white/90 transition-colors hover:bg-white/5 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"; 10 + import { MobileMenu } from "./mobile-menu"; 13 11 14 12 interface SiteHeaderProps { 15 13 user: User | null; 16 14 } 17 15 16 + const headerCtaClass = 17 + "inline-flex items-center gap-1.5 rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 cursor-pointer hover:bg-primary/80"; 18 + 18 19 export function SiteHeader({ user }: SiteHeaderProps) { 19 20 const [mobileOpen, setMobileOpen] = useState(false); 21 + const [displayText, setDisplayText] = useState("."); 22 + const [isTyping, setIsTyping] = useState(false); 20 23 21 - const closeMobile = () => setMobileOpen(false); 24 + useEffect(() => { 25 + let dotCycle = 1; 26 + let repeatCount = 0; 27 + const fullWord = "space"; 28 + let charIndex = 0; 29 + 30 + const DOT_SPEED = 400; 31 + const CURSOR_BLINK_SPEED = 500; 32 + const TYPE_SPEED = 200; 33 + 34 + const dotInterval = setInterval(() => { 35 + if (dotCycle < 3) { 36 + dotCycle++; 37 + } else { 38 + dotCycle = 1; 39 + repeatCount++; 40 + } 41 + 42 + if (repeatCount < 2) { 43 + setDisplayText(".".repeat(dotCycle)); 44 + } else { 45 + clearInterval(dotInterval); 46 + setDisplayText("."); 47 + 48 + let blinks = 0; 49 + const blinkInterval = setInterval(() => { 50 + setIsTyping((prev) => !prev); 51 + blinks++; 52 + 53 + if (blinks === 4) { 54 + clearInterval(blinkInterval); 55 + setIsTyping(true); 56 + 57 + const typeInterval = setInterval(() => { 58 + setDisplayText("." + fullWord.slice(0, charIndex + 1)); 59 + charIndex++; 60 + 61 + if (charIndex === fullWord.length) { 62 + clearInterval(typeInterval); 63 + setTimeout(() => setIsTyping(false), 400); 64 + } 65 + }, TYPE_SPEED); 66 + } 67 + }, CURSOR_BLINK_SPEED); 68 + } 69 + }, DOT_SPEED); 70 + 71 + return () => clearInterval(dotInterval); 72 + }, []); 22 73 23 74 return ( 24 - <header className="sticky top-0 z-50 w-full border-b border-white/10 bg-neutral-950"> 75 + <header className="sticky top-0 z-50 w-full border-b border-white/10 bg-slate-950/85"> 25 76 <div className="mx-auto flex h-14 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6"> 26 77 <Link 27 78 href="/" 28 - className="flex items-center gap-2 font-semibold text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950" 29 - onClick={closeMobile} 79 + className="flex items-center gap-2 font-semibold text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30" 80 + onClick={() => setMobileOpen(false)} 30 81 > 31 82 <Image 32 83 src="/logo.svg" ··· 35 86 height={40} 36 87 className="shrink-0" 37 88 /> 38 - <span className="text-lg">eny.space</span> 89 + 90 + {/* THE LAYOUT FIX IS HERE */} 91 + <div className="relative text-lg flex items-center tabular-nums"> 92 + {/* Hidden ghost text that reserves the full width of "eny.space" */} 93 + <span 94 + className="invisible pointer-events-none select-none" 95 + aria-hidden="true" 96 + > 97 + eny.space 98 + </span> 99 + 100 + {/* The actual visible animated text */} 101 + <div className="absolute left-0 flex items-center whitespace-nowrap"> 102 + <span>eny{displayText}</span> 103 + <span 104 + className={`ml-0.5 transition-opacity duration-100 ${isTyping ? "opacity-100" : "opacity-0"}`} 105 + > 106 + _ 107 + </span> 108 + </div> 109 + </div> 39 110 </Link> 40 111 41 - <nav className="hidden flex-1 justify-center gap-1 md:flex"> 42 - <Link href="/" className={navLinkClass}> 43 - Home 44 - </Link> 45 - {user && ( 46 - <Link href="/dashboard" className={navLinkClass}> 47 - Dashboard 48 - </Link> 49 - )} 50 - <Link href="/#about" className={navLinkClass}> 51 - About 52 - </Link> 53 - </nav> 54 - 55 112 <div className="hidden items-center gap-2 md:flex"> 56 - {user ? ( 113 + {!user ? ( 114 + <Button 115 + size="default" 116 + className={headerCtaClass} 117 + asChild 118 + > 119 + <Link href="/signup"> 120 + Get started 121 + <ArrowUpRightIcon className="ml-1 size-3.5" /> 122 + </Link> 123 + </Button> 124 + ) : ( 57 125 <form action={signOut}> 58 126 <Button 59 127 type="submit" 60 128 size="default" 61 - className="inline-flex items-center rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-white/90" 129 + className={headerCtaClass} 62 130 > 63 - <span>Sign out</span> 64 - <ArrowUpRightIcon className="ml-1 size-3.5" aria-hidden /> 131 + Sign out 132 + <ArrowUpRightIcon className="ml-0.5 size-3.5" /> 65 133 </Button> 66 134 </form> 67 - ) : ( 68 - <> 69 - <Button 70 - variant="outline" 71 - size="default" 72 - className="rounded-full border-white/20 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white/90 hover:bg-white/10 hover:text-white" 73 - asChild 74 - > 75 - <Link href="/login">Login</Link> 76 - </Button> 77 - <Button 78 - size="default" 79 - className="rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-white/90" 80 - asChild 81 - > 82 - <Link href="/signup"> 83 - Get started 84 - <ArrowUpRightIcon className="ml-1 size-3.5" aria-hidden /> 85 - </Link> 86 - </Button> 87 - </> 88 135 )} 89 136 </div> 90 137 91 138 <button 92 139 type="button" 93 140 aria-label="Toggle navigation" 94 - className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-white/20 text-white md:hidden" 141 + className="inline-flex h-9 w-9 cursor-pointer items-center justify-center text-white md:hidden" 95 142 onClick={() => setMobileOpen((open) => !open)} 96 143 > 97 144 {mobileOpen ? ( 98 - <XIcon className="size-4" aria-hidden /> 145 + <XIcon className="size-5" aria-hidden /> 99 146 ) : ( 100 - <MenuIcon className="size-4" aria-hidden /> 147 + <MenuIcon className="size-5" aria-hidden /> 101 148 )} 102 149 </button> 103 150 </div> 104 151 105 - {mobileOpen && ( 106 - <div className="border-t border-white/10 bg-neutral-950 px-4 pb-4 pt-3 md:hidden"> 107 - <nav className="flex flex-col gap-2"> 108 - <Link 109 - href="/" 110 - className="text-sm font-medium text-white/90" 111 - onClick={closeMobile} 112 - > 113 - Home 114 - </Link> 115 - {user && ( 116 - <Link 117 - href="/dashboard" 118 - className="text-sm font-medium text-white/90" 119 - onClick={closeMobile} 120 - > 121 - Dashboard 122 - </Link> 123 - )} 124 - <Link 125 - href="/#about" 126 - className="text-sm font-medium text-white/90" 127 - onClick={closeMobile} 128 - > 129 - About 130 - </Link> 131 - </nav> 132 - 133 - <div className="mt-3 flex flex-col gap-2"> 134 - {user ? ( 135 - <form action={signOut}> 136 - <Button 137 - type="submit" 138 - size="default" 139 - className="inline-flex h-9 items-center justify-center rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-white/90" 140 - > 141 - <span>Sign out</span> 142 - <ArrowUpRightIcon className="ml-1 size-3.5" aria-hidden /> 143 - </Button> 144 - </form> 145 - ) : ( 146 - <> 147 - <Button 148 - variant="outline" 149 - size="default" 150 - className="inline-flex h-9 w-full items-center justify-center rounded-full border-white/20 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white/90 hover:bg-white/10 hover:text-white" 151 - asChild 152 - > 153 - <Link href="/login" onClick={closeMobile}> 154 - Login 155 - </Link> 156 - </Button> 157 - <Button 158 - size="default" 159 - className="inline-flex h-9 w-full items-center justify-center rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-white/90" 160 - asChild 161 - > 162 - <Link href="/signup" onClick={closeMobile}> 163 - Get started 164 - <ArrowUpRightIcon className="ml-1 size-3.5" aria-hidden /> 165 - </Link> 166 - </Button> 167 - </> 168 - )} 169 - </div> 170 - </div> 171 - )} 152 + <MobileMenu 153 + user={user} 154 + open={mobileOpen} 155 + onClose={() => setMobileOpen(false)} 156 + /> 172 157 </header> 173 158 ); 174 159 } 175 -
+4 -12
app/components/testimonials/testimonials-section.tsx
··· 59 59 </div> 60 60 </div> 61 61 <div className="flex items-center gap-2 text-sm font-semibold text-white/85"> 62 - <span className="flex size-6 items-center justify-center rounded-full bg-amber-400/15"> 63 - <SparklesIcon className="size-3.5 text-amber-300" aria-hidden /> 62 + <span className="flex size-6 items-center justify-center rounded-full bg-fuchsia-400/15"> 63 + <SparklesIcon className="size-3.5 text-fuchsia-300" aria-hidden /> 64 64 </span> 65 65 <span>{testimonial.company}</span> 66 66 </div> ··· 71 71 72 72 export function TestimonialsSection() { 73 73 return ( 74 - <section className="relative w-full bg-neutral-950 px-4 py-20 sm:px-6 sm:py-24"> 74 + <section className="relative w-full px-4 py-20 sm:px-6 sm:py-24"> 75 75 <div className="mx-auto max-w-5xl text-center"> 76 76 <Heading 77 77 as="h2" ··· 85 85 </div> 86 86 87 87 <div className="mx-auto mt-12 max-w-6xl"> 88 - <div className="relative w-full overflow-hidden py-4"> 89 - <div 90 - className="pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-neutral-950 to-transparent z-10" 91 - aria-hidden="true" 92 - /> 93 - <div 94 - className="pointer-events-none absolute inset-y-0 right-0 w-16 bg-gradient-to-l from-neutral-950 to-transparent z-10" 95 - aria-hidden="true" 96 - /> 88 + <div className="relative w-full overflow-hidden pt-4 pb-8 [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]"> 97 89 <div className="flex w-max animate-marquee items-stretch gap-6 [transform:translateZ(0)]"> 98 90 {["a", "b"].map((blockId) => ( 99 91 <div
+289
app/dashboard/atproto-test-client.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useMemo, useState } from "react"; 4 + import { Heading } from "@/components/heading"; 5 + import { Paragraph } from "@/components/paragraph"; 6 + import { Button } from "@/actions/components/ui/button"; 7 + 8 + type ServiceResponse = { 9 + encrypted_config?: { 10 + hostname?: string; 11 + }; 12 + state?: number | string; 13 + }; 14 + 15 + function redactJwt(token: string | undefined) { 16 + if (!token) return ""; 17 + if (token.length <= 20) return token; 18 + return `${token.slice(0, 15)}...${token.slice(-10)}`; 19 + } 20 + 21 + function stripScheme(hostname?: string) { 22 + if (!hostname) return ""; 23 + return hostname.replace(/^https?:\/\//i, "").replace(/\/.*$/, ""); 24 + } 25 + 26 + export function AtprotoTestClient() { 27 + const [pdsHost, setPdsHost] = useState<string>(""); 28 + const [pdsState, setPdsState] = useState<number | string | null>(null); 29 + const [inviteCode, setInviteCode] = useState<string>(""); 30 + 31 + const [email, setEmail] = useState<string>(""); 32 + const [handle, setHandle] = useState<string>(""); 33 + const [newPassword, setNewPassword] = useState<string>(""); 34 + 35 + const [sessionIdentifier, setSessionIdentifier] = useState<string>(""); 36 + const [sessionPassword, setSessionPassword] = useState<string>(""); 37 + 38 + const [loading, setLoading] = useState(false); 39 + const [output, setOutput] = useState<any>(null); 40 + const [error, setError] = useState<string | null>(null); 41 + 42 + const pdsBareHost = useMemo(() => stripScheme(pdsHost), [pdsHost]); 43 + const pdsStateNum = useMemo(() => { 44 + if (pdsState === null) return null; 45 + const n = typeof pdsState === "number" ? pdsState : Number(pdsState); 46 + return Number.isFinite(n) ? n : null; 47 + }, [pdsState]); 48 + const isPdsReady = pdsStateNum !== null && pdsStateNum >= 3; 49 + const createAccountDisabled = 50 + loading || !inviteCode || !handle || !newPassword || !isPdsReady; 51 + const createSessionDisabled = 52 + loading || !handle || !newPassword || !isPdsReady; 53 + 54 + useEffect(() => { 55 + const load = async () => { 56 + try { 57 + const res = await fetch("/api/pds/service", { method: "GET" }); 58 + if (!res.ok) throw new Error(`Failed to load service (${res.status})`); 59 + const data = (await res.json()) as ServiceResponse; 60 + const host = data?.encrypted_config?.hostname || ""; 61 + setPdsHost(host); 62 + setPdsState(data?.state ?? null); 63 + 64 + if (!handle && host) { 65 + const bare = stripScheme(host); 66 + setHandle(`user1.${bare}`); 67 + setSessionIdentifier(`user1.${bare}`); 68 + } 69 + } catch (e) { 70 + setError(e instanceof Error ? e.message : "Unknown error"); 71 + } 72 + }; 73 + load(); 74 + }, []); 75 + 76 + const call = async (path: string, body: any) => { 77 + setLoading(true); 78 + setError(null); 79 + setOutput(null); 80 + try { 81 + const res = await fetch(path, { 82 + method: "POST", 83 + headers: { "Content-Type": "application/json" }, 84 + body: JSON.stringify(body), 85 + }); 86 + 87 + const contentType = res.headers.get("content-type") || ""; 88 + const payload = contentType.includes("application/json") 89 + ? await res.json() 90 + : await res.text().catch(() => ""); 91 + 92 + if (!res.ok) { 93 + if (typeof payload === "string") { 94 + throw new Error(payload); 95 + } 96 + 97 + // Prefer the nested upstream error if present. 98 + const upstreamMessage = 99 + payload?.payload?.message || 100 + payload?.payload?.error || 101 + payload?.message; 102 + 103 + const details = { 104 + route: path, 105 + httpStatus: res.status, 106 + upstream: payload?.payload, 107 + message: upstreamMessage || "Request failed", 108 + }; 109 + 110 + throw new Error(JSON.stringify(details, null, 2)); 111 + } 112 + 113 + return payload; 114 + } finally { 115 + setLoading(false); 116 + } 117 + }; 118 + 119 + const createInvite = async () => { 120 + const payload = await call("/api/pds/atproto/invite", { useCount: 1 }); 121 + const code = payload?.code || payload?.inviteCode || ""; 122 + setInviteCode(code); 123 + setOutput(payload); 124 + }; 125 + 126 + const createAccount = async () => { 127 + const payload = await call("/api/pds/atproto/create-account", { 128 + email, 129 + handle, 130 + password: newPassword, 131 + inviteCode, 132 + }); 133 + setOutput(payload); 134 + setSessionIdentifier(handle); 135 + setSessionPassword(newPassword); 136 + }; 137 + 138 + const createSession = async () => { 139 + const payload = await call("/api/pds/atproto/create-session", { 140 + identifier: sessionIdentifier || handle, 141 + password: sessionPassword || newPassword, 142 + }); 143 + setOutput(payload); 144 + }; 145 + 146 + return ( 147 + <section className="space-y-3 rounded-md border border-white/10 bg-white/5 p-4 text-white backdrop-blur-xl"> 148 + <Heading 149 + as="h2" 150 + className="text-sm font-semibold uppercase tracking-wide text-white/80" 151 + > 152 + AT Protocol (test) 153 + </Heading> 154 + 155 + {pdsHost ? ( 156 + <Paragraph className="text-sm text-white/70"> 157 + PDS endpoint:{" "} 158 + <span className="font-mono text-white">{pdsHost}</span> 159 + </Paragraph> 160 + ) : ( 161 + <Paragraph className="text-sm text-white/70">Loading PDS endpoint…</Paragraph> 162 + )} 163 + 164 + {pdsStateNum !== null && !isPdsReady && ( 165 + <Paragraph className="text-sm text-amber-100/90"> 166 + PDS not ready yet (state={pdsStateNum}). Waiting for provisioning to 167 + finish. 168 + </Paragraph> 169 + )} 170 + 171 + <div className="space-y-2 text-sm"> 172 + <div className="flex flex-wrap gap-2"> 173 + <Button 174 + onClick={createInvite} 175 + disabled={loading || !pdsBareHost || !isPdsReady} 176 + className="rounded-full" 177 + > 178 + {loading ? "Working..." : "Create invite"} 179 + </Button> 180 + </div> 181 + 182 + {inviteCode && ( 183 + <div className="space-y-1"> 184 + <Paragraph className="text-xs font-medium text-white/60"> 185 + Invite code 186 + </Paragraph> 187 + <Paragraph className="font-mono text-white break-all"> 188 + {inviteCode} 189 + </Paragraph> 190 + </div> 191 + )} 192 + </div> 193 + 194 + <div className="grid gap-3 md:grid-cols-2"> 195 + <label className="space-y-1"> 196 + <Paragraph className="text-xs font-medium text-white/60"> 197 + Email (optional) 198 + </Paragraph> 199 + <input 200 + value={email} 201 + onChange={(e) => setEmail(e.target.value)} 202 + className="w-full rounded-md border border-white/20 bg-transparent px-3 py-2 text-sm text-white placeholder:text-white/50" 203 + placeholder="leave blank to reuse your own email" 204 + /> 205 + </label> 206 + 207 + <label className="space-y-1"> 208 + <Paragraph className="text-xs font-medium text-white/60">Handle</Paragraph> 209 + <input 210 + value={handle} 211 + onChange={(e) => setHandle(e.target.value)} 212 + className="w-full rounded-md border border-white/20 bg-transparent px-3 py-2 text-sm text-white placeholder:text-white/50" 213 + placeholder={`user1.${pdsBareHost || "eny.k8s.frx.pub"}`} 214 + /> 215 + </label> 216 + 217 + <label className="space-y-1 md:col-span-2"> 218 + <Paragraph className="text-xs font-medium text-white/60"> 219 + Password 220 + </Paragraph> 221 + <input 222 + type="password" 223 + value={newPassword} 224 + onChange={(e) => setNewPassword(e.target.value)} 225 + className="w-full rounded-md border border-white/20 bg-transparent px-3 py-2 text-sm text-white placeholder:text-white/50" 226 + placeholder="new account password" 227 + /> 228 + </label> 229 + </div> 230 + 231 + <div className="flex flex-wrap gap-2"> 232 + <Button 233 + onClick={createAccount} 234 + disabled={createAccountDisabled} 235 + className={ 236 + createAccountDisabled 237 + ? "rounded-full bg-emerald-400/10 border border-emerald-200/10 opacity-60 cursor-not-allowed" 238 + : "rounded-full bg-emerald-400/20 hover:bg-emerald-400/35 border border-emerald-200/25" 239 + } 240 + > 241 + Create user 242 + </Button> 243 + <Button 244 + onClick={createSession} 245 + disabled={createSessionDisabled} 246 + className={ 247 + createSessionDisabled 248 + ? "rounded-full bg-sky-400/10 border border-sky-200/10 opacity-60 cursor-not-allowed" 249 + : "rounded-full bg-sky-400/20 hover:bg-sky-400/35 border border-sky-200/25" 250 + } 251 + > 252 + Login (create session) 253 + </Button> 254 + </div> 255 + 256 + {error && ( 257 + <Paragraph className="text-sm text-rose-100 bg-rose-950/40 rounded p-3"> 258 + {error} 259 + </Paragraph> 260 + )} 261 + 262 + {output && ( 263 + <div className="space-y-2"> 264 + <Paragraph className="text-xs font-medium text-white/60">Response</Paragraph> 265 + <pre className="max-h-64 overflow-auto rounded bg-neutral-900/90 p-3 text-xs text-neutral-100"> 266 + {output?.emailUsed && ( 267 + <Paragraph className="mb-2 text-xs text-white/70"> 268 + Email used:{" "} 269 + <span className="font-mono">{output.emailUsed}</span> 270 + </Paragraph> 271 + )} 272 + {output?.accessJwt 273 + ? JSON.stringify( 274 + { 275 + ...output, 276 + accessJwt: redactJwt(output.accessJwt), 277 + refreshJwt: redactJwt(output.refreshJwt), 278 + }, 279 + null, 280 + 2, 281 + ) 282 + : JSON.stringify(output, null, 2)} 283 + </pre> 284 + </div> 285 + )} 286 + </section> 287 + ); 288 + } 289 +
+156 -81
app/dashboard/dashboard-client.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 3 + import { useEffect, useMemo, useRef, useState } from "react"; 4 4 import { 5 5 createSubscriptionCheckout, 6 6 cancelSubscription, 7 7 resumeSubscription, 8 8 createBillingPortalSession, 9 9 } from "@/actions/subscription"; 10 + import { Heading } from "@/components/heading"; 11 + import { Paragraph } from "@/components/paragraph"; 12 + import { Button } from "@/actions/components/ui/button"; 10 13 11 14 interface DashboardClientProps { 12 15 subscribed: boolean; 13 16 subscription: any; 14 17 priceId: string; 18 + autoCheckoutFromPlan?: boolean; 19 + pdsPlan?: string; 20 + pdsUsername?: string; 21 + pdsHostname?: string; 22 + pdsDisksizeGb?: string; 15 23 } 16 24 17 25 export default function DashboardClient({ 18 26 subscribed, 19 27 subscription, 20 28 priceId, 29 + autoCheckoutFromPlan, 30 + pdsPlan, 31 + pdsUsername, 32 + pdsHostname, 33 + pdsDisksizeGb, 21 34 }: DashboardClientProps) { 22 35 const [loading, setLoading] = useState(false); 23 36 const [actionLoading, setActionLoading] = useState<string | null>(null); 37 + const hasAutoStartedCheckout = useRef(false); 38 + 39 + const planBasedDisksize = useMemo(() => { 40 + if (pdsDisksizeGb && Number(pdsDisksizeGb) > 0) { 41 + return Number(pdsDisksizeGb); 42 + } 43 + 44 + const p = (pdsPlan || "").toLowerCase(); 45 + if (p === "community" || p === "growth") return 50; 46 + if (p === "business" || p === "pro") return 200; 47 + return 10; 48 + }, [pdsDisksizeGb, pdsPlan]); 49 + 50 + const selectedUsername = pdsUsername || undefined; 51 + const selectedHostname = pdsHostname || undefined; 24 52 25 53 const handleSubscribe = async () => { 26 54 if (!priceId) { 27 55 alert( 28 - "Stripe price ID not configured. Please set NEXT_PUBLIC_STRIPE_PRICE_ID in your environment variables." 56 + "Stripe price ID not configured. Set NEXT_PUBLIC_STRIPE_PRICE_PERSONAL_ID (and COMMUNITY/BUSINESS) or NEXT_PUBLIC_STRIPE_PRICE_ID as fallback.", 29 57 ); 30 58 return; 31 59 } 32 60 33 61 setLoading(true); 34 62 try { 35 - const { url } = await createSubscriptionCheckout(priceId); 63 + const { url } = await createSubscriptionCheckout(priceId, { 64 + username: selectedUsername, 65 + hostname: selectedHostname, 66 + disksizeGb: planBasedDisksize, 67 + }); 36 68 if (url) { 37 69 window.location.href = url; 38 70 } ··· 44 76 } 45 77 }; 46 78 79 + useEffect(() => { 80 + if ( 81 + autoCheckoutFromPlan && 82 + !subscribed && 83 + !loading && 84 + !hasAutoStartedCheckout.current 85 + ) { 86 + hasAutoStartedCheckout.current = true; 87 + void handleSubscribe(); 88 + } 89 + }, [autoCheckoutFromPlan, subscribed, loading]); 90 + 47 91 const handleServerCall = async (endpoint: string) => { 48 92 try { 49 93 const response = await fetch(`/api/server/${endpoint}`, { ··· 60 104 } catch (error) { 61 105 console.error("Error making server call:", error); 62 106 alert( 63 - `Error: ${error instanceof Error ? error.message : "Unknown error"}` 107 + `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 64 108 ); 65 109 } 66 110 }; ··· 71 115 72 116 if (!hasSubscription) { 73 117 return ( 74 - <div> 75 - <h2>Subscribe to Access</h2> 76 - <p>You need an active subscription to access the server features.</p> 77 - <button 78 - className="cursor-pointer" 118 + <div className="space-y-3 text-white"> 119 + <Heading as="h2" className="text-base font-semibold"> 120 + Subscribe to Access 121 + </Heading> 122 + <Paragraph className="text-sm text-white/80"> 123 + You need an active subscription to access the server features. 124 + </Paragraph> 125 + {(pdsPlan || selectedHostname || selectedUsername) && ( 126 + <Paragraph className="text-xs text-white/70"> 127 + Selected plan settings: plan={pdsPlan || "personal"}, disksize= 128 + {planBasedDisksize}GiB 129 + {selectedHostname ? `, hostname=${selectedHostname}` : ""} 130 + {selectedUsername ? `, username=${selectedUsername}` : ""} 131 + </Paragraph> 132 + )} 133 + <Button 79 134 onClick={handleSubscribe} 80 135 disabled={loading} 136 + className="mt-1 rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-primary/80" 81 137 > 82 138 {loading ? "Loading..." : "Subscribe Now"} 83 - </button> 139 + </Button> 84 140 </div> 85 141 ); 86 142 } 87 143 88 144 if (isCanceled) { 89 145 return ( 90 - <div> 91 - <h2>Subscription Canceled</h2> 92 - <p> 146 + <div className="space-y-3 text-white"> 147 + <Heading as="h2" className="text-base font-semibold text-rose-300"> 148 + Subscription Canceled 149 + </Heading> 150 + <Paragraph className="text-sm text-white/80"> 93 151 Your subscription has been canceled. Subscribe again to regain access. 94 - </p> 95 - <button 96 - className="cursor-pointer" 152 + </Paragraph> 153 + <Button 97 154 onClick={handleSubscribe} 98 155 disabled={loading} 156 + className="mt-1 rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-primary/80" 99 157 > 100 158 {loading ? "Loading..." : "Subscribe Again"} 101 - </button> 159 + </Button> 102 160 </div> 103 161 ); 104 162 } ··· 106 164 const handleCancel = async () => { 107 165 if ( 108 166 !confirm( 109 - "Are you sure you want to cancel your subscription? You'll have access until the end of your billing period." 167 + "Are you sure you want to cancel your subscription? You'll have access until the end of your billing period.", 110 168 ) 111 169 ) { 112 170 return; ··· 117 175 const result = await cancelSubscription(); 118 176 if (result.success) { 119 177 alert( 120 - "Subscription canceled. You'll have access until the end of your billing period." 178 + "Subscription canceled. You'll have access until the end of your billing period.", 121 179 ); 122 180 window.location.reload(); 123 181 } else { ··· 169 227 const isCanceling = subscription?.cancel_at_period_end === true; 170 228 171 229 return ( 172 - <div> 173 - <h2>{isCanceling ? "Subscription Canceling" : "Active Subscription"}</h2> 230 + <div className="space-y-4 text-white"> 231 + <Heading 232 + as="h2" 233 + className={`text-base font-semibold ${ 234 + isCanceling ? "text-amber-300" : "text-emerald-300" 235 + }`} 236 + > 237 + {isCanceling ? "Cancellation scheduled" : "Active Subscription"} 238 + </Heading> 174 239 {subscription && ( 175 - <div> 176 - <p> 177 - <strong>Status:</strong> {subscription.status} 178 - </p> 240 + <div className="space-y-1 text-sm text-white/80"> 241 + <Paragraph> 242 + <span className="font-semibold text-white">Status:</span>{" "} 243 + {subscription.status} 244 + </Paragraph> 179 245 {subscription.current_period_end && ( 180 - <p> 181 - <strong>{isCanceling ? "Access until:" : "Renews:"}</strong>{" "} 246 + <Paragraph> 247 + <span className="font-semibold text-white"> 248 + {isCanceling ? "Access until:" : "Renews:"} 249 + </span>{" "} 182 250 {new Date(subscription.current_period_end).toLocaleDateString()} 183 - </p> 251 + </Paragraph> 184 252 )} 185 253 {isCanceling && ( 186 - <p> 254 + <Paragraph> 187 255 Your subscription will cancel at the end of the billing period. 188 - </p> 256 + </Paragraph> 189 257 )} 190 258 </div> 191 259 )} 192 260 193 - <p> 194 - <button 195 - className="cursor-pointer" 196 - onClick={handleManageBilling} 197 - disabled={actionLoading !== null} 198 - > 199 - {actionLoading === "billing" 200 - ? "Loading..." 201 - : "Manage Payment Method"} 202 - </button> 203 - </p> 204 - 205 - <p> 206 - {isCanceling ? ( 207 - <button 208 - className="cursor-pointer" 209 - onClick={handleResume} 261 + <div className="space-y-3 pt-2"> 262 + <div className="flex flex-wrap gap-2"> 263 + <Button 264 + onClick={handleManageBilling} 210 265 disabled={actionLoading !== null} 266 + className="rounded-full bg-white px-4 text-xs font-medium uppercase tracking-wide text-neutral-950 hover:bg-primary/80" 211 267 > 212 - {actionLoading === "resume" 268 + {actionLoading === "billing" 213 269 ? "Loading..." 214 - : "Resume Subscription"} 215 - </button> 216 - ) : ( 217 - <button 218 - className="cursor-pointer" 219 - onClick={handleCancel} 220 - disabled={actionLoading !== null} 221 - > 222 - {actionLoading === "cancel" 223 - ? "Loading..." 224 - : "Cancel Subscription"} 225 - </button> 226 - )} 227 - </p> 270 + : "Manage Payment Method"} 271 + </Button> 228 272 229 - <hr /> 230 - <h2>Server Actions</h2> 231 - <p>You have access to the following server endpoints:</p> 232 - <p> 233 - <button 234 - className="cursor-pointer" 235 - onClick={() => handleServerCall("action1")} 236 - > 237 - Call Server Action 1 238 - </button> 239 - </p> 240 - <p> 241 - <button 242 - className="cursor-pointer" 243 - onClick={() => handleServerCall("action2")} 244 - > 245 - Call Server Action 2 246 - </button> 247 - </p> 273 + {isCanceling ? ( 274 + <Button 275 + onClick={handleResume} 276 + disabled={actionLoading !== null} 277 + className="rounded-full border border-white/60 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white hover:bg-white/10" 278 + > 279 + {actionLoading === "resume" 280 + ? "Loading..." 281 + : "Resume Subscription"} 282 + </Button> 283 + ) : ( 284 + <Button 285 + onClick={handleCancel} 286 + disabled={actionLoading !== null} 287 + className="rounded-full border border-white/40 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white hover:bg-white/10" 288 + > 289 + {actionLoading === "cancel" 290 + ? "Loading..." 291 + : "Cancel Subscription"} 292 + </Button> 293 + )} 294 + </div> 295 + </div> 296 + 297 + <hr className="my-4 border-white/10" /> 298 + 299 + <div className="space-y-2"> 300 + <Heading as="h3" className="text-sm font-semibold text-white"> 301 + Server Actions 302 + </Heading> 303 + <Paragraph className="text-sm text-white/80"> 304 + You have access to the following server endpoints: 305 + </Paragraph> 306 + <div className="flex flex-wrap gap-2 pt-1"> 307 + <Button 308 + variant="outline" 309 + className="rounded-full border-white/60 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white hover:bg-white/10" 310 + onClick={() => handleServerCall("action1")} 311 + > 312 + Call Server Action 1 313 + </Button> 314 + <Button 315 + variant="outline" 316 + className="rounded-full border-white/60 bg-transparent px-4 text-xs font-medium uppercase tracking-wide text-white hover:bg-white/10" 317 + onClick={() => handleServerCall("action2")} 318 + > 319 + Call Server Action 2 320 + </Button> 321 + </div> 322 + </div> 248 323 </div> 249 324 ); 250 325 }
+104 -10
app/dashboard/page.tsx
··· 8 8 CardTitle, 9 9 CardDescription, 10 10 } from "@/actions/components/ui/card"; 11 + import { ButtonLink } from "@/components/button-link"; 12 + import { Heading } from "@/components/heading"; 13 + import { Paragraph } from "@/components/paragraph"; 11 14 import DashboardClient from "./dashboard-client"; 15 + import { ServiceDetailsClient } from "./service-details-client"; 16 + import { AtprotoTestClient } from "./atproto-test-client"; 17 + import { prelaunch } from "@/lib/prelaunch"; 18 + import { getPriceIdForPlan } from "@/lib/stripe-plans"; 12 19 13 - export default async function DashboardPage() { 20 + type DashboardPageProps = { 21 + searchParams?: { 22 + auto_checkout?: string; 23 + pds_plan?: string; 24 + pds_username?: string; 25 + pds_hostname?: string; 26 + pds_disksize_gb?: string; 27 + }; 28 + }; 29 + 30 + export default async function DashboardPage({ searchParams }: DashboardPageProps) { 14 31 const supabase = await createClient(); 15 32 const { 16 33 data: { user }, ··· 22 39 23 40 const { subscribed, subscription } = await getSubscriptionStatus(); 24 41 42 + // During prelaunch we only collect signups and notify them on launch. 43 + // Prevent non-subscribed users from reaching the dashboard subscribe UI. 44 + if (prelaunch && !subscribed) { 45 + redirect("/welcome"); 46 + } 47 + 48 + // Simple stubbed PDS status derived from subscription state 49 + const pdsStatus = subscribed ? "active" : "provisioning"; 50 + const pdsHostname = 51 + user.email?.split("@")[0]?.toLowerCase().replace(/[^a-z0-9-]/g, "-") + 52 + ".eny.space" || "pending.eny.space"; 53 + const pdsDashboardUrl = `https://${pdsHostname}`; 54 + 25 55 return ( 26 - <main className="flex flex-col gap-6 px-4 py-6"> 56 + <main className="mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 sm:px-6"> 27 57 <Card> 28 58 <CardHeader> 29 - <CardTitle>Dashboard</CardTitle> 30 - <CardDescription>Welcome back, {user.email}.</CardDescription> 59 + <Heading as="h1" className="text-xl font-semibold text-white"> 60 + My PDS 61 + </Heading> 62 + <Paragraph className="text-sm text-white/80"> 63 + Authenticated as {user.email}. This is your Personal Data Server 64 + overview. 65 + </Paragraph> 31 66 </CardHeader> 32 - <CardContent> 33 - <DashboardClient 34 - subscribed={subscribed} 35 - subscription={subscription} 36 - priceId={process.env.NEXT_PUBLIC_STRIPE_PRICE_ID || ""} 37 - /> 67 + <CardContent className="space-y-4"> 68 + <div className="grid gap-4 md:grid-cols-2 text-white"> 69 + <div className="space-y-2"> 70 + <Paragraph className="text-sm font-medium text-white/80"> 71 + Status 72 + </Paragraph> 73 + <Paragraph className="text-base font-semibold capitalize"> 74 + {pdsStatus} 75 + </Paragraph> 76 + </div> 77 + <div className="space-y-2"> 78 + <Paragraph className="text-sm font-medium text-white/80"> 79 + URL / Hostname 80 + </Paragraph> 81 + <a 82 + href={pdsDashboardUrl} 83 + target="_blank" 84 + rel="noreferrer" 85 + className="text-base font-semibold text-primary underline underline-offset-2" 86 + > 87 + {pdsHostname} 88 + </a> 89 + </div> 90 + </div> 91 + 92 + <div className="mt-4 space-y-2 rounded-md border border-white/10 bg-white/5 p-4 text-white backdrop-blur-xl"> 93 + <Paragraph className="text-sm font-medium text-white/80"> 94 + Usage summary 95 + </Paragraph> 96 + <ServiceDetailsClient mode="stats" /> 97 + </div> 98 + 99 + <div className="mt-4 flex flex-wrap gap-3"> 100 + <ButtonLink 101 + href={pdsDashboardUrl} 102 + className="border border-white/80 bg-transparent uppercase tracking-wide text-white hover:bg-white/10 hover:border-white focus-visible:ring-white/50" 103 + > 104 + Open dashboard 105 + </ButtonLink> 106 + </div> 107 + 108 + <hr className="my-6" /> 109 + 110 + <ServiceDetailsClient mode="details" /> 111 + 112 + <AtprotoTestClient /> 113 + 114 + <section className="space-y-2 text-white"> 115 + <Heading 116 + as="h2" 117 + className="text-sm font-semibold uppercase tracking-wide text-white/80" 118 + > 119 + Billing & Subscription 120 + </Heading> 121 + <DashboardClient 122 + subscribed={subscribed} 123 + subscription={subscription} 124 + priceId={getPriceIdForPlan(searchParams?.pds_plan)} 125 + autoCheckoutFromPlan={searchParams?.auto_checkout === "1"} 126 + pdsPlan={searchParams?.pds_plan} 127 + pdsUsername={searchParams?.pds_username} 128 + pdsHostname={searchParams?.pds_hostname} 129 + pdsDisksizeGb={searchParams?.pds_disksize_gb} 130 + /> 131 + </section> 38 132 </CardContent> 39 133 </Card> 40 134 </main>
+491
app/dashboard/service-details-client.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import { Heading } from "@/components/heading"; 5 + import { Paragraph } from "@/components/paragraph"; 6 + 7 + type ServiceStats = { 8 + cpuUsagePercent?: number; 9 + ramUsagePercent?: number; 10 + storageUsedBytes?: number; 11 + storageAllocatedBytes?: number; 12 + storageObjectsCount?: number; 13 + storageUsedDailyLast30d?: Array<{ date: string; usedBytes: number }>; 14 + bandwidthUsedBytesThisMonth?: number; 15 + bandwidthLimitBytesPerMonth?: number; 16 + requestsLast24h?: number; 17 + requestsPerHourLast24h?: Array<{ hour: string; count: number }>; 18 + activeUsers?: number; 19 + uniqueUsersLast30d?: number; 20 + userSlotsUsed?: number; 21 + userSlotsTotal?: number; 22 + uptimeSeconds?: number; 23 + lastBackupAt?: string; 24 + failedRequestsLast24h?: number; 25 + successfulRequestsLast24h?: number; 26 + }; 27 + 28 + type ServiceConfig = { 29 + hostname: string; 30 + adminPassword?: string; 31 + emailSmtpUrl?: string; 32 + pdsEmailFromAddress?: string; 33 + dataStorage?: { 34 + size?: string; 35 + }; 36 + }; 37 + 38 + type ServiceResponse = { 39 + id?: number | string; 40 + name: string; 41 + service: string; 42 + namespace: string; 43 + state?: number | string; 44 + kubeconfig_id: number | string; 45 + encrypted_config: ServiceConfig; 46 + install_cmd: string; 47 + created_at?: string; 48 + updated_at?: string; 49 + stats?: ServiceStats; 50 + }; 51 + 52 + function formatBytes(bytes?: number) { 53 + if (bytes === undefined || bytes === null || Number.isNaN(bytes)) return "—"; 54 + const units = ["B", "KiB", "MiB", "GiB", "TiB"]; 55 + let v = bytes; 56 + let i = 0; 57 + while (v >= 1024 && i < units.length - 1) { 58 + v /= 1024; 59 + i += 1; 60 + } 61 + return `${v.toFixed(v >= 10 || i === 0 ? 0 : 1)} ${units[i]}`; 62 + } 63 + 64 + function clampPct(n: number) { 65 + return Math.max(0, Math.min(100, n)); 66 + } 67 + 68 + export function ServiceDetailsClient({ 69 + mode = "all", 70 + }: { 71 + mode?: "all" | "stats" | "details"; 72 + }) { 73 + const [service, setService] = useState<ServiceResponse | null>(null); 74 + const [loading, setLoading] = useState(true); 75 + const [error, setError] = useState<string | null>(null); 76 + 77 + useEffect(() => { 78 + const fetchService = async () => { 79 + try { 80 + const res = await fetch("/api/pds/service", { 81 + method: "GET", 82 + }); 83 + 84 + if (!res.ok) { 85 + throw new Error(`Request failed with status ${res.status}`); 86 + } 87 + 88 + const data = (await res.json()) as ServiceResponse; 89 + setService(data); 90 + } catch (err) { 91 + console.error("Failed to load service details", err); 92 + setError( 93 + err instanceof Error ? err.message : "Unknown error fetching service", 94 + ); 95 + } finally { 96 + setLoading(false); 97 + } 98 + }; 99 + 100 + fetchService(); 101 + }, []); 102 + 103 + if (loading) { 104 + return ( 105 + <div className="mt-4 rounded-md border border-white/10 bg-white/5 p-4 text-white/80 text-sm"> 106 + Loading service details… 107 + </div> 108 + ); 109 + } 110 + 111 + if (error) { 112 + return ( 113 + <div className="mt-4 rounded-md border border-rose-500/50 bg-rose-950/40 p-4 text-sm text-rose-100"> 114 + Failed to load service details: {error} 115 + </div> 116 + ); 117 + } 118 + 119 + if (!service) { 120 + return null; 121 + } 122 + 123 + const cfg = service.encrypted_config || {}; 124 + const stats = service.stats; 125 + const maskedAdminPassword = 126 + cfg.adminPassword && cfg.adminPassword.length > 0 ? "••••••••" : undefined; 127 + 128 + const storagePct = 129 + stats?.storageUsedBytes !== undefined && 130 + stats?.storageAllocatedBytes !== undefined && 131 + stats.storageAllocatedBytes > 0 132 + ? clampPct((stats.storageUsedBytes / stats.storageAllocatedBytes) * 100) 133 + : undefined; 134 + 135 + const bandwidthPct = 136 + stats?.bandwidthUsedBytesThisMonth !== undefined && 137 + stats?.bandwidthLimitBytesPerMonth !== undefined && 138 + stats.bandwidthLimitBytesPerMonth > 0 139 + ? clampPct( 140 + (stats.bandwidthUsedBytesThisMonth / 141 + stats.bandwidthLimitBytesPerMonth) * 142 + 100, 143 + ) 144 + : undefined; 145 + 146 + const errorRatePct = 147 + stats?.failedRequestsLast24h !== undefined && 148 + stats?.successfulRequestsLast24h !== undefined 149 + ? clampPct( 150 + (stats.failedRequestsLast24h / 151 + Math.max( 152 + 1, 153 + stats.failedRequestsLast24h + stats.successfulRequestsLast24h, 154 + )) * 155 + 100, 156 + ) 157 + : undefined; 158 + 159 + const showDetails = mode === "all" || mode === "details"; 160 + const showStats = mode === "all" || mode === "stats"; 161 + 162 + return ( 163 + <section 164 + className={ 165 + mode === "stats" 166 + ? "space-y-3 text-white" 167 + : "mt-6 space-y-3 rounded-md border border-white/10 bg-white/5 p-4 text-white backdrop-blur-xl" 168 + } 169 + > 170 + {showDetails && ( 171 + <> 172 + <Heading 173 + as="h2" 174 + className="text-sm font-semibold uppercase tracking-wide text-white/80" 175 + > 176 + Service Details 177 + </Heading> 178 + <div className="grid gap-3 md:grid-cols-2 text-sm text-white/80"> 179 + {service.id !== undefined && ( 180 + <div className="space-y-1"> 181 + <Paragraph className="text-xs font-medium text-white/60"> 182 + ID 183 + </Paragraph> 184 + <Paragraph className="text-sm font-mono text-white"> 185 + {String(service.id)} 186 + </Paragraph> 187 + </div> 188 + )} 189 + <div className="space-y-1"> 190 + <Paragraph className="text-xs font-medium text-white/60"> 191 + Name 192 + </Paragraph> 193 + <Paragraph className="text-sm font-semibold text-white"> 194 + {service.name} 195 + </Paragraph> 196 + </div> 197 + <div className="space-y-1"> 198 + <Paragraph className="text-xs font-medium text-white/60"> 199 + Service Type 200 + </Paragraph> 201 + <Paragraph className="text-sm font-semibold text-white"> 202 + {service.service} 203 + </Paragraph> 204 + </div> 205 + <div className="space-y-1"> 206 + <Paragraph className="text-xs font-medium text-white/60"> 207 + Namespace 208 + </Paragraph> 209 + <Paragraph className="text-sm font-mono text-white"> 210 + {service.namespace} 211 + </Paragraph> 212 + </div> 213 + {service.state !== undefined && ( 214 + <div className="space-y-1"> 215 + <Paragraph className="text-xs font-medium text-white/60"> 216 + State 217 + </Paragraph> 218 + <Paragraph className="text-sm font-mono text-white"> 219 + {String(service.state)} 220 + </Paragraph> 221 + </div> 222 + )} 223 + {service.kubeconfig_id !== undefined && ( 224 + <div className="space-y-1"> 225 + <Paragraph className="text-xs font-medium text-white/60"> 226 + Kubeconfig ID 227 + </Paragraph> 228 + <Paragraph className="text-sm font-mono text-white"> 229 + {String(service.kubeconfig_id)} 230 + </Paragraph> 231 + </div> 232 + )} 233 + <div className="space-y-1"> 234 + <Paragraph className="text-xs font-medium text-white/60"> 235 + Hostname 236 + </Paragraph> 237 + <Paragraph className="text-sm font-semibold text-white"> 238 + {cfg.hostname} 239 + </Paragraph> 240 + </div> 241 + {maskedAdminPassword && ( 242 + <div className="space-y-1"> 243 + <Paragraph className="text-xs font-medium text-white/60"> 244 + Admin Password 245 + </Paragraph> 246 + <Paragraph className="text-sm font-mono text-white"> 247 + {maskedAdminPassword} 248 + </Paragraph> 249 + </div> 250 + )} 251 + {cfg.pdsEmailFromAddress && ( 252 + <div className="space-y-1"> 253 + <Paragraph className="text-xs font-medium text-white/60"> 254 + PDS Email From 255 + </Paragraph> 256 + <Paragraph className="text-sm font-mono text-white"> 257 + {cfg.pdsEmailFromAddress} 258 + </Paragraph> 259 + </div> 260 + )} 261 + {cfg.emailSmtpUrl && ( 262 + <div className="space-y-1 md:col-span-2"> 263 + <Paragraph className="text-xs font-medium text-white/60"> 264 + SMTP URL 265 + </Paragraph> 266 + <Paragraph className="text-sm font-mono text-white break-all"> 267 + {cfg.emailSmtpUrl} 268 + </Paragraph> 269 + </div> 270 + )} 271 + {cfg.dataStorage?.size && ( 272 + <div className="space-y-1"> 273 + <Paragraph className="text-xs font-medium text-white/60"> 274 + Data Storage 275 + </Paragraph> 276 + <Paragraph className="text-sm font-semibold text-white"> 277 + {cfg.dataStorage.size} 278 + </Paragraph> 279 + </div> 280 + )} 281 + {(service.created_at || service.updated_at) && ( 282 + <div className="space-y-1 md:col-span-2"> 283 + <Paragraph className="text-xs font-medium text-white/60"> 284 + Timestamps 285 + </Paragraph> 286 + <Paragraph className="text-sm font-mono text-white/90"> 287 + {service.created_at && ( 288 + <span className="mr-4"> 289 + created_at: {service.created_at} 290 + </span> 291 + )} 292 + {service.updated_at && ( 293 + <span>updated_at: {service.updated_at}</span> 294 + )} 295 + </Paragraph> 296 + </div> 297 + )} 298 + </div> 299 + </> 300 + )} 301 + 302 + {showStats && stats && ( 303 + <div className="mt-4 space-y-3"> 304 + {mode !== "stats" && ( 305 + <Paragraph className="text-xs font-medium text-white/60"> 306 + Usage stats 307 + </Paragraph> 308 + )} 309 + 310 + <div className="grid gap-3 md:grid-cols-3 text-sm text-white/80"> 311 + <div className="rounded border border-white/10 bg-white/5 p-3"> 312 + <Paragraph className="text-xs font-medium text-white/60"> 313 + CPU usage 314 + </Paragraph> 315 + <Paragraph className="text-sm font-semibold text-white"> 316 + {stats.cpuUsagePercent !== undefined 317 + ? `${clampPct(stats.cpuUsagePercent).toFixed(0)}%` 318 + : "—"} 319 + </Paragraph> 320 + {stats.cpuUsagePercent !== undefined && ( 321 + <div className="mt-2 h-2 w-full rounded bg-white/10"> 322 + <div 323 + className="h-2 rounded bg-fuchsia-400/80" 324 + style={{ width: `${clampPct(stats.cpuUsagePercent)}%` }} 325 + /> 326 + </div> 327 + )} 328 + </div> 329 + 330 + <div className="rounded border border-white/10 bg-white/5 p-3"> 331 + <Paragraph className="text-xs font-medium text-white/60"> 332 + RAM usage 333 + </Paragraph> 334 + <Paragraph className="text-sm font-semibold text-white"> 335 + {stats.ramUsagePercent !== undefined 336 + ? `${clampPct(stats.ramUsagePercent).toFixed(0)}%` 337 + : "—"} 338 + </Paragraph> 339 + {stats.ramUsagePercent !== undefined && ( 340 + <div className="mt-2 h-2 w-full rounded bg-white/10"> 341 + <div 342 + className="h-2 rounded bg-amber-300/80" 343 + style={{ width: `${clampPct(stats.ramUsagePercent)}%` }} 344 + /> 345 + </div> 346 + )} 347 + {stats.userSlotsUsed !== undefined && 348 + stats.userSlotsTotal !== undefined && ( 349 + <Paragraph className="mt-2 text-xs text-white/70"> 350 + Users: {stats.userSlotsUsed}/{stats.userSlotsTotal} 351 + </Paragraph> 352 + )} 353 + </div> 354 + 355 + <div className="rounded border border-white/10 bg-white/5 p-3"> 356 + <Paragraph className="text-xs font-medium text-white/60"> 357 + Storage 358 + </Paragraph> 359 + <Paragraph className="text-sm font-semibold text-white"> 360 + {formatBytes(stats.storageUsedBytes)} /{" "} 361 + {formatBytes(stats.storageAllocatedBytes)} 362 + </Paragraph> 363 + {storagePct !== undefined && ( 364 + <div className="mt-2 h-2 w-full rounded bg-white/10"> 365 + <div 366 + className="h-2 rounded bg-emerald-400/80" 367 + style={{ width: `${storagePct}%` }} 368 + /> 369 + </div> 370 + )} 371 + {stats.storageObjectsCount !== undefined && ( 372 + <Paragraph className="mt-2 text-xs text-white/70"> 373 + Objects: {stats.storageObjectsCount.toLocaleString()} 374 + </Paragraph> 375 + )} 376 + </div> 377 + 378 + <div className="rounded border border-white/10 bg-white/5 p-3"> 379 + <Paragraph className="text-xs font-medium text-white/60"> 380 + Bandwidth (month) 381 + </Paragraph> 382 + <Paragraph className="text-sm font-semibold text-white"> 383 + {formatBytes(stats.bandwidthUsedBytesThisMonth)} /{" "} 384 + {formatBytes(stats.bandwidthLimitBytesPerMonth)} 385 + </Paragraph> 386 + {bandwidthPct !== undefined && ( 387 + <div className="mt-2 h-2 w-full rounded bg-white/10"> 388 + <div 389 + className="h-2 rounded bg-sky-400/80" 390 + style={{ width: `${bandwidthPct}%` }} 391 + /> 392 + </div> 393 + )} 394 + {stats.requestsLast24h !== undefined && ( 395 + <Paragraph className="mt-2 text-xs text-white/70"> 396 + Requests (24h): {stats.requestsLast24h.toLocaleString()} 397 + </Paragraph> 398 + )} 399 + </div> 400 + 401 + <div className="rounded border border-white/10 bg-white/5 p-3"> 402 + <Paragraph className="text-xs font-medium text-white/60"> 403 + Health (24h) 404 + </Paragraph> 405 + <Paragraph className="text-sm font-semibold text-white"> 406 + Error rate:{" "} 407 + {errorRatePct !== undefined 408 + ? `${errorRatePct.toFixed(2)}%` 409 + : "—"} 410 + </Paragraph> 411 + {stats.lastBackupAt && ( 412 + <Paragraph className="mt-2 text-xs text-white/70"> 413 + Last backup: {new Date(stats.lastBackupAt).toLocaleString()} 414 + </Paragraph> 415 + )} 416 + {stats.uptimeSeconds !== undefined && ( 417 + <Paragraph className="mt-1 text-xs text-white/70"> 418 + Uptime: {(stats.uptimeSeconds / 3600).toFixed(1)}h 419 + </Paragraph> 420 + )} 421 + </div> 422 + </div> 423 + 424 + {Array.isArray(stats.requestsPerHourLast24h) && 425 + stats.requestsPerHourLast24h.length > 0 && ( 426 + <div className="rounded border border-white/10 bg-white/5 p-3"> 427 + <Paragraph className="text-xs font-medium text-white/60"> 428 + Requests per hour (last 24h) 429 + </Paragraph> 430 + <div className="mt-3 flex h-16 items-end gap-1"> 431 + {(() => { 432 + const max = Math.max( 433 + ...stats.requestsPerHourLast24h!.map((p) => p.count), 434 + ); 435 + return stats.requestsPerHourLast24h!.map((p, idx) => ( 436 + <div 437 + key={`${p.hour}-${idx}`} 438 + className="w-full rounded-sm bg-white/10" 439 + title={`${new Date(p.hour).toLocaleString()}: ${p.count.toLocaleString()}`} 440 + style={{ 441 + height: `${clampPct((p.count / Math.max(1, max)) * 100)}%`, 442 + backgroundColor: "rgba(56, 189, 248, 0.6)", 443 + }} 444 + /> 445 + )); 446 + })()} 447 + </div> 448 + </div> 449 + )} 450 + 451 + {Array.isArray(stats.storageUsedDailyLast30d) && 452 + stats.storageUsedDailyLast30d.length > 0 && ( 453 + <div className="rounded border border-white/10 bg-white/5 p-3"> 454 + <Paragraph className="text-xs font-medium text-white/60"> 455 + Storage used (last 30d) 456 + </Paragraph> 457 + <div className="mt-3 flex h-16 items-end gap-1"> 458 + {(() => { 459 + const max = Math.max( 460 + ...stats.storageUsedDailyLast30d!.map((p) => p.usedBytes), 461 + ); 462 + return stats.storageUsedDailyLast30d!.map((p, idx) => ( 463 + <div 464 + key={`${p.date}-${idx}`} 465 + className="w-full rounded-sm bg-emerald-400/50" 466 + title={`${p.date}: ${formatBytes(p.usedBytes)}`} 467 + style={{ 468 + height: `${clampPct((p.usedBytes / Math.max(1, max)) * 100)}%`, 469 + }} 470 + /> 471 + )); 472 + })()} 473 + </div> 474 + </div> 475 + )} 476 + </div> 477 + )} 478 + 479 + {showDetails && service.install_cmd && ( 480 + <div className="mt-3 space-y-1"> 481 + <Paragraph className="text-xs font-medium text-white/60"> 482 + Install command 483 + </Paragraph> 484 + <pre className="max-h-64 overflow-auto rounded bg-neutral-900/90 p-3 text-xs text-neutral-100"> 485 + {service.install_cmd} 486 + </pre> 487 + </div> 488 + )} 489 + </section> 490 + ); 491 + }
+66 -64
app/globals.css
··· 5 5 @custom-variant dark (&:is(.dark *)); 6 6 7 7 :root { 8 - --background: oklch(1 0 0); 9 - --foreground: oklch(0.145 0 0); 10 - --card: oklch(1 0 0); 11 - --card-foreground: oklch(0.145 0 0); 12 - --popover: oklch(1 0 0); 13 - --popover-foreground: oklch(0.145 0 0); 14 - --primary: oklch(0.65 0.18 132); 15 - --primary-foreground: oklch(0.99 0.03 121); 16 - --secondary: oklch(0.967 0.001 286.375); 17 - --secondary-foreground: oklch(0.21 0.006 285.885); 18 - --muted: oklch(0.97 0 0); 19 - --muted-foreground: oklch(0.556 0 0); 20 - --accent: oklch(0.97 0 0); 21 - --accent-foreground: oklch(0.205 0 0); 22 - --destructive: oklch(0.58 0.22 27); 23 - --border: oklch(0.922 0 0); 24 - --input: oklch(0.922 0 0); 25 - --ring: oklch(0.708 0 0); 26 - --chart-1: oklch(0.9 0.18 127); 27 - --chart-2: oklch(0.85 0.21 129); 28 - --chart-3: oklch(0.77 0.2 131); 29 - --chart-4: oklch(0.65 0.18 132); 30 - --chart-5: oklch(0.53 0.14 132); 8 + --background: oklch(1 0 0); /* pure white */ 9 + --foreground: oklch(0.145 0 0); /* very dark neutral (near black) */ 10 + --card: oklch(1 0 0); /* pure white */ 11 + --card-foreground: oklch(0.145 0 0); /* very dark neutral */ 12 + --popover: oklch(1 0 0); /* pure white */ 13 + --popover-foreground: oklch(0.145 0 0); /* very dark neutral */ 14 + /* Softer, lighter lilac primary for light mode */ 15 + --primary: oklch(0.88 0.16 325); /* light lilac / fuchsia */ 16 + --primary-foreground: oklch(0.22 0.05 325); /* deep plum text on lilac */ 17 + --secondary: oklch(0.967 0.001 286.375); /* very light cool gray with violet tint */ 18 + --secondary-foreground: oklch(0.21 0.006 285.885); /* deep cool gray text */ 19 + --muted: oklch(0.97 0 0); /* very light neutral gray */ 20 + --muted-foreground: oklch(0.556 0 0); /* medium neutral gray text */ 21 + --accent: oklch(0.97 0 0); /* very light neutral gray (accent bg) */ 22 + --accent-foreground: oklch(0.205 0 0); /* dark neutral text on accent */ 23 + --destructive: oklch(0.58 0.22 27); /* vivid warm red/orange */ 24 + --border: oklch(0.922 0 0); /* light neutral gray border */ 25 + --input: oklch(0.922 0 0); /* light neutral gray input */ 26 + --ring: oklch(0.708 0 0); /* medium neutral gray focus ring */ 27 + --chart-1: oklch(0.9 0.16 325); /* very light lilac */ 28 + --chart-2: oklch(0.86 0.18 325); /* light lilac */ 29 + --chart-3: oklch(0.82 0.19 325); /* mid lilac */ 30 + --chart-4: oklch(0.78 0.19 325); /* deeper lilac */ 31 + --chart-5: oklch(0.72 0.18 325); /* deepest lilac */ 31 32 --radius: 0.625rem; 32 - --sidebar: oklch(0.985 0 0); 33 - --sidebar-foreground: oklch(0.145 0 0); 34 - --sidebar-primary: oklch(0.65 0.18 132); 35 - --sidebar-primary-foreground: oklch(0.99 0.03 121); 36 - --sidebar-accent: oklch(0.97 0 0); 37 - --sidebar-accent-foreground: oklch(0.205 0 0); 38 - --sidebar-border: oklch(0.922 0 0); 39 - --sidebar-ring: oklch(0.708 0 0); 33 + --sidebar: oklch(0.985 0 0); /* off-white sidebar */ 34 + --sidebar-foreground: oklch(0.145 0 0); /* very dark neutral text */ 35 + --sidebar-primary: oklch(0.82 0.19 325); /* saturated lilac sidebar accent */ 36 + --sidebar-primary-foreground: oklch(0.99 0.02 325); /* soft near-white on lilac */ 37 + --sidebar-accent: oklch(0.97 0 0); /* very light neutral accent */ 38 + --sidebar-accent-foreground: oklch(0.205 0 0); /* dark neutral on accent */ 39 + --sidebar-border: oklch(0.922 0 0); /* light gray sidebar border */ 40 + --sidebar-ring: oklch(0.708 0 0); /* medium gray sidebar ring */ 40 41 } 41 42 42 43 .dark { 43 - --background: oklch(0.145 0 0); 44 - --foreground: oklch(0.985 0 0); 45 - --card: oklch(0.205 0 0); 46 - --card-foreground: oklch(0.985 0 0); 47 - --popover: oklch(0.205 0 0); 48 - --popover-foreground: oklch(0.985 0 0); 49 - --primary: oklch(0.77 0.2 131); 50 - --primary-foreground: oklch(0.27 0.07 132); 51 - --secondary: oklch(0.274 0.006 286.033); 52 - --secondary-foreground: oklch(0.985 0 0); 53 - --muted: oklch(0.269 0 0); 54 - --muted-foreground: oklch(0.708 0 0); 55 - --accent: oklch(0.371 0 0); 56 - --accent-foreground: oklch(0.985 0 0); 57 - --destructive: oklch(0.704 0.191 22.216); 58 - --border: oklch(1 0 0 / 10%); 59 - --input: oklch(1 0 0 / 15%); 60 - --ring: oklch(0.556 0 0); 61 - --chart-1: oklch(0.9 0.18 127); 62 - --chart-2: oklch(0.85 0.21 129); 63 - --chart-3: oklch(0.77 0.2 131); 64 - --chart-4: oklch(0.65 0.18 132); 65 - --chart-5: oklch(0.53 0.14 132); 66 - --sidebar: oklch(0.205 0 0); 67 - --sidebar-foreground: oklch(0.985 0 0); 68 - --sidebar-primary: oklch(0.85 0.21 129); 69 - --sidebar-primary-foreground: oklch(0.27 0.07 132); 70 - --sidebar-accent: oklch(0.269 0 0); 71 - --sidebar-accent-foreground: oklch(0.985 0 0); 72 - --sidebar-border: oklch(1 0 0 / 10%); 73 - --sidebar-ring: oklch(0.556 0 0); 44 + --background: oklch(0.145 0 0); /* almost-black charcoal */ 45 + --foreground: oklch(0.985 0 0); /* near-white text */ 46 + --card: oklch(0.205 0 0); /* very dark gray card */ 47 + --card-foreground: oklch(0.985 0 0); /* near-white card text */ 48 + --popover: oklch(0.205 0 0); /* very dark gray popover */ 49 + --popover-foreground: oklch(0.985 0 0); /* near-white popover text */ 50 + /* Slightly lighter lilac primary for dark mode */ 51 + --primary: oklch(0.82 0.17 325); /* bright lilac / fuchsia */ 52 + --primary-foreground: oklch(0.2 0.06 325); /* deep plum on lilac */ 53 + --secondary: oklch(0.274 0.006 286.033); /* deep cool gray with violet tint */ 54 + --secondary-foreground: oklch(0.985 0 0); /* near-white on secondary */ 55 + --muted: oklch(0.269 0 0); /* dark muted gray */ 56 + --muted-foreground: oklch(0.708 0 0); /* medium gray on muted */ 57 + --accent: oklch(0.371 0 0); /* medium-dark accent gray */ 58 + --accent-foreground: oklch(0.985 0 0); /* near-white on accent */ 59 + --destructive: oklch(0.704 0.191 22.216); /* bright warm red/orange */ 60 + --border: oklch(1 0 0 / 10%); /* subtle light border on dark */ 61 + --input: oklch(1 0 0 / 15%); /* subtle light input on dark */ 62 + --ring: oklch(0.556 0 0); /* medium gray ring on dark */ 63 + --chart-1: oklch(0.88 0.17 325); /* very light lilac on dark */ 64 + --chart-2: oklch(0.84 0.19 325); /* light lilac on dark */ 65 + --chart-3: oklch(0.8 0.2 325); /* mid lilac on dark */ 66 + --chart-4: oklch(0.75 0.2 325); /* deeper lilac on dark */ 67 + --chart-5: oklch(0.7 0.19 325); /* deepest lilac on dark */ 68 + --sidebar: oklch(0.205 0 0); /* very dark gray sidebar */ 69 + --sidebar-foreground: oklch(0.985 0 0); /* near-white sidebar text */ 70 + --sidebar-primary: oklch(0.8 0.2 325); /* saturated lilac sidebar accent */ 71 + --sidebar-primary-foreground: oklch(0.2 0.06 325); /* deep plum text on accent */ 72 + --sidebar-accent: oklch(0.269 0 0); /* dark accent gray */ 73 + --sidebar-accent-foreground: oklch(0.985 0 0); /* near-white on accent */ 74 + --sidebar-border: oklch(1 0 0 / 10%); /* subtle light sidebar border */ 75 + --sidebar-ring: oklch(0.556 0 0); /* medium gray sidebar ring */ 74 76 } 75 77 76 78 @theme inline { 77 - --font-sans: var(--font-sans); 79 + --font-heading: var(--font-heading); 78 80 --color-sidebar-ring: var(--sidebar-ring); 79 81 --color-sidebar-border: var(--sidebar-border); 80 82 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); ··· 130 132 @apply border-border outline-ring/50; 131 133 } 132 134 body { 133 - @apply bg-background text-foreground; 135 + @apply bg-transparent text-foreground; 134 136 } 135 137 }
+25 -5
app/layout.tsx
··· 4 4 import { createClient } from "@/lib/supabase/server"; 5 5 import { SiteHeader } from "@/components/site-header"; 6 6 import { Footer } from "@/components/footer"; 7 + import { SiteBackground } from "@/components/site-background"; 7 8 8 9 import "./globals.css"; 9 - import { Noto_Sans } from "next/font/google"; 10 + import { Doto, Fira_Mono } from "next/font/google"; 11 + 12 + const firaMono = Fira_Mono({ 13 + subsets: ["latin"], 14 + weight: ["400", "500", "700"], 15 + variable: "--font-sans", 16 + }); 10 17 11 - const notoSans = Noto_Sans({ variable: "--font-sans" }); 18 + const doto = Doto({ 19 + subsets: ["latin"], 20 + variable: "--font-heading", 21 + }); 12 22 13 23 interface LayoutProps { 14 24 children: React.ReactNode; ··· 19 29 default: "eny.space", 20 30 template: "%s | eny.space", 21 31 }, 32 + icons: { 33 + icon: [ 34 + { url: "/favicon.svg", type: "image/svg+xml" }, 35 + { url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" }, 36 + { url: "/favicon.ico", sizes: "16x16 32x32 48x48", type: "image/x-icon" }, 37 + ], 38 + apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }], 39 + shortcut: "/favicon.ico", 40 + }, 22 41 twitter: { 23 42 card: "summary_large_image", 24 43 description: "eny.space – your data, your space, use it enywhere.", ··· 32 51 } = await supabase.auth.getUser(); 33 52 34 53 return ( 35 - <html lang="en" className={notoSans.variable}> 36 - <body> 54 + <html lang="en" className={`${firaMono.variable} ${doto.variable}`}> 55 + <body className="min-h-screen flex flex-col"> 56 + <SiteBackground /> 37 57 <SiteHeader user={user} /> 38 - {children} 58 + <main className="flex-1">{children}</main> 39 59 <Footer /> 40 60 <SpeedInsights /> 41 61 <Analytics />
+33 -2
app/login/page.tsx
··· 12 12 import { Input } from "@/actions/components/ui/input"; 13 13 import { Label } from "@/actions/components/ui/label"; 14 14 15 - export default function LoginPage() { 15 + type LoginPageProps = { 16 + searchParams?: { 17 + auto_checkout?: string; 18 + pds_plan?: string; 19 + pds_username?: string; 20 + pds_hostname?: string; 21 + pds_disksize_gb?: string; 22 + }; 23 + }; 24 + 25 + export default function LoginPage({ searchParams }: LoginPageProps) { 26 + const nextParams = new URLSearchParams(); 27 + if (searchParams?.auto_checkout) { 28 + nextParams.set("auto_checkout", searchParams.auto_checkout); 29 + } 30 + if (searchParams?.pds_plan) { 31 + nextParams.set("pds_plan", searchParams.pds_plan); 32 + } 33 + if (searchParams?.pds_username) { 34 + nextParams.set("pds_username", searchParams.pds_username); 35 + } 36 + if (searchParams?.pds_hostname) { 37 + nextParams.set("pds_hostname", searchParams.pds_hostname); 38 + } 39 + if (searchParams?.pds_disksize_gb) { 40 + nextParams.set("pds_disksize_gb", searchParams.pds_disksize_gb); 41 + } 42 + 43 + const next = `/dashboard${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 44 + const signupHref = `/signup${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 45 + 16 46 return ( 17 47 <main className="flex min-h-[60vh] items-center justify-center px-4"> 18 48 <Card className="w-full max-w-sm"> ··· 24 54 </CardHeader> 25 55 <CardContent> 26 56 <form action={signIn} className="space-y-4"> 57 + <input type="hidden" name="next" value={next} /> 27 58 <div className="space-y-2"> 28 59 <Label htmlFor="email">Email</Label> 29 60 <Input ··· 52 83 <CardFooter className="flex justify-center text-sm text-muted-foreground"> 53 84 <span> 54 85 Don&apos;t have an account?{" "} 55 - <Link href="/signup" className="underline underline-offset-4"> 86 + <Link href={signupHref} className="underline underline-offset-4"> 56 87 Sign up 57 88 </Link> 58 89 </span>
+33 -2
app/signup/page.tsx
··· 12 12 import { Input } from "@/actions/components/ui/input"; 13 13 import { Label } from "@/actions/components/ui/label"; 14 14 15 - export default function SignUpPage() { 15 + type SignUpPageProps = { 16 + searchParams?: { 17 + auto_checkout?: string; 18 + pds_plan?: string; 19 + pds_username?: string; 20 + pds_hostname?: string; 21 + pds_disksize_gb?: string; 22 + }; 23 + }; 24 + 25 + export default function SignUpPage({ searchParams }: SignUpPageProps) { 26 + const nextParams = new URLSearchParams(); 27 + if (searchParams?.auto_checkout) { 28 + nextParams.set("auto_checkout", searchParams.auto_checkout); 29 + } 30 + if (searchParams?.pds_plan) { 31 + nextParams.set("pds_plan", searchParams.pds_plan); 32 + } 33 + if (searchParams?.pds_username) { 34 + nextParams.set("pds_username", searchParams.pds_username); 35 + } 36 + if (searchParams?.pds_hostname) { 37 + nextParams.set("pds_hostname", searchParams.pds_hostname); 38 + } 39 + if (searchParams?.pds_disksize_gb) { 40 + nextParams.set("pds_disksize_gb", searchParams.pds_disksize_gb); 41 + } 42 + 43 + const next = `/dashboard${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 44 + const loginHref = `/login${nextParams.toString() ? `?${nextParams.toString()}` : ""}`; 45 + 16 46 return ( 17 47 <main className="flex min-h-[60vh] items-center justify-center px-4"> 18 48 <Card className="w-full max-w-sm"> ··· 24 54 </CardHeader> 25 55 <CardContent> 26 56 <form action={signUp} className="space-y-4"> 57 + <input type="hidden" name="next" value={next} /> 27 58 <div className="space-y-2"> 28 59 <Label htmlFor="email">Email</Label> 29 60 <Input ··· 53 84 <CardFooter className="flex justify-center text-sm text-muted-foreground"> 54 85 <span> 55 86 Already have an account?{" "} 56 - <Link href="/login" className="underline underline-offset-4"> 87 + <Link href={loginHref} className="underline underline-offset-4"> 57 88 Login 58 89 </Link> 59 90 </span>
+102
app/welcome/page.tsx
··· 1 + import { redirect } from "next/navigation"; 2 + import { createClient } from "@/lib/supabase/server"; 3 + import { getSubscriptionStatus } from "@/actions/subscription"; 4 + import { prelaunch } from "@/lib/prelaunch"; 5 + import { 6 + Card, 7 + CardContent, 8 + CardDescription, 9 + CardHeader, 10 + CardTitle, 11 + } from "@/actions/components/ui/card"; 12 + import { ButtonLink } from "@/components/button-link"; 13 + import { Heading } from "@/components/heading"; 14 + import { Paragraph } from "@/components/paragraph"; 15 + import DashboardClient from "../dashboard/dashboard-client"; 16 + import { getPriceIdForPlan } from "@/lib/stripe-plans"; 17 + 18 + type WelcomePageProps = { 19 + searchParams?: { 20 + auto_checkout?: string; 21 + pds_plan?: string; 22 + pds_username?: string; 23 + pds_hostname?: string; 24 + pds_disksize_gb?: string; 25 + }; 26 + }; 27 + 28 + export default async function WelcomePage({ searchParams }: WelcomePageProps) { 29 + const supabase = await createClient(); 30 + const { 31 + data: { user }, 32 + } = await supabase.auth.getUser(); 33 + 34 + if (!user) { 35 + redirect("/login"); 36 + } 37 + 38 + const { subscribed, subscription } = await getSubscriptionStatus(); 39 + 40 + // If they already have access, skip the onboarding. 41 + if (subscribed) { 42 + redirect("/dashboard"); 43 + } 44 + 45 + return ( 46 + <main className="flex min-h-[60vh] items-center justify-center px-4 py-8"> 47 + <Card className="w-full max-w-2xl bg-white/5"> 48 + <CardHeader> 49 + <CardTitle>Welcome</CardTitle> 50 + <CardDescription> 51 + {prelaunch 52 + ? "Thanks for registering. We'll notify you when we're live." 53 + : "Almost there — pick a plan to activate your access."} 54 + </CardDescription> 55 + </CardHeader> 56 + 57 + <CardContent className="space-y-6"> 58 + {prelaunch ? ( 59 + <div className="space-y-3 text-white"> 60 + <Heading as="h2" className="text-base font-semibold text-white"> 61 + You're on the launch list 62 + </Heading> 63 + <Paragraph className="text-sm text-white/80"> 64 + We don't offer PDS hosting yet. Once we launch and start 65 + offering packages, we'll email you and unlock your dashboard. 66 + </Paragraph> 67 + <Paragraph className="text-xs text-white/60"> 68 + Registered as:{" "} 69 + <span className="font-mono text-white">{user.email}</span> 70 + </Paragraph> 71 + <div className="flex flex-wrap gap-3 pt-2"> 72 + <ButtonLink 73 + href="/" 74 + className="border border-white/80 bg-transparent uppercase tracking-wide text-white hover:bg-white/10 hover:border-white focus-visible:ring-white/50" 75 + > 76 + Back to home 77 + </ButtonLink> 78 + </div> 79 + </div> 80 + ) : ( 81 + <div className="space-y-4 text-white"> 82 + <Heading as="h2" className="text-base font-semibold"> 83 + Subscribe to Access 84 + </Heading> 85 + <DashboardClient 86 + subscribed={subscribed} 87 + subscription={subscription} 88 + priceId={getPriceIdForPlan(searchParams?.pds_plan)} 89 + autoCheckoutFromPlan={searchParams?.auto_checkout === "1"} 90 + pdsPlan={searchParams?.pds_plan} 91 + pdsUsername={searchParams?.pds_username} 92 + pdsHostname={searchParams?.pds_hostname} 93 + pdsDisksizeGb={searchParams?.pds_disksize_gb} 94 + /> 95 + </div> 96 + )} 97 + </CardContent> 98 + </Card> 99 + </main> 100 + ); 101 + } 102 +
+9
lib/prelaunch.ts
··· 1 + /** 2 + * Feature flag for “prelaunch” mode. 3 + * 4 + * Set `NEXT_PUBLIC_PRELAUNCH=true` to enable prelaunch gating that hides 5 + * elements meant to ship only after launch. 6 + */ 7 + export const prelaunch = 8 + process.env.NEXT_PUBLIC_PRELAUNCH?.toLowerCase() === "true"; 9 +
+84
lib/stripe-plans.ts
··· 1 + import { stripe } from "@/lib/stripe"; 2 + 3 + /** Matches Stripe product names: Personal, Community, Business */ 4 + export const PLAN_KEYS = ["personal", "community", "business"] as const; 5 + export type PlanKey = (typeof PLAN_KEYS)[number]; 6 + 7 + /** Old query-param keys → current plan keys (backwards compatibility). */ 8 + const LEGACY_PLAN_KEYS: Record<string, PlanKey> = { 9 + starter: "personal", 10 + growth: "community", 11 + pro: "business", 12 + }; 13 + 14 + function getDefaultFallbackPriceId(): string | null { 15 + const fallback = process.env.NEXT_PUBLIC_STRIPE_PRICE_ID; 16 + return fallback && fallback.trim().length ? fallback : null; 17 + } 18 + 19 + function getEnvPriceId(planKey: PlanKey): string | null { 20 + const fallback = getDefaultFallbackPriceId(); 21 + 22 + const map: Record<PlanKey, string | undefined> = { 23 + personal: process.env.NEXT_PUBLIC_STRIPE_PRICE_PERSONAL_ID, 24 + community: process.env.NEXT_PUBLIC_STRIPE_PRICE_COMMUNITY_ID, 25 + business: process.env.NEXT_PUBLIC_STRIPE_PRICE_BUSINESS_ID, 26 + }; 27 + 28 + return (map[planKey] && map[planKey]!.trim().length ? map[planKey]! : fallback) ?? null; 29 + } 30 + 31 + function normalizePlanKey(planKey: string | undefined | null): PlanKey { 32 + const raw = (planKey || "personal").toLowerCase(); 33 + const fromLegacy = LEGACY_PLAN_KEYS[raw]; 34 + const candidate = fromLegacy ?? raw; 35 + return (PLAN_KEYS as readonly string[]).includes(candidate) 36 + ? (candidate as PlanKey) 37 + : "personal"; 38 + } 39 + 40 + export function getPriceIdForPlan(planKey: string | undefined | null): string { 41 + const key = normalizePlanKey(planKey); 42 + return getEnvPriceId(key) || ""; 43 + } 44 + 45 + type StripePriceAmount = { 46 + priceId: string; 47 + unitAmount: number | null; 48 + currency: string | null; 49 + }; 50 + 51 + async function retrievePriceById(priceId: string): Promise<StripePriceAmount> { 52 + const price = await stripe.prices.retrieve(priceId); 53 + return { 54 + priceId, 55 + unitAmount: price.unit_amount ?? null, 56 + currency: price.currency ?? null, 57 + }; 58 + } 59 + 60 + /** 61 + * Fetch amounts for each plan from Stripe (using Price IDs from env). 62 + * 63 + * Note: this is server-side only; it never exposes Stripe secrets to the client. 64 + */ 65 + export async function getStripePlanAmounts(): Promise< 66 + Record<PlanKey, StripePriceAmount> 67 + > { 68 + const results = {} as Record<PlanKey, StripePriceAmount>; 69 + 70 + await Promise.all( 71 + PLAN_KEYS.map(async (planKey) => { 72 + const priceId = getEnvPriceId(planKey); 73 + 74 + if (!priceId) { 75 + results[planKey] = { priceId: "", unitAmount: null, currency: null }; 76 + return; 77 + } 78 + 79 + results[planKey] = await retrievePriceById(priceId); 80 + }), 81 + ); 82 + 83 + return results; 84 + }
+300 -410
package-lock.json
··· 14 14 "@vercel/speed-insights": "^1.3.1", 15 15 "class-variance-authority": "^0.7.1", 16 16 "clsx": "^2.1.1", 17 + "framer-motion": "^12.35.2", 17 18 "lucide-react": "^0.562.0", 18 19 "next": "latest", 19 20 "postcss": "^8.5.6", ··· 37 38 "version": "5.2.0", 38 39 "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 39 40 "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 40 - "license": "MIT", 41 41 "engines": { 42 42 "node": ">=10" 43 43 }, ··· 115 115 "url": "https://opencollective.com/babel" 116 116 } 117 117 }, 118 - "node_modules/@babel/core/node_modules/semver": { 119 - "version": "6.3.1", 120 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 121 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 122 - "bin": { 123 - "semver": "bin/semver.js" 124 - } 125 - }, 126 118 "node_modules/@babel/generator": { 127 119 "version": "7.29.1", 128 120 "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", ··· 164 156 "node": ">=6.9.0" 165 157 } 166 158 }, 167 - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { 168 - "version": "6.3.1", 169 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 170 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 171 - "bin": { 172 - "semver": "bin/semver.js" 173 - } 174 - }, 175 159 "node_modules/@babel/helper-create-class-features-plugin": { 176 160 "version": "7.28.6", 177 161 "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", ··· 192 176 "@babel/core": "^7.0.0" 193 177 } 194 178 }, 195 - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { 196 - "version": "6.3.1", 197 - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 198 - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 199 - "bin": { 200 - "semver": "bin/semver.js" 201 - } 202 - }, 203 179 "node_modules/@babel/helper-globals": { 204 180 "version": "7.28.0", 205 181 "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", ··· 526 502 } 527 503 }, 528 504 "node_modules/@dotenvx/dotenvx": { 529 - "version": "1.53.0", 530 - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.53.0.tgz", 531 - "integrity": "sha512-OnsVuJ5O2WCUMXBnyuYah08/I6Tnt1FEZ2PGH9skSRuRh3LK5UoGa6Bzi5Toj/F/0mbeFfv+eNKTsYRoGgRh3Q==", 505 + "version": "1.54.1", 506 + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.54.1.tgz", 507 + "integrity": "sha512-41gU3q7v05GM92QPuPUf4CmUw+mmF8p4wLUh6MCRlxpCkJ9ByLcY9jUf6MwrMNmiKyG/rIckNxj9SCfmNCmCqw==", 532 508 "dependencies": { 533 509 "commander": "^11.1.0", 534 510 "dotenv": "^17.2.1", ··· 659 635 } 660 636 }, 661 637 "node_modules/@emnapi/runtime": { 662 - "version": "1.7.1", 663 - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", 664 - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", 665 - "license": "MIT", 638 + "version": "1.8.1", 639 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", 640 + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", 666 641 "optional": true, 667 642 "dependencies": { 668 643 "tslib": "^2.4.0" ··· 714 689 } 715 690 }, 716 691 "node_modules/@img/colour": { 717 - "version": "1.0.0", 718 - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", 719 - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", 720 - "license": "MIT", 692 + "version": "1.1.0", 693 + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", 694 + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", 721 695 "optional": true, 722 696 "engines": { 723 697 "node": ">=18" ··· 730 704 "cpu": [ 731 705 "arm64" 732 706 ], 733 - "license": "Apache-2.0", 734 707 "optional": true, 735 708 "os": [ 736 709 "darwin" ··· 752 725 "cpu": [ 753 726 "x64" 754 727 ], 755 - "license": "Apache-2.0", 756 728 "optional": true, 757 729 "os": [ 758 730 "darwin" ··· 774 746 "cpu": [ 775 747 "arm64" 776 748 ], 777 - "license": "LGPL-3.0-or-later", 778 749 "optional": true, 779 750 "os": [ 780 751 "darwin" ··· 790 761 "cpu": [ 791 762 "x64" 792 763 ], 793 - "license": "LGPL-3.0-or-later", 794 764 "optional": true, 795 765 "os": [ 796 766 "darwin" ··· 806 776 "cpu": [ 807 777 "arm" 808 778 ], 809 - "license": "LGPL-3.0-or-later", 810 779 "optional": true, 811 780 "os": [ 812 781 "linux" ··· 822 791 "cpu": [ 823 792 "arm64" 824 793 ], 825 - "license": "LGPL-3.0-or-later", 826 794 "optional": true, 827 795 "os": [ 828 796 "linux" ··· 838 806 "cpu": [ 839 807 "ppc64" 840 808 ], 841 - "license": "LGPL-3.0-or-later", 842 809 "optional": true, 843 810 "os": [ 844 811 "linux" ··· 854 821 "cpu": [ 855 822 "riscv64" 856 823 ], 857 - "license": "LGPL-3.0-or-later", 858 824 "optional": true, 859 825 "os": [ 860 826 "linux" ··· 870 836 "cpu": [ 871 837 "s390x" 872 838 ], 873 - "license": "LGPL-3.0-or-later", 874 839 "optional": true, 875 840 "os": [ 876 841 "linux" ··· 886 851 "cpu": [ 887 852 "x64" 888 853 ], 889 - "license": "LGPL-3.0-or-later", 890 854 "optional": true, 891 855 "os": [ 892 856 "linux" ··· 902 866 "cpu": [ 903 867 "arm64" 904 868 ], 905 - "license": "LGPL-3.0-or-later", 906 869 "optional": true, 907 870 "os": [ 908 871 "linux" ··· 918 881 "cpu": [ 919 882 "x64" 920 883 ], 921 - "license": "LGPL-3.0-or-later", 922 884 "optional": true, 923 885 "os": [ 924 886 "linux" ··· 934 896 "cpu": [ 935 897 "arm" 936 898 ], 937 - "license": "Apache-2.0", 938 899 "optional": true, 939 900 "os": [ 940 901 "linux" ··· 956 917 "cpu": [ 957 918 "arm64" 958 919 ], 959 - "license": "Apache-2.0", 960 920 "optional": true, 961 921 "os": [ 962 922 "linux" ··· 978 938 "cpu": [ 979 939 "ppc64" 980 940 ], 981 - "license": "Apache-2.0", 982 941 "optional": true, 983 942 "os": [ 984 943 "linux" ··· 1000 959 "cpu": [ 1001 960 "riscv64" 1002 961 ], 1003 - "license": "Apache-2.0", 1004 962 "optional": true, 1005 963 "os": [ 1006 964 "linux" ··· 1022 980 "cpu": [ 1023 981 "s390x" 1024 982 ], 1025 - "license": "Apache-2.0", 1026 983 "optional": true, 1027 984 "os": [ 1028 985 "linux" ··· 1044 1001 "cpu": [ 1045 1002 "x64" 1046 1003 ], 1047 - "license": "Apache-2.0", 1048 1004 "optional": true, 1049 1005 "os": [ 1050 1006 "linux" ··· 1066 1022 "cpu": [ 1067 1023 "arm64" 1068 1024 ], 1069 - "license": "Apache-2.0", 1070 1025 "optional": true, 1071 1026 "os": [ 1072 1027 "linux" ··· 1088 1043 "cpu": [ 1089 1044 "x64" 1090 1045 ], 1091 - "license": "Apache-2.0", 1092 1046 "optional": true, 1093 1047 "os": [ 1094 1048 "linux" ··· 1110 1064 "cpu": [ 1111 1065 "wasm32" 1112 1066 ], 1113 - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 1114 1067 "optional": true, 1115 1068 "dependencies": { 1116 1069 "@emnapi/runtime": "^1.7.0" ··· 1129 1082 "cpu": [ 1130 1083 "arm64" 1131 1084 ], 1132 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 1133 1085 "optional": true, 1134 1086 "os": [ 1135 1087 "win32" ··· 1148 1100 "cpu": [ 1149 1101 "ia32" 1150 1102 ], 1151 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 1152 1103 "optional": true, 1153 1104 "os": [ 1154 1105 "win32" ··· 1167 1118 "cpu": [ 1168 1119 "x64" 1169 1120 ], 1170 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 1171 1121 "optional": true, 1172 1122 "os": [ 1173 1123 "win32" ··· 1261 1211 "version": "0.3.13", 1262 1212 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 1263 1213 "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 1264 - "license": "MIT", 1265 1214 "dependencies": { 1266 1215 "@jridgewell/sourcemap-codec": "^1.5.0", 1267 1216 "@jridgewell/trace-mapping": "^0.3.24" ··· 1271 1220 "version": "2.3.5", 1272 1221 "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 1273 1222 "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 1274 - "license": "MIT", 1275 1223 "dependencies": { 1276 1224 "@jridgewell/gen-mapping": "^0.3.5", 1277 1225 "@jridgewell/trace-mapping": "^0.3.24" ··· 1281 1229 "version": "3.1.2", 1282 1230 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 1283 1231 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 1284 - "license": "MIT", 1285 1232 "engines": { 1286 1233 "node": ">=6.0.0" 1287 1234 } ··· 1289 1236 "node_modules/@jridgewell/sourcemap-codec": { 1290 1237 "version": "1.5.5", 1291 1238 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 1292 - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 1293 - "license": "MIT" 1239 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" 1294 1240 }, 1295 1241 "node_modules/@jridgewell/trace-mapping": { 1296 1242 "version": "0.3.31", 1297 1243 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 1298 1244 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 1299 - "license": "MIT", 1300 1245 "dependencies": { 1301 1246 "@jridgewell/resolve-uri": "^3.1.0", 1302 1247 "@jridgewell/sourcemap-codec": "^1.4.14" ··· 1358 1303 } 1359 1304 }, 1360 1305 "node_modules/@next/env": { 1361 - "version": "16.0.10", 1362 - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz", 1363 - "integrity": "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==", 1364 - "license": "MIT" 1306 + "version": "16.1.6", 1307 + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", 1308 + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==" 1365 1309 }, 1366 1310 "node_modules/@next/swc-darwin-arm64": { 1367 - "version": "16.0.10", 1368 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.10.tgz", 1369 - "integrity": "sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==", 1311 + "version": "16.1.6", 1312 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", 1313 + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", 1370 1314 "cpu": [ 1371 1315 "arm64" 1372 1316 ], 1373 - "license": "MIT", 1374 1317 "optional": true, 1375 1318 "os": [ 1376 1319 "darwin" ··· 1380 1323 } 1381 1324 }, 1382 1325 "node_modules/@next/swc-darwin-x64": { 1383 - "version": "16.0.10", 1384 - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.10.tgz", 1385 - "integrity": "sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==", 1326 + "version": "16.1.6", 1327 + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", 1328 + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", 1386 1329 "cpu": [ 1387 1330 "x64" 1388 1331 ], 1389 - "license": "MIT", 1390 1332 "optional": true, 1391 1333 "os": [ 1392 1334 "darwin" ··· 1396 1338 } 1397 1339 }, 1398 1340 "node_modules/@next/swc-linux-arm64-gnu": { 1399 - "version": "16.0.10", 1400 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.10.tgz", 1401 - "integrity": "sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==", 1341 + "version": "16.1.6", 1342 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", 1343 + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", 1402 1344 "cpu": [ 1403 1345 "arm64" 1404 1346 ], 1405 - "license": "MIT", 1406 1347 "optional": true, 1407 1348 "os": [ 1408 1349 "linux" ··· 1412 1353 } 1413 1354 }, 1414 1355 "node_modules/@next/swc-linux-arm64-musl": { 1415 - "version": "16.0.10", 1416 - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.10.tgz", 1417 - "integrity": "sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==", 1356 + "version": "16.1.6", 1357 + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", 1358 + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", 1418 1359 "cpu": [ 1419 1360 "arm64" 1420 1361 ], 1421 - "license": "MIT", 1422 1362 "optional": true, 1423 1363 "os": [ 1424 1364 "linux" ··· 1428 1368 } 1429 1369 }, 1430 1370 "node_modules/@next/swc-linux-x64-gnu": { 1431 - "version": "16.0.10", 1432 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.10.tgz", 1433 - "integrity": "sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==", 1371 + "version": "16.1.6", 1372 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", 1373 + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", 1434 1374 "cpu": [ 1435 1375 "x64" 1436 1376 ], 1437 - "license": "MIT", 1438 1377 "optional": true, 1439 1378 "os": [ 1440 1379 "linux" ··· 1444 1383 } 1445 1384 }, 1446 1385 "node_modules/@next/swc-linux-x64-musl": { 1447 - "version": "16.0.10", 1448 - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.10.tgz", 1449 - "integrity": "sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==", 1386 + "version": "16.1.6", 1387 + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", 1388 + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", 1450 1389 "cpu": [ 1451 1390 "x64" 1452 1391 ], 1453 - "license": "MIT", 1454 1392 "optional": true, 1455 1393 "os": [ 1456 1394 "linux" ··· 1460 1398 } 1461 1399 }, 1462 1400 "node_modules/@next/swc-win32-arm64-msvc": { 1463 - "version": "16.0.10", 1464 - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.10.tgz", 1465 - "integrity": "sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==", 1401 + "version": "16.1.6", 1402 + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", 1403 + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", 1466 1404 "cpu": [ 1467 1405 "arm64" 1468 1406 ], 1469 - "license": "MIT", 1470 1407 "optional": true, 1471 1408 "os": [ 1472 1409 "win32" ··· 1476 1413 } 1477 1414 }, 1478 1415 "node_modules/@next/swc-win32-x64-msvc": { 1479 - "version": "16.0.10", 1480 - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.10.tgz", 1481 - "integrity": "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==", 1416 + "version": "16.1.6", 1417 + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", 1418 + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", 1482 1419 "cpu": [ 1483 1420 "x64" 1484 1421 ], 1485 - "license": "MIT", 1486 1422 "optional": true, 1487 1423 "os": [ 1488 1424 "win32" ··· 1592 1528 "version": "1.1.2", 1593 1529 "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", 1594 1530 "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", 1595 - "license": "MIT", 1596 1531 "peerDependencies": { 1597 1532 "@types/react": "*", 1598 1533 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" ··· 1666 1601 "version": "1.2.4", 1667 1602 "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", 1668 1603 "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", 1669 - "license": "MIT", 1670 1604 "dependencies": { 1671 1605 "@radix-ui/react-compose-refs": "1.1.2" 1672 1606 }, ··· 1847 1781 } 1848 1782 }, 1849 1783 "node_modules/@supabase/auth-js": { 1850 - "version": "2.90.1", 1851 - "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.90.1.tgz", 1852 - "integrity": "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng==", 1853 - "license": "MIT", 1784 + "version": "2.99.1", 1785 + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.99.1.tgz", 1786 + "integrity": "sha512-x7lKKTvKjABJt/FYcRSPiTT01Xhm2FF8RhfL8+RHMkmlwmRQ88/lREupIHKwFPW0W6pTCJqkZb7Yhpw/EZ+fNw==", 1854 1787 "dependencies": { 1855 1788 "tslib": "2.8.1" 1856 1789 }, ··· 1859 1792 } 1860 1793 }, 1861 1794 "node_modules/@supabase/functions-js": { 1862 - "version": "2.90.1", 1863 - "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.90.1.tgz", 1864 - "integrity": "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw==", 1865 - "license": "MIT", 1795 + "version": "2.99.1", 1796 + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.99.1.tgz", 1797 + "integrity": "sha512-WQE62W5geYImCO4jzFxCk/avnK7JmOdtqu2eiPz3zOaNiIJajNRSAwMMDgEGd2EMs+sUVYj1LfBjfmW3EzHgIA==", 1866 1798 "dependencies": { 1867 1799 "tslib": "2.8.1" 1868 1800 }, ··· 1871 1803 } 1872 1804 }, 1873 1805 "node_modules/@supabase/postgrest-js": { 1874 - "version": "2.90.1", 1875 - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.90.1.tgz", 1876 - "integrity": "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ==", 1877 - "license": "MIT", 1806 + "version": "2.99.1", 1807 + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.99.1.tgz", 1808 + "integrity": "sha512-gtw2ibJrADvfqrpUWXGNlrYUvxttF4WVWfPpTFKOb2IRj7B6YRWMDgcrYqIuD4ZEabK4m6YKQCCGy6clgf1lPA==", 1878 1809 "dependencies": { 1879 1810 "tslib": "2.8.1" 1880 1811 }, ··· 1883 1814 } 1884 1815 }, 1885 1816 "node_modules/@supabase/realtime-js": { 1886 - "version": "2.90.1", 1887 - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.90.1.tgz", 1888 - "integrity": "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w==", 1889 - "license": "MIT", 1817 + "version": "2.99.1", 1818 + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.99.1.tgz", 1819 + "integrity": "sha512-9EDdy/5wOseGFqxW88ShV9JMRhm7f+9JGY5x+LqT8c7R0X1CTLwg5qie8FiBWcXTZ+68yYxVWunI+7W4FhkWOg==", 1890 1820 "dependencies": { 1891 1821 "@types/phoenix": "^1.6.6", 1892 1822 "@types/ws": "^8.18.1", ··· 1901 1831 "version": "0.8.0", 1902 1832 "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz", 1903 1833 "integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==", 1904 - "license": "MIT", 1905 1834 "dependencies": { 1906 1835 "cookie": "^1.0.2" 1907 1836 }, ··· 1910 1839 } 1911 1840 }, 1912 1841 "node_modules/@supabase/storage-js": { 1913 - "version": "2.90.1", 1914 - "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.90.1.tgz", 1915 - "integrity": "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg==", 1916 - "license": "MIT", 1842 + "version": "2.99.1", 1843 + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.99.1.tgz", 1844 + "integrity": "sha512-mf7zPfqofI62SOoyQJeNUVxe72E4rQsbWim6lTDPeLu3lHija/cP5utlQADGrjeTgOUN6znx/rWn7SjrETP1dw==", 1917 1845 "dependencies": { 1918 1846 "iceberg-js": "^0.8.1", 1919 1847 "tslib": "2.8.1" ··· 1923 1851 } 1924 1852 }, 1925 1853 "node_modules/@supabase/supabase-js": { 1926 - "version": "2.90.1", 1927 - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.90.1.tgz", 1928 - "integrity": "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w==", 1929 - "license": "MIT", 1854 + "version": "2.99.1", 1855 + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.99.1.tgz", 1856 + "integrity": "sha512-5MRoYD9ffXq8F6a036dm65YoSHisC3by/d22mauKE99Vrwf792KxYIIr/iqCX7E4hkuugbPZ5EGYHTB7MKy6Vg==", 1930 1857 "dependencies": { 1931 - "@supabase/auth-js": "2.90.1", 1932 - "@supabase/functions-js": "2.90.1", 1933 - "@supabase/postgrest-js": "2.90.1", 1934 - "@supabase/realtime-js": "2.90.1", 1935 - "@supabase/storage-js": "2.90.1" 1858 + "@supabase/auth-js": "2.99.1", 1859 + "@supabase/functions-js": "2.99.1", 1860 + "@supabase/postgrest-js": "2.99.1", 1861 + "@supabase/realtime-js": "2.99.1", 1862 + "@supabase/storage-js": "2.99.1" 1936 1863 }, 1937 1864 "engines": { 1938 1865 "node": ">=20.0.0" ··· 1942 1869 "version": "0.5.15", 1943 1870 "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", 1944 1871 "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", 1945 - "license": "Apache-2.0", 1946 1872 "dependencies": { 1947 1873 "tslib": "^2.8.0" 1948 1874 } 1949 1875 }, 1950 1876 "node_modules/@tailwindcss/node": { 1951 - "version": "4.1.18", 1952 - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", 1953 - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", 1954 - "license": "MIT", 1877 + "version": "4.2.1", 1878 + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", 1879 + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", 1955 1880 "dependencies": { 1956 - "@jridgewell/remapping": "^2.3.4", 1957 - "enhanced-resolve": "^5.18.3", 1881 + "@jridgewell/remapping": "^2.3.5", 1882 + "enhanced-resolve": "^5.19.0", 1958 1883 "jiti": "^2.6.1", 1959 - "lightningcss": "1.30.2", 1884 + "lightningcss": "1.31.1", 1960 1885 "magic-string": "^0.30.21", 1961 1886 "source-map-js": "^1.2.1", 1962 - "tailwindcss": "4.1.18" 1887 + "tailwindcss": "4.2.1" 1963 1888 } 1964 1889 }, 1965 1890 "node_modules/@tailwindcss/oxide": { 1966 - "version": "4.1.18", 1967 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", 1968 - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", 1969 - "license": "MIT", 1891 + "version": "4.2.1", 1892 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", 1893 + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", 1970 1894 "engines": { 1971 - "node": ">= 10" 1895 + "node": ">= 20" 1972 1896 }, 1973 1897 "optionalDependencies": { 1974 - "@tailwindcss/oxide-android-arm64": "4.1.18", 1975 - "@tailwindcss/oxide-darwin-arm64": "4.1.18", 1976 - "@tailwindcss/oxide-darwin-x64": "4.1.18", 1977 - "@tailwindcss/oxide-freebsd-x64": "4.1.18", 1978 - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", 1979 - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", 1980 - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", 1981 - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", 1982 - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", 1983 - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", 1984 - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", 1985 - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" 1898 + "@tailwindcss/oxide-android-arm64": "4.2.1", 1899 + "@tailwindcss/oxide-darwin-arm64": "4.2.1", 1900 + "@tailwindcss/oxide-darwin-x64": "4.2.1", 1901 + "@tailwindcss/oxide-freebsd-x64": "4.2.1", 1902 + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", 1903 + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", 1904 + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", 1905 + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", 1906 + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", 1907 + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", 1908 + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", 1909 + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" 1986 1910 } 1987 1911 }, 1988 1912 "node_modules/@tailwindcss/oxide-android-arm64": { 1989 - "version": "4.1.18", 1990 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", 1991 - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", 1913 + "version": "4.2.1", 1914 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", 1915 + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", 1992 1916 "cpu": [ 1993 1917 "arm64" 1994 1918 ], 1995 - "license": "MIT", 1996 1919 "optional": true, 1997 1920 "os": [ 1998 1921 "android" 1999 1922 ], 2000 1923 "engines": { 2001 - "node": ">= 10" 1924 + "node": ">= 20" 2002 1925 } 2003 1926 }, 2004 1927 "node_modules/@tailwindcss/oxide-darwin-arm64": { 2005 - "version": "4.1.18", 2006 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", 2007 - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", 1928 + "version": "4.2.1", 1929 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", 1930 + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", 2008 1931 "cpu": [ 2009 1932 "arm64" 2010 1933 ], 2011 - "license": "MIT", 2012 1934 "optional": true, 2013 1935 "os": [ 2014 1936 "darwin" 2015 1937 ], 2016 1938 "engines": { 2017 - "node": ">= 10" 1939 + "node": ">= 20" 2018 1940 } 2019 1941 }, 2020 1942 "node_modules/@tailwindcss/oxide-darwin-x64": { 2021 - "version": "4.1.18", 2022 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", 2023 - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", 1943 + "version": "4.2.1", 1944 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", 1945 + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", 2024 1946 "cpu": [ 2025 1947 "x64" 2026 1948 ], 2027 - "license": "MIT", 2028 1949 "optional": true, 2029 1950 "os": [ 2030 1951 "darwin" 2031 1952 ], 2032 1953 "engines": { 2033 - "node": ">= 10" 1954 + "node": ">= 20" 2034 1955 } 2035 1956 }, 2036 1957 "node_modules/@tailwindcss/oxide-freebsd-x64": { 2037 - "version": "4.1.18", 2038 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", 2039 - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", 1958 + "version": "4.2.1", 1959 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", 1960 + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", 2040 1961 "cpu": [ 2041 1962 "x64" 2042 1963 ], 2043 - "license": "MIT", 2044 1964 "optional": true, 2045 1965 "os": [ 2046 1966 "freebsd" 2047 1967 ], 2048 1968 "engines": { 2049 - "node": ">= 10" 1969 + "node": ">= 20" 2050 1970 } 2051 1971 }, 2052 1972 "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 2053 - "version": "4.1.18", 2054 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", 2055 - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", 1973 + "version": "4.2.1", 1974 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", 1975 + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", 2056 1976 "cpu": [ 2057 1977 "arm" 2058 1978 ], 2059 - "license": "MIT", 2060 1979 "optional": true, 2061 1980 "os": [ 2062 1981 "linux" 2063 1982 ], 2064 1983 "engines": { 2065 - "node": ">= 10" 1984 + "node": ">= 20" 2066 1985 } 2067 1986 }, 2068 1987 "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 2069 - "version": "4.1.18", 2070 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", 2071 - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", 1988 + "version": "4.2.1", 1989 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", 1990 + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", 2072 1991 "cpu": [ 2073 1992 "arm64" 2074 1993 ], 2075 - "license": "MIT", 2076 1994 "optional": true, 2077 1995 "os": [ 2078 1996 "linux" 2079 1997 ], 2080 1998 "engines": { 2081 - "node": ">= 10" 1999 + "node": ">= 20" 2082 2000 } 2083 2001 }, 2084 2002 "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 2085 - "version": "4.1.18", 2086 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", 2087 - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", 2003 + "version": "4.2.1", 2004 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", 2005 + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", 2088 2006 "cpu": [ 2089 2007 "arm64" 2090 2008 ], 2091 - "license": "MIT", 2092 2009 "optional": true, 2093 2010 "os": [ 2094 2011 "linux" 2095 2012 ], 2096 2013 "engines": { 2097 - "node": ">= 10" 2014 + "node": ">= 20" 2098 2015 } 2099 2016 }, 2100 2017 "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 2101 - "version": "4.1.18", 2102 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", 2103 - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", 2018 + "version": "4.2.1", 2019 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", 2020 + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", 2104 2021 "cpu": [ 2105 2022 "x64" 2106 2023 ], 2107 - "license": "MIT", 2108 2024 "optional": true, 2109 2025 "os": [ 2110 2026 "linux" 2111 2027 ], 2112 2028 "engines": { 2113 - "node": ">= 10" 2029 + "node": ">= 20" 2114 2030 } 2115 2031 }, 2116 2032 "node_modules/@tailwindcss/oxide-linux-x64-musl": { 2117 - "version": "4.1.18", 2118 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", 2119 - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", 2033 + "version": "4.2.1", 2034 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", 2035 + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", 2120 2036 "cpu": [ 2121 2037 "x64" 2122 2038 ], 2123 - "license": "MIT", 2124 2039 "optional": true, 2125 2040 "os": [ 2126 2041 "linux" 2127 2042 ], 2128 2043 "engines": { 2129 - "node": ">= 10" 2044 + "node": ">= 20" 2130 2045 } 2131 2046 }, 2132 2047 "node_modules/@tailwindcss/oxide-wasm32-wasi": { 2133 - "version": "4.1.18", 2134 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", 2135 - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", 2048 + "version": "4.2.1", 2049 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", 2050 + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", 2136 2051 "bundleDependencies": [ 2137 2052 "@napi-rs/wasm-runtime", 2138 2053 "@emnapi/core", ··· 2144 2059 "cpu": [ 2145 2060 "wasm32" 2146 2061 ], 2147 - "license": "MIT", 2148 2062 "optional": true, 2149 2063 "dependencies": { 2150 - "@emnapi/core": "^1.7.1", 2151 - "@emnapi/runtime": "^1.7.1", 2064 + "@emnapi/core": "^1.8.1", 2065 + "@emnapi/runtime": "^1.8.1", 2152 2066 "@emnapi/wasi-threads": "^1.1.0", 2153 - "@napi-rs/wasm-runtime": "^1.1.0", 2067 + "@napi-rs/wasm-runtime": "^1.1.1", 2154 2068 "@tybys/wasm-util": "^0.10.1", 2155 - "tslib": "^2.4.0" 2069 + "tslib": "^2.8.1" 2156 2070 }, 2157 2071 "engines": { 2158 2072 "node": ">=14.0.0" 2159 2073 } 2160 2074 }, 2161 2075 "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 2162 - "version": "4.1.18", 2163 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", 2164 - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", 2076 + "version": "4.2.1", 2077 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", 2078 + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", 2165 2079 "cpu": [ 2166 2080 "arm64" 2167 2081 ], 2168 - "license": "MIT", 2169 2082 "optional": true, 2170 2083 "os": [ 2171 2084 "win32" 2172 2085 ], 2173 2086 "engines": { 2174 - "node": ">= 10" 2087 + "node": ">= 20" 2175 2088 } 2176 2089 }, 2177 2090 "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 2178 - "version": "4.1.18", 2179 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", 2180 - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", 2091 + "version": "4.2.1", 2092 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", 2093 + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", 2181 2094 "cpu": [ 2182 2095 "x64" 2183 2096 ], 2184 - "license": "MIT", 2185 2097 "optional": true, 2186 2098 "os": [ 2187 2099 "win32" 2188 2100 ], 2189 2101 "engines": { 2190 - "node": ">= 10" 2102 + "node": ">= 20" 2191 2103 } 2192 2104 }, 2193 2105 "node_modules/@tailwindcss/postcss": { 2194 - "version": "4.1.18", 2195 - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", 2196 - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", 2197 - "license": "MIT", 2106 + "version": "4.2.1", 2107 + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", 2108 + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", 2198 2109 "dependencies": { 2199 2110 "@alloc/quick-lru": "^5.2.0", 2200 - "@tailwindcss/node": "4.1.18", 2201 - "@tailwindcss/oxide": "4.1.18", 2202 - "postcss": "^8.4.41", 2203 - "tailwindcss": "4.1.18" 2111 + "@tailwindcss/node": "4.2.1", 2112 + "@tailwindcss/oxide": "4.2.1", 2113 + "postcss": "^8.5.6", 2114 + "tailwindcss": "4.2.1" 2204 2115 } 2205 2116 }, 2206 2117 "node_modules/@ts-morph/common": { ··· 2216 2127 "node_modules/@types/node": { 2217 2128 "version": "20.4.6", 2218 2129 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz", 2219 - "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==", 2220 - "license": "MIT" 2130 + "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==" 2221 2131 }, 2222 2132 "node_modules/@types/phoenix": { 2223 2133 "version": "1.6.7", 2224 2134 "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", 2225 - "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", 2226 - "license": "MIT" 2135 + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==" 2227 2136 }, 2228 2137 "node_modules/@types/prop-types": { 2229 2138 "version": "15.7.15", 2230 2139 "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", 2231 2140 "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", 2232 - "devOptional": true, 2233 - "license": "MIT" 2141 + "devOptional": true 2234 2142 }, 2235 2143 "node_modules/@types/react": { 2236 2144 "version": "18.2.8", 2237 2145 "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.8.tgz", 2238 2146 "integrity": "sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==", 2239 2147 "devOptional": true, 2240 - "license": "MIT", 2241 2148 "dependencies": { 2242 2149 "@types/prop-types": "*", 2243 2150 "@types/scheduler": "*", ··· 2248 2155 "version": "0.26.0", 2249 2156 "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", 2250 2157 "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", 2251 - "devOptional": true, 2252 - "license": "MIT" 2158 + "devOptional": true 2253 2159 }, 2254 2160 "node_modules/@types/statuses": { 2255 2161 "version": "2.0.6", ··· 2265 2171 "version": "8.18.1", 2266 2172 "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", 2267 2173 "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", 2268 - "license": "MIT", 2269 2174 "dependencies": { 2270 2175 "@types/node": "*" 2271 2176 } ··· 2274 2179 "version": "1.6.1", 2275 2180 "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", 2276 2181 "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", 2277 - "license": "MPL-2.0", 2278 2182 "peerDependencies": { 2279 2183 "@remix-run/react": "^2", 2280 2184 "@sveltejs/kit": "^1 || ^2", ··· 2312 2216 "version": "1.3.1", 2313 2217 "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", 2314 2218 "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", 2315 - "license": "Apache-2.0", 2316 2219 "peerDependencies": { 2317 2220 "@sveltejs/kit": "^1 || ^2", 2318 2221 "next": ">= 13", ··· 2575 2478 "version": "1.0.2", 2576 2479 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 2577 2480 "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 2578 - "license": "MIT", 2579 2481 "dependencies": { 2580 2482 "es-errors": "^1.3.0", 2581 2483 "function-bind": "^1.1.2" ··· 2588 2490 "version": "1.0.4", 2589 2491 "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 2590 2492 "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 2591 - "license": "MIT", 2592 2493 "dependencies": { 2593 2494 "call-bind-apply-helpers": "^1.0.2", 2594 2495 "get-intrinsic": "^1.3.0" ··· 2609 2510 } 2610 2511 }, 2611 2512 "node_modules/caniuse-lite": { 2612 - "version": "1.0.30001760", 2613 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", 2614 - "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", 2513 + "version": "1.0.30001777", 2514 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", 2515 + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", 2615 2516 "funding": [ 2616 2517 { 2617 2518 "type": "opencollective", ··· 2625 2526 "type": "github", 2626 2527 "url": "https://github.com/sponsors/ai" 2627 2528 } 2628 - ], 2629 - "license": "CC-BY-4.0" 2529 + ] 2630 2530 }, 2631 2531 "node_modules/chalk": { 2632 2532 "version": "5.6.2", ··· 2686 2586 "node_modules/client-only": { 2687 2587 "version": "0.0.1", 2688 2588 "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 2689 - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 2690 - "license": "MIT" 2589 + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 2691 2590 }, 2692 2591 "node_modules/cliui": { 2693 2592 "version": "8.0.1", ··· 2821 2720 "version": "1.1.1", 2822 2721 "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", 2823 2722 "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", 2824 - "license": "MIT", 2825 2723 "engines": { 2826 2724 "node": ">=18" 2827 2725 }, ··· 2926 2824 "version": "3.2.3", 2927 2825 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", 2928 2826 "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", 2929 - "devOptional": true, 2930 - "license": "MIT" 2827 + "devOptional": true 2931 2828 }, 2932 2829 "node_modules/data-uri-to-buffer": { 2933 2830 "version": "4.0.1", ··· 3023 2920 "version": "2.1.2", 3024 2921 "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 3025 2922 "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 3026 - "license": "Apache-2.0", 3027 2923 "engines": { 3028 2924 "node": ">=8" 3029 2925 } ··· 3056 2952 "version": "1.0.1", 3057 2953 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 3058 2954 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 3059 - "license": "MIT", 3060 2955 "dependencies": { 3061 2956 "call-bind-apply-helpers": "^1.0.1", 3062 2957 "es-errors": "^1.3.0", ··· 3067 2962 } 3068 2963 }, 3069 2964 "node_modules/eciesjs": { 3070 - "version": "0.4.17", 3071 - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz", 3072 - "integrity": "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==", 2965 + "version": "0.4.18", 2966 + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", 2967 + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", 3073 2968 "dependencies": { 3074 2969 "@ecies/ciphers": "^0.2.5", 3075 2970 "@noble/ciphers": "^1.3.0", ··· 3106 3001 } 3107 3002 }, 3108 3003 "node_modules/enhanced-resolve": { 3109 - "version": "5.18.4", 3110 - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", 3111 - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", 3112 - "license": "MIT", 3004 + "version": "5.20.0", 3005 + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", 3006 + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", 3113 3007 "dependencies": { 3114 3008 "graceful-fs": "^4.2.4", 3115 - "tapable": "^2.2.0" 3009 + "tapable": "^2.3.0" 3116 3010 }, 3117 3011 "engines": { 3118 3012 "node": ">=10.13.0" ··· 3138 3032 "version": "1.0.1", 3139 3033 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 3140 3034 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 3141 - "license": "MIT", 3142 3035 "engines": { 3143 3036 "node": ">= 0.4" 3144 3037 } ··· 3147 3040 "version": "1.3.0", 3148 3041 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 3149 3042 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 3150 - "license": "MIT", 3151 3043 "engines": { 3152 3044 "node": ">= 0.4" 3153 3045 } ··· 3156 3048 "version": "1.1.1", 3157 3049 "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 3158 3050 "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 3159 - "license": "MIT", 3160 3051 "dependencies": { 3161 3052 "es-errors": "^1.3.0" 3162 3053 }, ··· 3284 3175 } 3285 3176 }, 3286 3177 "node_modules/express-rate-limit": { 3287 - "version": "8.3.0", 3288 - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", 3289 - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", 3178 + "version": "8.3.1", 3179 + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", 3180 + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", 3290 3181 "dependencies": { 3291 3182 "ip-address": "10.1.0" 3292 3183 }, ··· 3453 3344 "node": ">= 0.6" 3454 3345 } 3455 3346 }, 3347 + "node_modules/framer-motion": { 3348 + "version": "12.35.2", 3349 + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz", 3350 + "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==", 3351 + "dependencies": { 3352 + "motion-dom": "^12.35.2", 3353 + "motion-utils": "^12.29.2", 3354 + "tslib": "^2.4.0" 3355 + }, 3356 + "peerDependencies": { 3357 + "@emotion/is-prop-valid": "*", 3358 + "react": "^18.0.0 || ^19.0.0", 3359 + "react-dom": "^18.0.0 || ^19.0.0" 3360 + }, 3361 + "peerDependenciesMeta": { 3362 + "@emotion/is-prop-valid": { 3363 + "optional": true 3364 + }, 3365 + "react": { 3366 + "optional": true 3367 + }, 3368 + "react-dom": { 3369 + "optional": true 3370 + } 3371 + } 3372 + }, 3456 3373 "node_modules/fresh": { 3457 3374 "version": "2.0.0", 3458 3375 "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", ··· 3478 3395 "version": "1.1.2", 3479 3396 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 3480 3397 "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 3481 - "license": "MIT", 3482 3398 "funding": { 3483 3399 "url": "https://github.com/sponsors/ljharb" 3484 3400 } ··· 3524 3440 "version": "1.3.0", 3525 3441 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 3526 3442 "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 3527 - "license": "MIT", 3528 3443 "dependencies": { 3529 3444 "call-bind-apply-helpers": "^1.0.2", 3530 3445 "es-define-property": "^1.0.1", ··· 3567 3482 "version": "1.0.1", 3568 3483 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 3569 3484 "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 3570 - "license": "MIT", 3571 3485 "dependencies": { 3572 3486 "dunder-proto": "^1.0.1", 3573 3487 "es-object-atoms": "^1.0.0" ··· 3606 3520 "version": "1.2.0", 3607 3521 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 3608 3522 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 3609 - "license": "MIT", 3610 3523 "engines": { 3611 3524 "node": ">= 0.4" 3612 3525 }, ··· 3617 3530 "node_modules/graceful-fs": { 3618 3531 "version": "4.2.11", 3619 3532 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 3620 - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 3621 - "license": "ISC" 3533 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 3622 3534 }, 3623 3535 "node_modules/graphql": { 3624 3536 "version": "16.13.1", ··· 3632 3544 "version": "1.1.0", 3633 3545 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 3634 3546 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 3635 - "license": "MIT", 3636 3547 "engines": { 3637 3548 "node": ">= 0.4" 3638 3549 }, ··· 3644 3555 "version": "2.0.2", 3645 3556 "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 3646 3557 "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 3647 - "license": "MIT", 3648 3558 "dependencies": { 3649 3559 "function-bind": "^1.1.2" 3650 3560 }, ··· 3658 3568 "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==" 3659 3569 }, 3660 3570 "node_modules/hono": { 3661 - "version": "4.12.5", 3662 - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", 3663 - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", 3571 + "version": "4.12.7", 3572 + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", 3573 + "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", 3664 3574 "engines": { 3665 3575 "node": ">=16.9.0" 3666 3576 } ··· 3708 3618 "version": "0.8.1", 3709 3619 "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", 3710 3620 "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", 3711 - "license": "MIT", 3712 3621 "engines": { 3713 3622 "node": ">=20.0.0" 3714 3623 } ··· 3956 3865 "version": "2.6.1", 3957 3866 "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", 3958 3867 "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", 3959 - "license": "MIT", 3960 3868 "bin": { 3961 3869 "jiti": "lib/jiti-cli.mjs" 3962 3870 } 3963 3871 }, 3964 3872 "node_modules/jose": { 3965 - "version": "6.2.0", 3966 - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.0.tgz", 3967 - "integrity": "sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==", 3873 + "version": "6.2.1", 3874 + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.1.tgz", 3875 + "integrity": "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==", 3968 3876 "funding": { 3969 3877 "url": "https://github.com/sponsors/panva" 3970 3878 } ··· 3972 3880 "node_modules/js-tokens": { 3973 3881 "version": "4.0.0", 3974 3882 "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 3975 - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 3976 - "license": "MIT" 3883 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 3977 3884 }, 3978 3885 "node_modules/js-yaml": { 3979 3886 "version": "4.1.1", ··· 4043 3950 } 4044 3951 }, 4045 3952 "node_modules/lightningcss": { 4046 - "version": "1.30.2", 4047 - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", 4048 - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", 4049 - "license": "MPL-2.0", 3953 + "version": "1.31.1", 3954 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", 3955 + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", 4050 3956 "dependencies": { 4051 3957 "detect-libc": "^2.0.3" 4052 3958 }, ··· 4058 3964 "url": "https://opencollective.com/parcel" 4059 3965 }, 4060 3966 "optionalDependencies": { 4061 - "lightningcss-android-arm64": "1.30.2", 4062 - "lightningcss-darwin-arm64": "1.30.2", 4063 - "lightningcss-darwin-x64": "1.30.2", 4064 - "lightningcss-freebsd-x64": "1.30.2", 4065 - "lightningcss-linux-arm-gnueabihf": "1.30.2", 4066 - "lightningcss-linux-arm64-gnu": "1.30.2", 4067 - "lightningcss-linux-arm64-musl": "1.30.2", 4068 - "lightningcss-linux-x64-gnu": "1.30.2", 4069 - "lightningcss-linux-x64-musl": "1.30.2", 4070 - "lightningcss-win32-arm64-msvc": "1.30.2", 4071 - "lightningcss-win32-x64-msvc": "1.30.2" 3967 + "lightningcss-android-arm64": "1.31.1", 3968 + "lightningcss-darwin-arm64": "1.31.1", 3969 + "lightningcss-darwin-x64": "1.31.1", 3970 + "lightningcss-freebsd-x64": "1.31.1", 3971 + "lightningcss-linux-arm-gnueabihf": "1.31.1", 3972 + "lightningcss-linux-arm64-gnu": "1.31.1", 3973 + "lightningcss-linux-arm64-musl": "1.31.1", 3974 + "lightningcss-linux-x64-gnu": "1.31.1", 3975 + "lightningcss-linux-x64-musl": "1.31.1", 3976 + "lightningcss-win32-arm64-msvc": "1.31.1", 3977 + "lightningcss-win32-x64-msvc": "1.31.1" 4072 3978 } 4073 3979 }, 4074 3980 "node_modules/lightningcss-android-arm64": { 4075 - "version": "1.30.2", 4076 - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", 4077 - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", 3981 + "version": "1.31.1", 3982 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", 3983 + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", 4078 3984 "cpu": [ 4079 3985 "arm64" 4080 3986 ], 4081 - "license": "MPL-2.0", 4082 3987 "optional": true, 4083 3988 "os": [ 4084 3989 "android" ··· 4092 3997 } 4093 3998 }, 4094 3999 "node_modules/lightningcss-darwin-arm64": { 4095 - "version": "1.30.2", 4096 - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", 4097 - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", 4000 + "version": "1.31.1", 4001 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", 4002 + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", 4098 4003 "cpu": [ 4099 4004 "arm64" 4100 4005 ], 4101 - "license": "MPL-2.0", 4102 4006 "optional": true, 4103 4007 "os": [ 4104 4008 "darwin" ··· 4112 4016 } 4113 4017 }, 4114 4018 "node_modules/lightningcss-darwin-x64": { 4115 - "version": "1.30.2", 4116 - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", 4117 - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", 4019 + "version": "1.31.1", 4020 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", 4021 + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", 4118 4022 "cpu": [ 4119 4023 "x64" 4120 4024 ], 4121 - "license": "MPL-2.0", 4122 4025 "optional": true, 4123 4026 "os": [ 4124 4027 "darwin" ··· 4132 4035 } 4133 4036 }, 4134 4037 "node_modules/lightningcss-freebsd-x64": { 4135 - "version": "1.30.2", 4136 - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", 4137 - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", 4038 + "version": "1.31.1", 4039 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", 4040 + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", 4138 4041 "cpu": [ 4139 4042 "x64" 4140 4043 ], 4141 - "license": "MPL-2.0", 4142 4044 "optional": true, 4143 4045 "os": [ 4144 4046 "freebsd" ··· 4152 4054 } 4153 4055 }, 4154 4056 "node_modules/lightningcss-linux-arm-gnueabihf": { 4155 - "version": "1.30.2", 4156 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", 4157 - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", 4057 + "version": "1.31.1", 4058 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", 4059 + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", 4158 4060 "cpu": [ 4159 4061 "arm" 4160 4062 ], 4161 - "license": "MPL-2.0", 4162 4063 "optional": true, 4163 4064 "os": [ 4164 4065 "linux" ··· 4172 4073 } 4173 4074 }, 4174 4075 "node_modules/lightningcss-linux-arm64-gnu": { 4175 - "version": "1.30.2", 4176 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", 4177 - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", 4076 + "version": "1.31.1", 4077 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", 4078 + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", 4178 4079 "cpu": [ 4179 4080 "arm64" 4180 4081 ], 4181 - "license": "MPL-2.0", 4182 4082 "optional": true, 4183 4083 "os": [ 4184 4084 "linux" ··· 4192 4092 } 4193 4093 }, 4194 4094 "node_modules/lightningcss-linux-arm64-musl": { 4195 - "version": "1.30.2", 4196 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", 4197 - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", 4095 + "version": "1.31.1", 4096 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", 4097 + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", 4198 4098 "cpu": [ 4199 4099 "arm64" 4200 4100 ], 4201 - "license": "MPL-2.0", 4202 4101 "optional": true, 4203 4102 "os": [ 4204 4103 "linux" ··· 4212 4111 } 4213 4112 }, 4214 4113 "node_modules/lightningcss-linux-x64-gnu": { 4215 - "version": "1.30.2", 4216 - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", 4217 - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", 4114 + "version": "1.31.1", 4115 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", 4116 + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", 4218 4117 "cpu": [ 4219 4118 "x64" 4220 4119 ], 4221 - "license": "MPL-2.0", 4222 4120 "optional": true, 4223 4121 "os": [ 4224 4122 "linux" ··· 4232 4130 } 4233 4131 }, 4234 4132 "node_modules/lightningcss-linux-x64-musl": { 4235 - "version": "1.30.2", 4236 - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", 4237 - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", 4133 + "version": "1.31.1", 4134 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", 4135 + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", 4238 4136 "cpu": [ 4239 4137 "x64" 4240 4138 ], 4241 - "license": "MPL-2.0", 4242 4139 "optional": true, 4243 4140 "os": [ 4244 4141 "linux" ··· 4252 4149 } 4253 4150 }, 4254 4151 "node_modules/lightningcss-win32-arm64-msvc": { 4255 - "version": "1.30.2", 4256 - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", 4257 - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", 4152 + "version": "1.31.1", 4153 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", 4154 + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", 4258 4155 "cpu": [ 4259 4156 "arm64" 4260 4157 ], 4261 - "license": "MPL-2.0", 4262 4158 "optional": true, 4263 4159 "os": [ 4264 4160 "win32" ··· 4272 4168 } 4273 4169 }, 4274 4170 "node_modules/lightningcss-win32-x64-msvc": { 4275 - "version": "1.30.2", 4276 - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", 4277 - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", 4171 + "version": "1.31.1", 4172 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", 4173 + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", 4278 4174 "cpu": [ 4279 4175 "x64" 4280 4176 ], 4281 - "license": "MPL-2.0", 4282 4177 "optional": true, 4283 4178 "os": [ 4284 4179 "win32" ··· 4326 4221 "version": "1.4.0", 4327 4222 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 4328 4223 "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 4329 - "license": "MIT", 4330 4224 "dependencies": { 4331 4225 "js-tokens": "^3.0.0 || ^4.0.0" 4332 4226 }, ··· 4354 4248 "version": "0.30.21", 4355 4249 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 4356 4250 "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 4357 - "license": "MIT", 4358 4251 "dependencies": { 4359 4252 "@jridgewell/sourcemap-codec": "^1.5.5" 4360 4253 } ··· 4363 4256 "version": "1.1.0", 4364 4257 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 4365 4258 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 4366 - "license": "MIT", 4367 4259 "engines": { 4368 4260 "node": ">= 0.4" 4369 4261 } ··· 4487 4379 "url": "https://github.com/sponsors/ljharb" 4488 4380 } 4489 4381 }, 4382 + "node_modules/motion-dom": { 4383 + "version": "12.35.2", 4384 + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz", 4385 + "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==", 4386 + "dependencies": { 4387 + "motion-utils": "^12.29.2" 4388 + } 4389 + }, 4390 + "node_modules/motion-utils": { 4391 + "version": "12.29.2", 4392 + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", 4393 + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==" 4394 + }, 4490 4395 "node_modules/ms": { 4491 4396 "version": "2.1.3", 4492 4397 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 4553 4458 "url": "https://github.com/sponsors/ai" 4554 4459 } 4555 4460 ], 4556 - "license": "MIT", 4557 4461 "bin": { 4558 4462 "nanoid": "bin/nanoid.cjs" 4559 4463 }, ··· 4570 4474 } 4571 4475 }, 4572 4476 "node_modules/next": { 4573 - "version": "16.0.10", 4574 - "resolved": "https://registry.npmjs.org/next/-/next-16.0.10.tgz", 4575 - "integrity": "sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==", 4576 - "license": "MIT", 4477 + "version": "16.1.6", 4478 + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", 4479 + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", 4577 4480 "dependencies": { 4578 - "@next/env": "16.0.10", 4481 + "@next/env": "16.1.6", 4579 4482 "@swc/helpers": "0.5.15", 4483 + "baseline-browser-mapping": "^2.8.3", 4580 4484 "caniuse-lite": "^1.0.30001579", 4581 4485 "postcss": "8.4.31", 4582 4486 "styled-jsx": "5.1.6" ··· 4588 4492 "node": ">=20.9.0" 4589 4493 }, 4590 4494 "optionalDependencies": { 4591 - "@next/swc-darwin-arm64": "16.0.10", 4592 - "@next/swc-darwin-x64": "16.0.10", 4593 - "@next/swc-linux-arm64-gnu": "16.0.10", 4594 - "@next/swc-linux-arm64-musl": "16.0.10", 4595 - "@next/swc-linux-x64-gnu": "16.0.10", 4596 - "@next/swc-linux-x64-musl": "16.0.10", 4597 - "@next/swc-win32-arm64-msvc": "16.0.10", 4598 - "@next/swc-win32-x64-msvc": "16.0.10", 4495 + "@next/swc-darwin-arm64": "16.1.6", 4496 + "@next/swc-darwin-x64": "16.1.6", 4497 + "@next/swc-linux-arm64-gnu": "16.1.6", 4498 + "@next/swc-linux-arm64-musl": "16.1.6", 4499 + "@next/swc-linux-x64-gnu": "16.1.6", 4500 + "@next/swc-linux-x64-musl": "16.1.6", 4501 + "@next/swc-win32-arm64-msvc": "16.1.6", 4502 + "@next/swc-win32-x64-msvc": "16.1.6", 4599 4503 "sharp": "^0.34.4" 4600 4504 }, 4601 4505 "peerDependencies": { ··· 4639 4543 "url": "https://github.com/sponsors/ai" 4640 4544 } 4641 4545 ], 4642 - "license": "MIT", 4643 4546 "dependencies": { 4644 4547 "nanoid": "^3.3.6", 4645 4548 "picocolors": "^1.0.0", ··· 4728 4631 "version": "1.13.4", 4729 4632 "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 4730 4633 "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 4731 - "license": "MIT", 4732 4634 "engines": { 4733 4635 "node": ">= 0.4" 4734 4636 }, ··· 4896 4798 "node_modules/picocolors": { 4897 4799 "version": "1.1.1", 4898 4800 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 4899 - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 4900 - "license": "ISC" 4801 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 4901 4802 }, 4902 4803 "node_modules/picomatch": { 4903 4804 "version": "4.0.3", ··· 4919 4820 } 4920 4821 }, 4921 4822 "node_modules/postcss": { 4922 - "version": "8.5.6", 4923 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", 4924 - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", 4823 + "version": "8.5.8", 4824 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", 4825 + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", 4925 4826 "funding": [ 4926 4827 { 4927 4828 "type": "opencollective", ··· 4936 4837 "url": "https://github.com/sponsors/ai" 4937 4838 } 4938 4839 ], 4939 - "license": "MIT", 4940 4840 "dependencies": { 4941 4841 "nanoid": "^3.3.11", 4942 4842 "picocolors": "^1.1.1", ··· 6355 6255 "version": "18.2.0", 6356 6256 "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 6357 6257 "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 6358 - "license": "MIT", 6359 6258 "dependencies": { 6360 6259 "loose-envify": "^1.1.0" 6361 6260 }, ··· 6367 6266 "version": "18.2.0", 6368 6267 "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 6369 6268 "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 6370 - "license": "MIT", 6371 6269 "dependencies": { 6372 6270 "loose-envify": "^1.1.0", 6373 6271 "scheduler": "^0.23.0" ··· 6581 6479 "version": "0.23.2", 6582 6480 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 6583 6481 "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 6584 - "license": "MIT", 6585 6482 "dependencies": { 6586 6483 "loose-envify": "^1.1.0" 6587 6484 } 6588 6485 }, 6589 6486 "node_modules/semver": { 6590 - "version": "7.7.3", 6591 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 6592 - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 6593 - "license": "ISC", 6594 - "optional": true, 6487 + "version": "6.3.1", 6488 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 6489 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 6595 6490 "bin": { 6596 6491 "semver": "bin/semver.js" 6597 - }, 6598 - "engines": { 6599 - "node": ">=10" 6600 6492 } 6601 6493 }, 6602 6494 "node_modules/send": { ··· 6645 6537 "node_modules/server-only": { 6646 6538 "version": "0.0.1", 6647 6539 "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", 6648 - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", 6649 - "license": "MIT" 6540 + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" 6650 6541 }, 6651 6542 "node_modules/setprototypeof": { 6652 6543 "version": "1.2.0", ··· 6703 6594 "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", 6704 6595 "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", 6705 6596 "hasInstallScript": true, 6706 - "license": "Apache-2.0", 6707 6597 "optional": true, 6708 6598 "dependencies": { 6709 6599 "@img/colour": "^1.0.0", ··· 6743 6633 "@img/sharp-win32-x64": "0.34.5" 6744 6634 } 6745 6635 }, 6636 + "node_modules/sharp/node_modules/semver": { 6637 + "version": "7.7.4", 6638 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", 6639 + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", 6640 + "optional": true, 6641 + "bin": { 6642 + "semver": "bin/semver.js" 6643 + }, 6644 + "engines": { 6645 + "node": ">=10" 6646 + } 6647 + }, 6746 6648 "node_modules/shebang-command": { 6747 6649 "version": "2.0.0", 6748 6650 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", ··· 6766 6668 "version": "1.1.0", 6767 6669 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 6768 6670 "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 6769 - "license": "MIT", 6770 6671 "dependencies": { 6771 6672 "es-errors": "^1.3.0", 6772 6673 "object-inspect": "^1.13.3", ··· 6785 6686 "version": "1.0.0", 6786 6687 "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 6787 6688 "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 6788 - "license": "MIT", 6789 6689 "dependencies": { 6790 6690 "es-errors": "^1.3.0", 6791 6691 "object-inspect": "^1.13.3" ··· 6801 6701 "version": "1.0.1", 6802 6702 "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 6803 6703 "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 6804 - "license": "MIT", 6805 6704 "dependencies": { 6806 6705 "call-bound": "^1.0.2", 6807 6706 "es-errors": "^1.3.0", ··· 6819 6718 "version": "1.0.2", 6820 6719 "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 6821 6720 "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 6822 - "license": "MIT", 6823 6721 "dependencies": { 6824 6722 "call-bound": "^1.0.2", 6825 6723 "es-errors": "^1.3.0", ··· 6862 6760 "version": "1.2.1", 6863 6761 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 6864 6762 "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 6865 - "license": "BSD-3-Clause", 6866 6763 "engines": { 6867 6764 "node": ">=0.10.0" 6868 6765 } ··· 6960 6857 "version": "14.8.0", 6961 6858 "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.8.0.tgz", 6962 6859 "integrity": "sha512-Qdecqk7lx095BE829NWxrG1+69NjPuHrpZqeR61i2KO00fpKkjMX9TYjwFU+WjQD6ZcNmCGOwNfnz5VnI5bjIg==", 6963 - "license": "MIT", 6964 6860 "dependencies": { 6965 6861 "@types/node": ">=8.1.0", 6966 6862 "qs": "^6.11.0" ··· 6973 6869 "version": "5.1.6", 6974 6870 "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", 6975 6871 "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", 6976 - "license": "MIT", 6977 6872 "dependencies": { 6978 6873 "client-only": "0.0.1" 6979 6874 }, ··· 7018 6913 } 7019 6914 }, 7020 6915 "node_modules/tailwindcss": { 7021 - "version": "4.1.18", 7022 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", 7023 - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", 7024 - "license": "MIT" 6916 + "version": "4.2.1", 6917 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", 6918 + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==" 7025 6919 }, 7026 6920 "node_modules/tapable": { 7027 6921 "version": "2.3.0", 7028 6922 "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 7029 6923 "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", 7030 - "license": "MIT", 7031 6924 "engines": { 7032 6925 "node": ">=6" 7033 6926 }, ··· 7050 6943 } 7051 6944 }, 7052 6945 "node_modules/tldts": { 7053 - "version": "7.0.24", 7054 - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.24.tgz", 7055 - "integrity": "sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==", 6946 + "version": "7.0.25", 6947 + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", 6948 + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", 7056 6949 "dependencies": { 7057 - "tldts-core": "^7.0.24" 6950 + "tldts-core": "^7.0.25" 7058 6951 }, 7059 6952 "bin": { 7060 6953 "tldts": "bin/cli.js" 7061 6954 } 7062 6955 }, 7063 6956 "node_modules/tldts-core": { 7064 - "version": "7.0.24", 7065 - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.24.tgz", 7066 - "integrity": "sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==" 6957 + "version": "7.0.25", 6958 + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", 6959 + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==" 7067 6960 }, 7068 6961 "node_modules/to-regex-range": { 7069 6962 "version": "5.0.1", ··· 7120 7013 "node_modules/tslib": { 7121 7014 "version": "2.8.1", 7122 7015 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 7123 - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 7124 - "license": "0BSD" 7016 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 7125 7017 }, 7126 7018 "node_modules/tw-animate-css": { 7127 7019 "version": "1.4.0", ··· 7164 7056 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 7165 7057 "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 7166 7058 "devOptional": true, 7167 - "license": "Apache-2.0", 7168 7059 "bin": { 7169 7060 "tsc": "bin/tsc", 7170 7061 "tsserver": "bin/tsserver" ··· 7388 7279 "version": "8.19.0", 7389 7280 "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", 7390 7281 "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", 7391 - "license": "MIT", 7392 7282 "engines": { 7393 7283 "node": ">=10.0.0" 7394 7284 },
+1
package.json
··· 15 15 "@vercel/speed-insights": "^1.3.1", 16 16 "class-variance-authority": "^0.7.1", 17 17 "clsx": "^2.1.1", 18 + "framer-motion": "^12.35.2", 18 19 "lucide-react": "^0.562.0", 19 20 "next": "latest", 20 21 "postcss": "^8.5.6",
public/apple-touch-icon.png

This is a binary file and will not be displayed.

public/earth.png

This is a binary file and will not be displayed.

public/earth2.jpg

This is a binary file and will not be displayed.

public/favicon-96x96.png

This is a binary file and will not be displayed.

public/favicon.ico

This is a binary file and will not be displayed.

+12
public/favicon.svg
··· 1 + <svg 2 + width="150" 3 + height="150" 4 + viewBox="0 0 150 150" 5 + fill="none" 6 + xmlns="http://www.w3.org/2000/svg" 7 + > 8 + <path 9 + d="M143.619 35.5547C139.137 27.8262 127.307 26.0859 109.342 30.4805C101.025 24.0495 91.0706 20.08 80.6105 19.0229C70.1504 17.9658 59.6035 19.8635 50.1679 24.5004C40.7323 29.1373 32.7863 36.3276 27.2325 45.2544C21.6788 54.1812 18.74 64.4866 18.7499 75C18.7506 77.4115 18.9033 79.8206 19.2069 82.2129C2.9823 99.0234 3.21668 108.984 6.3866 114.445C9.37488 119.625 15.6093 121.875 23.8124 121.875C29.5078 121.757 35.1697 120.971 40.6815 119.531C48.9996 125.956 58.9523 129.92 69.4098 130.973C79.8672 132.026 90.4104 130.126 99.8423 125.488C109.274 120.851 117.217 113.661 122.769 104.737C128.32 95.8126 131.259 85.5103 131.25 75C131.251 72.6003 131.1 70.2029 130.799 67.8223C138.123 60.2051 142.998 52.9688 144.656 46.7754C145.799 42.4863 145.453 38.7129 143.619 35.5547ZM74.9999 28.125C85.6167 28.1359 95.9163 31.7457 104.217 38.365C112.518 44.9844 118.329 54.2222 120.703 64.5703C110.707 73.9805 97.1776 83.6309 84.3339 91.0137C68.1913 100.277 54.1874 106.055 43.0194 109.219C36.1409 102.803 31.3532 94.4659 29.2795 85.2915C27.2059 76.1171 27.9423 66.5309 31.393 57.7809C34.8437 49.0309 40.8488 41.5224 48.6264 36.233C56.4041 30.9437 65.5941 28.1184 74.9999 28.125ZM14.496 109.74C13.6405 108.27 14.1151 105.498 15.7968 102.123C17.4597 98.9609 19.4829 96.0018 21.8261 93.3047C24.1487 100.034 27.7277 106.262 32.3729 111.656C22.2655 113.355 15.9608 112.271 14.496 109.74ZM74.9999 121.875C67.0963 121.883 59.3209 119.879 52.4061 116.051C65.0981 111.562 77.3443 105.901 88.9862 99.1406C101.631 91.8809 112.822 84.0117 121.84 76.2422C121.506 88.4482 116.426 100.043 107.68 108.564C98.934 117.085 87.2104 121.86 74.9999 121.875ZM135.598 44.3496C134.601 48.0469 131.971 52.2774 128.197 56.7305C125.881 49.9929 122.301 43.7587 117.65 38.3613C125.976 36.9844 133.687 37.0899 135.521 40.2598C136.049 41.1797 136.078 42.5567 135.598 44.3496Z" 10 + fill="black" 11 + /> 12 + </svg>
public/jupiter.jpg

This is a binary file and will not be displayed.

public/jupiter2.png

This is a binary file and will not be displayed.

+2 -4
public/logo.svg
··· 1 - <?xml version="1.0" encoding="UTF-8"?> 2 - <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1280" height="1280"> 3 - <path d="M0 0 C0.81952148 0.37697021 1.63904297 0.75394043 2.48339844 1.14233398 C11.60285155 5.34423447 20.53345418 9.57571453 29 15 C30.18360833 15.72476307 31.36863018 16.44722038 32.5546875 17.16796875 C46.72974953 25.82640202 60.36518708 35.19579918 73 46 C74.16665776 46.99219797 75.33332501 47.98438479 76.5 48.9765625 C79.49524674 51.52960075 82.4772543 54.09757669 85.45507812 56.67089844 C86.77004037 57.80216495 88.09493747 58.92186671 89.421875 60.0390625 C93.40501694 63.42010158 96.85096472 66.9899968 100.2265625 70.9765625 C102.02461917 73.02808972 103.9157426 74.91519541 105.875 76.8125 C117.27976871 88.08567523 126.98680285 101.77032865 136 115 C136.72445312 116.02609375 137.44890625 117.0521875 138.1953125 118.109375 C149.2185272 133.93422347 158.34831911 151.30931379 166 169 C166.47308594 170.08667969 166.94617188 171.17335938 167.43359375 172.29296875 C173.60118867 187.08516795 178.77580507 202.5412441 183 218 C191.44559799 218.17788528 199.80780198 217.69099294 208.22385406 217.07402039 C210.57253644 216.90208377 212.92154201 216.73537984 215.27069092 216.56994629 C221.9422003 216.09910602 228.612987 215.61832316 235.28369141 215.13623047 C239.38744579 214.84000895 243.49154851 214.54903854 247.5958786 214.26091003 C249.88180192 214.09880775 252.16722718 213.93067445 254.45269775 213.7623291 C261.35894704 213.26645294 268.26668378 212.8695614 275.19042969 212.74414062 C276.94356476 212.70673767 276.94356476 212.70673767 278.7321167 212.6685791 C283.6286285 213.1651716 286.87960948 214.81736556 290.25 218.375 C292.86510786 222.2976618 294.04899829 224.65254715 293.875 229.4375 C292.5211383 234.9496512 290.58481365 237.89055902 285.91914368 241.15673828 C279.09492723 244.37226141 271.22051996 243.4377612 263.85620117 243.3046875 C261.98679061 243.28616696 260.11736129 243.26947191 258.24792004 243.25436401 C253.95263776 243.21536571 249.65777666 243.1624677 245.36275864 243.10095215 C238.03405749 242.99698969 230.70519033 242.92239103 223.37612181 242.85097337 C205.08949117 242.67173396 186.80341632 242.45106706 168.51730435 242.22583044 C159.25423381 242.11181257 149.99112738 242.00084257 140.72801854 241.88998723 C124.2191264 241.69234224 107.71026259 241.49242862 91.20141602 241.29101562 C75.19850098 241.09578017 59.19557304 240.90165839 43.19262695 240.70898438 C42.20029355 240.69703639 41.20796015 240.68508841 40.18555603 240.67277837 C35.20554558 240.6128243 30.22553488 240.5528916 25.24552405 240.49296951 C-15.83635763 239.99862339 -56.91819366 239.50056505 -98 239 C-98 238.34 -98 237.68 -98 237 C-94.0772445 236.11799576 -90.25605616 235.73677137 -86.24584961 235.47509766 C-85.58342034 235.43094601 -84.92099108 235.38679436 -84.2384882 235.34130478 C-82.04330016 235.19576448 -79.84783634 235.05492764 -77.65234375 234.9140625 C-76.09722926 234.81179793 -74.54213522 234.70922196 -72.98706055 234.60635376 C-68.83005453 234.33214466 -64.67280103 234.06187973 -60.51548386 233.79243469 C-56.21599178 233.51319162 -51.91671364 233.23069836 -47.61743164 232.94824219 C-46.76663497 232.89235176 -45.9158383 232.83646133 -45.03925991 232.77887726 C-43.32703585 232.66639738 -41.61481248 232.55390708 -39.9025898 232.44140625 C-37.34604683 232.27358827 -34.78947358 232.10623782 -32.2328949 231.93896484 C-22.87255654 231.32580498 -13.51349711 230.69804944 -4.15625 230.0390625 C-3.17010208 229.96968979 -2.18395416 229.90031708 -1.16792297 229.82884216 C0.69423473 229.69778159 2.55639194 229.56671403 4.41854858 229.43563843 C20.27341435 228.32242873 36.1087474 227.41589311 52 227 C28.48303999 174.33120936 -8.88782019 132.94486147 -63.59375 111.59765625 C-67.04662044 110.3410253 -70.50394586 109.12915527 -74 108 C-74.94004883 107.69078613 -75.88009766 107.38157227 -76.84863281 107.06298828 C-118.45317339 93.75330041 -164.24458983 95.77523221 -205 111 C-205.8161377 111.30276855 -206.63227539 111.60553711 -207.47314453 111.91748047 C-230.53456122 120.64579466 -252.10337515 134.03226571 -270 151 C-271.68585984 152.48103575 -273.37338885 153.96017343 -275.0625 155.4375 C-300.22364275 178.21166031 -318.80195062 210.61137709 -328.39599609 242.97558594 C-328.95177564 244.83836846 -329.54636871 246.68945411 -330.14453125 248.5390625 C-333.58184819 259.94529026 -334.52692461 271.69369358 -335.67456055 283.51074219 C-336 286 -336 286 -337 289 C-336.24820213 288.98955375 -335.49640427 288.97910749 -334.72182465 288.96834469 C-293.24246821 288.39439767 -251.76291629 287.90813975 -210.28125 287.53515625 C-209.23160655 287.52569292 -208.1819631 287.5162296 -207.10051227 287.5064795 C-197.31794611 287.41828419 -187.53537512 287.3306339 -177.75279903 287.24354649 C-106.47918426 286.60890316 -35.20721591 285.9227042 36.0625 284.9375 C37.62000779 284.91601508 39.17751562 284.89453324 40.7350235 284.8730545 C55.2741428 284.67240874 69.8132208 284.468893 84.3522644 284.26283836 C136.85791209 283.51880763 189.36409672 282.94243828 241.87305307 282.48904628 C246.38426787 282.45004984 250.8954738 282.41012889 255.40667725 282.36984253 C268.67279676 282.25224972 281.93888756 282.1470754 295.20529938 282.068326 C301.35536669 282.03155845 307.50527833 281.98835802 313.6552124 281.93341064 C319.95170712 281.87751368 326.2481379 281.8366901 332.54480553 281.80629158 C334.91375068 281.79235237 337.28267174 281.77321585 339.65152931 281.74853325 C342.91841738 281.71539305 346.18487402 281.70125976 349.4519043 281.69165039 C350.40687742 281.67755945 351.36185055 281.66346851 352.34576225 281.64895058 C360.63382175 281.65973849 367.26181189 283.53746305 373.4609375 289.26953125 C377.91126877 294.27333011 380.10440405 300.31814082 380 307 C379.2633535 313.73505373 376.30879778 319.88072763 371.36328125 324.54296875 C364.94911712 329.45809228 358.17568314 330.31464934 350.3125 330.1875 C349.60544922 330.18685547 348.89839844 330.18621094 348.16992188 330.18554688 C342.52031747 330.14249316 336.8900743 329.84474055 331.25 329.54296875 C319.50622724 328.92311547 307.75605631 328.61867614 296.00097179 328.29819489 C271.19987653 327.62177489 246.41454064 326.76666647 221.62908173 325.65774918 C188.37191297 324.16991624 155.10709618 323.11636312 121.83016777 322.17552948 C116.83594671 322.03412419 111.84178383 321.89076116 106.84765625 321.74609375 C105.86691006 321.71772808 104.88616386 321.68936241 103.87569809 321.66013718 C87.60406101 321.18452577 71.34266729 320.53954986 55.07943153 319.83660507 C38.37624292 319.1161931 21.67655252 318.57284445 4.96310425 318.16629028 C-22.95005425 317.4841153 -50.84562012 316.51288028 -78.74337769 315.37091064 C-83.50076148 315.17617174 -88.25817341 314.98213834 -93.015625 314.7890625 C-94.21104007 314.74052061 -94.21104007 314.74052061 -95.43060493 314.69099808 C-116.38782218 313.84312763 -137.35012931 313.14386423 -158.3125 312.4375 C-181.3783415 311.65978759 -204.44001782 310.86464293 -227.49557495 309.81860352 C-248.019523 308.89296795 -268.55379362 308.29634458 -289.08935547 307.69702148 C-305.06426974 307.2287885 -321.03269911 306.68195513 -337 306 C-332.83721069 350.28271298 -321.03864146 390.35389674 -292 425 C-291.54044922 425.54978516 -291.08089844 426.09957031 -290.60742188 426.66601562 C-270.49209967 450.66278202 -245.21131168 471.03688217 -216 483 C-214.86433594 483.47953125 -213.72867187 483.9590625 -212.55859375 484.453125 C-166.45230384 503.6473276 -113.04922114 506.33421506 -66.0625 488.125 C-63.69932313 487.10393917 -61.34516891 486.06176878 -59 485 C-57.64873805 484.42117287 -56.29716258 483.84307723 -54.9453125 483.265625 C-36.87201051 475.35334032 -15.51439117 463.69376054 -7.625 444.6875 C-6.23861212 436.50781149 -7.30923154 428.90899331 -12 422 C-15.78006204 416.99967521 -19.59482757 413.24310346 -25 410 C-25.84498047 409.37931641 -25.84498047 409.37931641 -26.70703125 408.74609375 C-39.74845449 399.30585247 -55.43098238 393.79178019 -70.59179688 388.79980469 C-72.8874013 388.03739592 -75.17098743 387.24700731 -77.453125 386.4453125 C-94.46665541 380.5215192 -111.97492709 376.24400154 -129.58813477 372.51586914 C-131.42643526 372.12267864 -133.26247165 371.71888686 -135.09741211 371.31030273 C-143.83687428 369.38482551 -152.59916928 367.83348946 -161.4375 366.4375 C-163.37206055 366.12522461 -163.37206055 366.12522461 -165.34570312 365.80664062 C-174.21378779 364.39005625 -183.09361855 363.14600372 -192 362 C-192 361.01 -192 360.02 -192 359 C-174.93963303 359.24208307 -157.87928634 359.48554849 -140.81896019 359.73049068 C-132.89054582 359.84429281 -124.96212626 359.95770164 -117.03369141 360.07006836 C-107.84275483 360.20037598 -98.65183887 360.33207676 -89.4609375 360.46484375 C-88.70015616 360.47580755 -87.93937483 360.48677135 -87.15553951 360.49806738 C-67.8765974 360.77615397 -48.59910296 361.09006679 -29.32185745 361.47047615 C-3.01784746 361.98873757 23.28473973 362.25424325 49.59298706 362.44555664 C62.43835546 362.5390792 75.28362534 362.64526654 88.12890625 362.75 C89.4088553 362.76035782 90.68880436 362.77071564 92.00753975 362.78138733 C124.73477588 363.04668073 157.46095613 363.38671159 190.18638611 363.82156372 C198.45325137 363.93116943 206.72007292 364.0313398 214.98725033 364.11433029 C220.94016887 364.17467005 226.89280754 364.24914014 232.8454895 364.32983398 C235.19819474 364.35941695 237.55096674 364.38417571 239.90377808 364.40356445 C243.03906095 364.43013636 246.17359396 364.47440242 249.30859375 364.5234375 C250.20195435 364.52665009 251.09531494 364.52986267 252.01574707 364.53317261 C257.39856495 364.64128791 261.2242674 365.45955875 266 368 C267.70091875 370.21858967 267.95238868 371.69052639 268.375 374.4375 C267.86233872 377.94068541 266.47871818 379.52128182 264 382 C261.14387256 382.97054331 258.50107568 383.12186828 255.5 383.140625 C237.48314124 383.46327056 220.98389724 386.36246221 205 395 C203.73542969 395.65742187 203.73542969 395.65742187 202.4453125 396.328125 C179.13021457 409.28923376 166.78509512 430.60877584 154.37109375 453.22924805 C148.12930001 464.59791958 141.71783758 475.56523937 134 486 C133.51966309 486.66 133.03932617 487.32 132.54443359 488 C126.16026194 496.76967189 119.50449706 505.24579868 112.37329102 513.421875 C111.40855875 514.53050244 110.45135077 515.64570077 109.49975586 516.765625 C104.77542249 522.27443144 99.74056099 527.44563145 94.60961914 532.57568359 C93.16718144 534.02034996 91.73298435 535.47289854 90.29882812 536.92578125 C88.93467773 538.29283203 88.93467773 538.29283203 87.54296875 539.6875 C86.71821045 540.51636719 85.89345215 541.34523438 85.04370117 542.19921875 C83 544 83 544 81 544 C81 544.66 81 545.32 81 546 C68.9840536 557.56601008 53.95203579 566.95219487 40 576 C39.09121094 576.60328125 38.18242188 577.2065625 37.24609375 577.828125 C31.76658773 581.41357702 26.21191334 584.50545563 20.31640625 587.3515625 C17.94737407 588.52910264 15.64571165 589.73445795 13.33984375 591.0234375 C1.87345416 597.39507428 -10.03038028 602.27948577 -22.27587891 606.95751953 C-24.44496308 607.78759566 -26.60489965 608.63496421 -28.76171875 609.49609375 C-50.30974428 617.74220139 -73.86861575 622.64622575 -96.75 625.1875 C-97.52223907 625.27371613 -98.29447815 625.35993225 -99.09011841 625.44876099 C-125.94408589 628.37463847 -152.22105241 627.52049032 -179 624 C-179.94004883 623.88253418 -180.88009766 623.76506836 -181.84863281 623.64404297 C-231.68341913 617.35750272 -278.44029652 597.7658453 -320 570 C-320.92425781 569.38382813 -321.84851563 568.76765625 -322.80078125 568.1328125 C-333.85919468 560.5674822 -344.14703878 552.06335109 -354 543 C-355.41207844 541.74481917 -356.8284912 540.49449073 -358.25 539.25 C-358.89324219 538.68667969 -359.53648437 538.12335937 -360.19921875 537.54296875 C-361.78999235 536.17994148 -363.42343723 534.86706017 -365.0625 533.5625 C-368.10505563 530.90835573 -370.62166258 528.08480873 -373.23046875 525.015625 C-374.83077503 523.19275957 -376.48869775 521.53961978 -378.25 519.875 C-383.20730242 514.99515543 -387.30767706 509.37557702 -391.54296875 503.87597656 C-392.98029226 502.02537438 -394.43861788 500.19381589 -395.90625 498.3671875 C-401.41672544 491.49483608 -406.44046932 484.55337566 -411 477 C-412.37430079 474.77040219 -413.74932368 472.54124935 -415.125 470.3125 C-415.79789063 469.21164062 -416.47078125 468.11078125 -417.1640625 466.9765625 C-418.57625843 464.68700229 -420.0243424 462.4356495 -421.5 460.1875 C-436.90389769 435.53092552 -447.26413521 406.07933033 -454 378 C-454.22139648 377.08911621 -454.44279297 376.17823242 -454.67089844 375.23974609 C-460.44738189 351.00141796 -463.8098876 325.9249354 -463 301 C-464.21362213 301.0104686 -465.42724426 301.02093719 -466.67764282 301.03172302 C-477.87407841 301.09810038 -489.00490254 300.81780458 -500.18492508 300.23461723 C-518.06717644 299.31075482 -535.95266072 298.67532138 -553.8515625 298.18359375 C-554.9869622 298.15226978 -556.12236191 298.12094582 -557.29216766 298.08867264 C-564.2312066 297.89827166 -571.17038034 297.7132298 -578.10961914 297.53027344 C-587.49793899 297.2825089 -596.88612916 297.0301319 -606.27421188 296.77354622 C-609.49204809 296.68655439 -612.70995627 296.60280548 -615.92788696 296.51939392 C-627.63337202 296.20732084 -639.31294993 295.7305766 -651 295 C-651 294.34 -651 293.68 -651 293 C-647.26788977 291.75596326 -643.78316281 291.83133129 -639.89428711 291.81054688 C-638.64620569 291.79807718 -638.64620569 291.79807718 -637.3729105 291.78535557 C-634.59107611 291.75864525 -631.8092337 291.73876882 -629.02734375 291.71875 C-627.05822324 291.70094416 -625.08910701 291.68265953 -623.11999512 291.66392517 C-618.91618657 291.62471772 -614.71236708 291.58831685 -610.5085144 291.5541687 C-600.62843159 291.47370501 -590.74847797 291.37938452 -580.86851692 291.28530693 C-578.65935492 291.26432422 -576.45019123 291.24351841 -574.24102592 291.22288704 C-551.699903 291.01204131 -529.16157638 290.68837798 -506.62280273 290.3046875 C-492.08021035 290.06158667 -477.54466498 289.93917051 -463 290 C-463.02392822 289.19892822 -463.04785645 288.39785645 -463.07250977 287.57250977 C-463.23685879 277.53725862 -462.27784296 267.86796153 -460.9375 257.9375 C-460.82740601 257.11051605 -460.71731201 256.2835321 -460.60388184 255.43148804 C-451.80786134 190.21780463 -423.21981592 128.7489231 -379 80 C-378.46036621 79.40396973 -377.92073242 78.80793945 -377.36474609 78.19384766 C-373.94530852 74.4290949 -370.50856591 70.68197919 -367 67 C-366.46874512 66.43812988 -365.93749023 65.87625977 -365.39013672 65.29736328 C-363.96690655 63.82501163 -362.48719596 62.40771151 -361 61 C-360.34 61 -359.68 61 -359 61 C-359 60.34 -359 59.68 -359 59 C-357.3203125 57.2734375 -357.3203125 57.2734375 -355.125 55.375 C-354.40570313 54.74335938 -353.68640625 54.11171875 -352.9453125 53.4609375 C-351 52 -351 52 -349 52 C-349 51.34 -349 50.68 -349 50 C-346.86956389 48.16878244 -344.73240909 46.44881162 -342.5 44.75 C-341.85361572 44.25129395 -341.20723145 43.75258789 -340.54125977 43.23876953 C-338.69861779 41.81992826 -336.84988012 40.40938175 -335 39 C-333.88431641 38.13761719 -333.88431641 38.13761719 -332.74609375 37.2578125 C-330.85079942 35.80871057 -328.93326669 34.39806857 -327 33 C-326.11957031 32.360625 -325.23914063 31.72125 -324.33203125 31.0625 C-294.20508548 9.67962341 -260.54434815 -6.88838167 -225 -17 C-224.28102539 -17.21317871 -223.56205078 -17.42635742 -222.82128906 -17.64599609 C-207.81351426 -22.08269882 -192.45887415 -24.67633728 -177 -27 C-176.10684082 -27.13567383 -175.21368164 -27.27134766 -174.29345703 -27.41113281 C-115.79417589 -35.81042293 -53.22637909 -24.81181726 0 0 Z " fill="#FDFDFD" transform="translate(739,332)"/> 4 - <path d="" fill="#FFFFFF" transform="translate(0,0)"/> 1 + <svg width="150" height="150" viewBox="0 0 150 150" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M143.619 35.5547C139.137 27.8262 127.307 26.0859 109.342 30.4805C101.025 24.0495 91.0706 20.08 80.6105 19.0229C70.1504 17.9658 59.6035 19.8635 50.1679 24.5004C40.7323 29.1373 32.7863 36.3276 27.2325 45.2544C21.6788 54.1812 18.74 64.4866 18.7499 75C18.7506 77.4115 18.9033 79.8206 19.2069 82.2129C2.9823 99.0234 3.21668 108.984 6.3866 114.445C9.37488 119.625 15.6093 121.875 23.8124 121.875C29.5078 121.757 35.1697 120.971 40.6815 119.531C48.9996 125.956 58.9523 129.92 69.4098 130.973C79.8672 132.026 90.4104 130.126 99.8423 125.488C109.274 120.851 117.217 113.661 122.769 104.737C128.32 95.8126 131.259 85.5103 131.25 75C131.251 72.6003 131.1 70.2029 130.799 67.8223C138.123 60.2051 142.998 52.9688 144.656 46.7754C145.799 42.4863 145.453 38.7129 143.619 35.5547ZM74.9999 28.125C85.6167 28.1359 95.9163 31.7457 104.217 38.365C112.518 44.9844 118.329 54.2222 120.703 64.5703C110.707 73.9805 97.1776 83.6309 84.3339 91.0137C68.1913 100.277 54.1874 106.055 43.0194 109.219C36.1409 102.803 31.3532 94.4659 29.2795 85.2915C27.2059 76.1171 27.9423 66.5309 31.393 57.7809C34.8437 49.0309 40.8488 41.5224 48.6264 36.233C56.4041 30.9437 65.5941 28.1184 74.9999 28.125ZM14.496 109.74C13.6405 108.27 14.1151 105.498 15.7968 102.123C17.4597 98.9609 19.4829 96.0018 21.8261 93.3047C24.1487 100.034 27.7277 106.262 32.3729 111.656C22.2655 113.355 15.9608 112.271 14.496 109.74ZM74.9999 121.875C67.0963 121.883 59.3209 119.879 52.4061 116.051C65.0981 111.562 77.3443 105.901 88.9862 99.1406C101.631 91.8809 112.822 84.0117 121.84 76.2422C121.506 88.4482 116.426 100.043 107.68 108.564C98.934 117.085 87.2104 121.86 74.9999 121.875ZM135.598 44.3496C134.601 48.0469 131.971 52.2774 128.197 56.7305C125.881 49.9929 122.301 43.7587 117.65 38.3613C125.976 36.9844 133.687 37.0899 135.521 40.2598C136.049 41.1797 136.078 42.5567 135.598 44.3496Z" fill="white"/> 5 3 </svg>
+5
public/logo2.svg
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1280" height="1280"> 3 + <path d="M0 0 C0.81952148 0.37697021 1.63904297 0.75394043 2.48339844 1.14233398 C11.60285155 5.34423447 20.53345418 9.57571453 29 15 C30.18360833 15.72476307 31.36863018 16.44722038 32.5546875 17.16796875 C46.72974953 25.82640202 60.36518708 35.19579918 73 46 C74.16665776 46.99219797 75.33332501 47.98438479 76.5 48.9765625 C79.49524674 51.52960075 82.4772543 54.09757669 85.45507812 56.67089844 C86.77004037 57.80216495 88.09493747 58.92186671 89.421875 60.0390625 C93.40501694 63.42010158 96.85096472 66.9899968 100.2265625 70.9765625 C102.02461917 73.02808972 103.9157426 74.91519541 105.875 76.8125 C117.27976871 88.08567523 126.98680285 101.77032865 136 115 C136.72445312 116.02609375 137.44890625 117.0521875 138.1953125 118.109375 C149.2185272 133.93422347 158.34831911 151.30931379 166 169 C166.47308594 170.08667969 166.94617188 171.17335938 167.43359375 172.29296875 C173.60118867 187.08516795 178.77580507 202.5412441 183 218 C191.44559799 218.17788528 199.80780198 217.69099294 208.22385406 217.07402039 C210.57253644 216.90208377 212.92154201 216.73537984 215.27069092 216.56994629 C221.9422003 216.09910602 228.612987 215.61832316 235.28369141 215.13623047 C239.38744579 214.84000895 243.49154851 214.54903854 247.5958786 214.26091003 C249.88180192 214.09880775 252.16722718 213.93067445 254.45269775 213.7623291 C261.35894704 213.26645294 268.26668378 212.8695614 275.19042969 212.74414062 C276.94356476 212.70673767 276.94356476 212.70673767 278.7321167 212.6685791 C283.6286285 213.1651716 286.87960948 214.81736556 290.25 218.375 C292.86510786 222.2976618 294.04899829 224.65254715 293.875 229.4375 C292.5211383 234.9496512 290.58481365 237.89055902 285.91914368 241.15673828 C279.09492723 244.37226141 271.22051996 243.4377612 263.85620117 243.3046875 C261.98679061 243.28616696 260.11736129 243.26947191 258.24792004 243.25436401 C253.95263776 243.21536571 249.65777666 243.1624677 245.36275864 243.10095215 C238.03405749 242.99698969 230.70519033 242.92239103 223.37612181 242.85097337 C205.08949117 242.67173396 186.80341632 242.45106706 168.51730435 242.22583044 C159.25423381 242.11181257 149.99112738 242.00084257 140.72801854 241.88998723 C124.2191264 241.69234224 107.71026259 241.49242862 91.20141602 241.29101562 C75.19850098 241.09578017 59.19557304 240.90165839 43.19262695 240.70898438 C42.20029355 240.69703639 41.20796015 240.68508841 40.18555603 240.67277837 C35.20554558 240.6128243 30.22553488 240.5528916 25.24552405 240.49296951 C-15.83635763 239.99862339 -56.91819366 239.50056505 -98 239 C-98 238.34 -98 237.68 -98 237 C-94.0772445 236.11799576 -90.25605616 235.73677137 -86.24584961 235.47509766 C-85.58342034 235.43094601 -84.92099108 235.38679436 -84.2384882 235.34130478 C-82.04330016 235.19576448 -79.84783634 235.05492764 -77.65234375 234.9140625 C-76.09722926 234.81179793 -74.54213522 234.70922196 -72.98706055 234.60635376 C-68.83005453 234.33214466 -64.67280103 234.06187973 -60.51548386 233.79243469 C-56.21599178 233.51319162 -51.91671364 233.23069836 -47.61743164 232.94824219 C-46.76663497 232.89235176 -45.9158383 232.83646133 -45.03925991 232.77887726 C-43.32703585 232.66639738 -41.61481248 232.55390708 -39.9025898 232.44140625 C-37.34604683 232.27358827 -34.78947358 232.10623782 -32.2328949 231.93896484 C-22.87255654 231.32580498 -13.51349711 230.69804944 -4.15625 230.0390625 C-3.17010208 229.96968979 -2.18395416 229.90031708 -1.16792297 229.82884216 C0.69423473 229.69778159 2.55639194 229.56671403 4.41854858 229.43563843 C20.27341435 228.32242873 36.1087474 227.41589311 52 227 C28.48303999 174.33120936 -8.88782019 132.94486147 -63.59375 111.59765625 C-67.04662044 110.3410253 -70.50394586 109.12915527 -74 108 C-74.94004883 107.69078613 -75.88009766 107.38157227 -76.84863281 107.06298828 C-118.45317339 93.75330041 -164.24458983 95.77523221 -205 111 C-205.8161377 111.30276855 -206.63227539 111.60553711 -207.47314453 111.91748047 C-230.53456122 120.64579466 -252.10337515 134.03226571 -270 151 C-271.68585984 152.48103575 -273.37338885 153.96017343 -275.0625 155.4375 C-300.22364275 178.21166031 -318.80195062 210.61137709 -328.39599609 242.97558594 C-328.95177564 244.83836846 -329.54636871 246.68945411 -330.14453125 248.5390625 C-333.58184819 259.94529026 -334.52692461 271.69369358 -335.67456055 283.51074219 C-336 286 -336 286 -337 289 C-336.24820213 288.98955375 -335.49640427 288.97910749 -334.72182465 288.96834469 C-293.24246821 288.39439767 -251.76291629 287.90813975 -210.28125 287.53515625 C-209.23160655 287.52569292 -208.1819631 287.5162296 -207.10051227 287.5064795 C-197.31794611 287.41828419 -187.53537512 287.3306339 -177.75279903 287.24354649 C-106.47918426 286.60890316 -35.20721591 285.9227042 36.0625 284.9375 C37.62000779 284.91601508 39.17751562 284.89453324 40.7350235 284.8730545 C55.2741428 284.67240874 69.8132208 284.468893 84.3522644 284.26283836 C136.85791209 283.51880763 189.36409672 282.94243828 241.87305307 282.48904628 C246.38426787 282.45004984 250.8954738 282.41012889 255.40667725 282.36984253 C268.67279676 282.25224972 281.93888756 282.1470754 295.20529938 282.068326 C301.35536669 282.03155845 307.50527833 281.98835802 313.6552124 281.93341064 C319.95170712 281.87751368 326.2481379 281.8366901 332.54480553 281.80629158 C334.91375068 281.79235237 337.28267174 281.77321585 339.65152931 281.74853325 C342.91841738 281.71539305 346.18487402 281.70125976 349.4519043 281.69165039 C350.40687742 281.67755945 351.36185055 281.66346851 352.34576225 281.64895058 C360.63382175 281.65973849 367.26181189 283.53746305 373.4609375 289.26953125 C377.91126877 294.27333011 380.10440405 300.31814082 380 307 C379.2633535 313.73505373 376.30879778 319.88072763 371.36328125 324.54296875 C364.94911712 329.45809228 358.17568314 330.31464934 350.3125 330.1875 C349.60544922 330.18685547 348.89839844 330.18621094 348.16992188 330.18554688 C342.52031747 330.14249316 336.8900743 329.84474055 331.25 329.54296875 C319.50622724 328.92311547 307.75605631 328.61867614 296.00097179 328.29819489 C271.19987653 327.62177489 246.41454064 326.76666647 221.62908173 325.65774918 C188.37191297 324.16991624 155.10709618 323.11636312 121.83016777 322.17552948 C116.83594671 322.03412419 111.84178383 321.89076116 106.84765625 321.74609375 C105.86691006 321.71772808 104.88616386 321.68936241 103.87569809 321.66013718 C87.60406101 321.18452577 71.34266729 320.53954986 55.07943153 319.83660507 C38.37624292 319.1161931 21.67655252 318.57284445 4.96310425 318.16629028 C-22.95005425 317.4841153 -50.84562012 316.51288028 -78.74337769 315.37091064 C-83.50076148 315.17617174 -88.25817341 314.98213834 -93.015625 314.7890625 C-94.21104007 314.74052061 -94.21104007 314.74052061 -95.43060493 314.69099808 C-116.38782218 313.84312763 -137.35012931 313.14386423 -158.3125 312.4375 C-181.3783415 311.65978759 -204.44001782 310.86464293 -227.49557495 309.81860352 C-248.019523 308.89296795 -268.55379362 308.29634458 -289.08935547 307.69702148 C-305.06426974 307.2287885 -321.03269911 306.68195513 -337 306 C-332.83721069 350.28271298 -321.03864146 390.35389674 -292 425 C-291.54044922 425.54978516 -291.08089844 426.09957031 -290.60742188 426.66601562 C-270.49209967 450.66278202 -245.21131168 471.03688217 -216 483 C-214.86433594 483.47953125 -213.72867187 483.9590625 -212.55859375 484.453125 C-166.45230384 503.6473276 -113.04922114 506.33421506 -66.0625 488.125 C-63.69932313 487.10393917 -61.34516891 486.06176878 -59 485 C-57.64873805 484.42117287 -56.29716258 483.84307723 -54.9453125 483.265625 C-36.87201051 475.35334032 -15.51439117 463.69376054 -7.625 444.6875 C-6.23861212 436.50781149 -7.30923154 428.90899331 -12 422 C-15.78006204 416.99967521 -19.59482757 413.24310346 -25 410 C-25.84498047 409.37931641 -25.84498047 409.37931641 -26.70703125 408.74609375 C-39.74845449 399.30585247 -55.43098238 393.79178019 -70.59179688 388.79980469 C-72.8874013 388.03739592 -75.17098743 387.24700731 -77.453125 386.4453125 C-94.46665541 380.5215192 -111.97492709 376.24400154 -129.58813477 372.51586914 C-131.42643526 372.12267864 -133.26247165 371.71888686 -135.09741211 371.31030273 C-143.83687428 369.38482551 -152.59916928 367.83348946 -161.4375 366.4375 C-163.37206055 366.12522461 -163.37206055 366.12522461 -165.34570312 365.80664062 C-174.21378779 364.39005625 -183.09361855 363.14600372 -192 362 C-192 361.01 -192 360.02 -192 359 C-174.93963303 359.24208307 -157.87928634 359.48554849 -140.81896019 359.73049068 C-132.89054582 359.84429281 -124.96212626 359.95770164 -117.03369141 360.07006836 C-107.84275483 360.20037598 -98.65183887 360.33207676 -89.4609375 360.46484375 C-88.70015616 360.47580755 -87.93937483 360.48677135 -87.15553951 360.49806738 C-67.8765974 360.77615397 -48.59910296 361.09006679 -29.32185745 361.47047615 C-3.01784746 361.98873757 23.28473973 362.25424325 49.59298706 362.44555664 C62.43835546 362.5390792 75.28362534 362.64526654 88.12890625 362.75 C89.4088553 362.76035782 90.68880436 362.77071564 92.00753975 362.78138733 C124.73477588 363.04668073 157.46095613 363.38671159 190.18638611 363.82156372 C198.45325137 363.93116943 206.72007292 364.0313398 214.98725033 364.11433029 C220.94016887 364.17467005 226.89280754 364.24914014 232.8454895 364.32983398 C235.19819474 364.35941695 237.55096674 364.38417571 239.90377808 364.40356445 C243.03906095 364.43013636 246.17359396 364.47440242 249.30859375 364.5234375 C250.20195435 364.52665009 251.09531494 364.52986267 252.01574707 364.53317261 C257.39856495 364.64128791 261.2242674 365.45955875 266 368 C267.70091875 370.21858967 267.95238868 371.69052639 268.375 374.4375 C267.86233872 377.94068541 266.47871818 379.52128182 264 382 C261.14387256 382.97054331 258.50107568 383.12186828 255.5 383.140625 C237.48314124 383.46327056 220.98389724 386.36246221 205 395 C203.73542969 395.65742187 203.73542969 395.65742187 202.4453125 396.328125 C179.13021457 409.28923376 166.78509512 430.60877584 154.37109375 453.22924805 C148.12930001 464.59791958 141.71783758 475.56523937 134 486 C133.51966309 486.66 133.03932617 487.32 132.54443359 488 C126.16026194 496.76967189 119.50449706 505.24579868 112.37329102 513.421875 C111.40855875 514.53050244 110.45135077 515.64570077 109.49975586 516.765625 C104.77542249 522.27443144 99.74056099 527.44563145 94.60961914 532.57568359 C93.16718144 534.02034996 91.73298435 535.47289854 90.29882812 536.92578125 C88.93467773 538.29283203 88.93467773 538.29283203 87.54296875 539.6875 C86.71821045 540.51636719 85.89345215 541.34523438 85.04370117 542.19921875 C83 544 83 544 81 544 C81 544.66 81 545.32 81 546 C68.9840536 557.56601008 53.95203579 566.95219487 40 576 C39.09121094 576.60328125 38.18242188 577.2065625 37.24609375 577.828125 C31.76658773 581.41357702 26.21191334 584.50545563 20.31640625 587.3515625 C17.94737407 588.52910264 15.64571165 589.73445795 13.33984375 591.0234375 C1.87345416 597.39507428 -10.03038028 602.27948577 -22.27587891 606.95751953 C-24.44496308 607.78759566 -26.60489965 608.63496421 -28.76171875 609.49609375 C-50.30974428 617.74220139 -73.86861575 622.64622575 -96.75 625.1875 C-97.52223907 625.27371613 -98.29447815 625.35993225 -99.09011841 625.44876099 C-125.94408589 628.37463847 -152.22105241 627.52049032 -179 624 C-179.94004883 623.88253418 -180.88009766 623.76506836 -181.84863281 623.64404297 C-231.68341913 617.35750272 -278.44029652 597.7658453 -320 570 C-320.92425781 569.38382813 -321.84851563 568.76765625 -322.80078125 568.1328125 C-333.85919468 560.5674822 -344.14703878 552.06335109 -354 543 C-355.41207844 541.74481917 -356.8284912 540.49449073 -358.25 539.25 C-358.89324219 538.68667969 -359.53648437 538.12335937 -360.19921875 537.54296875 C-361.78999235 536.17994148 -363.42343723 534.86706017 -365.0625 533.5625 C-368.10505563 530.90835573 -370.62166258 528.08480873 -373.23046875 525.015625 C-374.83077503 523.19275957 -376.48869775 521.53961978 -378.25 519.875 C-383.20730242 514.99515543 -387.30767706 509.37557702 -391.54296875 503.87597656 C-392.98029226 502.02537438 -394.43861788 500.19381589 -395.90625 498.3671875 C-401.41672544 491.49483608 -406.44046932 484.55337566 -411 477 C-412.37430079 474.77040219 -413.74932368 472.54124935 -415.125 470.3125 C-415.79789063 469.21164062 -416.47078125 468.11078125 -417.1640625 466.9765625 C-418.57625843 464.68700229 -420.0243424 462.4356495 -421.5 460.1875 C-436.90389769 435.53092552 -447.26413521 406.07933033 -454 378 C-454.22139648 377.08911621 -454.44279297 376.17823242 -454.67089844 375.23974609 C-460.44738189 351.00141796 -463.8098876 325.9249354 -463 301 C-464.21362213 301.0104686 -465.42724426 301.02093719 -466.67764282 301.03172302 C-477.87407841 301.09810038 -489.00490254 300.81780458 -500.18492508 300.23461723 C-518.06717644 299.31075482 -535.95266072 298.67532138 -553.8515625 298.18359375 C-554.9869622 298.15226978 -556.12236191 298.12094582 -557.29216766 298.08867264 C-564.2312066 297.89827166 -571.17038034 297.7132298 -578.10961914 297.53027344 C-587.49793899 297.2825089 -596.88612916 297.0301319 -606.27421188 296.77354622 C-609.49204809 296.68655439 -612.70995627 296.60280548 -615.92788696 296.51939392 C-627.63337202 296.20732084 -639.31294993 295.7305766 -651 295 C-651 294.34 -651 293.68 -651 293 C-647.26788977 291.75596326 -643.78316281 291.83133129 -639.89428711 291.81054688 C-638.64620569 291.79807718 -638.64620569 291.79807718 -637.3729105 291.78535557 C-634.59107611 291.75864525 -631.8092337 291.73876882 -629.02734375 291.71875 C-627.05822324 291.70094416 -625.08910701 291.68265953 -623.11999512 291.66392517 C-618.91618657 291.62471772 -614.71236708 291.58831685 -610.5085144 291.5541687 C-600.62843159 291.47370501 -590.74847797 291.37938452 -580.86851692 291.28530693 C-578.65935492 291.26432422 -576.45019123 291.24351841 -574.24102592 291.22288704 C-551.699903 291.01204131 -529.16157638 290.68837798 -506.62280273 290.3046875 C-492.08021035 290.06158667 -477.54466498 289.93917051 -463 290 C-463.02392822 289.19892822 -463.04785645 288.39785645 -463.07250977 287.57250977 C-463.23685879 277.53725862 -462.27784296 267.86796153 -460.9375 257.9375 C-460.82740601 257.11051605 -460.71731201 256.2835321 -460.60388184 255.43148804 C-451.80786134 190.21780463 -423.21981592 128.7489231 -379 80 C-378.46036621 79.40396973 -377.92073242 78.80793945 -377.36474609 78.19384766 C-373.94530852 74.4290949 -370.50856591 70.68197919 -367 67 C-366.46874512 66.43812988 -365.93749023 65.87625977 -365.39013672 65.29736328 C-363.96690655 63.82501163 -362.48719596 62.40771151 -361 61 C-360.34 61 -359.68 61 -359 61 C-359 60.34 -359 59.68 -359 59 C-357.3203125 57.2734375 -357.3203125 57.2734375 -355.125 55.375 C-354.40570313 54.74335938 -353.68640625 54.11171875 -352.9453125 53.4609375 C-351 52 -351 52 -349 52 C-349 51.34 -349 50.68 -349 50 C-346.86956389 48.16878244 -344.73240909 46.44881162 -342.5 44.75 C-341.85361572 44.25129395 -341.20723145 43.75258789 -340.54125977 43.23876953 C-338.69861779 41.81992826 -336.84988012 40.40938175 -335 39 C-333.88431641 38.13761719 -333.88431641 38.13761719 -332.74609375 37.2578125 C-330.85079942 35.80871057 -328.93326669 34.39806857 -327 33 C-326.11957031 32.360625 -325.23914063 31.72125 -324.33203125 31.0625 C-294.20508548 9.67962341 -260.54434815 -6.88838167 -225 -17 C-224.28102539 -17.21317871 -223.56205078 -17.42635742 -222.82128906 -17.64599609 C-207.81351426 -22.08269882 -192.45887415 -24.67633728 -177 -27 C-176.10684082 -27.13567383 -175.21368164 -27.27134766 -174.29345703 -27.41113281 C-115.79417589 -35.81042293 -53.22637909 -24.81181726 0 0 Z " fill="#FDFDFD" transform="translate(739,332)"/> 4 + <path d="" fill="#FFFFFF" transform="translate(0,0)"/> 5 + </svg>
public/mars.png

This is a binary file and will not be displayed.

public/neptune.png

This is a binary file and will not be displayed.

public/venus.png

This is a binary file and will not be displayed.

+35
supabase/migrations/006_add_pds_services.sql
··· 1 + -- Track which PDS service has been provisioned for each user 2 + -- This lets the dashboard fetch the correct service via GET /service/{id} 3 + CREATE TABLE IF NOT EXISTS pds_services ( 4 + user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, 5 + pds_service_id BIGINT, 6 + hostname TEXT, 7 + status TEXT NOT NULL DEFAULT 'provisioning', 8 + created_at TIMESTAMPTZ DEFAULT NOW(), 9 + updated_at TIMESTAMPTZ DEFAULT NOW() 10 + ); 11 + 12 + ALTER TABLE pds_services ENABLE ROW LEVEL SECURITY; 13 + 14 + -- Users can see only their own provisioned PDS 15 + DROP POLICY IF EXISTS "Users can view own pds services" ON pds_services; 16 + CREATE POLICY "Users can view own pds services" 17 + ON pds_services 18 + FOR SELECT 19 + USING (auth.uid() = user_id); 20 + 21 + -- Keep updated_at current 22 + CREATE OR REPLACE FUNCTION update_pds_services_updated_at_column() 23 + RETURNS TRIGGER AS $$ 24 + BEGIN 25 + NEW.updated_at = NOW(); 26 + RETURN NEW; 27 + END; 28 + $$ language 'plpgsql'; 29 + 30 + DROP TRIGGER IF EXISTS update_pds_services_updated_at ON pds_services; 31 + CREATE TRIGGER update_pds_services_updated_at 32 + BEFORE UPDATE ON pds_services 33 + FOR EACH ROW 34 + EXECUTE FUNCTION update_pds_services_updated_at_column(); 35 +