mirror of
https://github.com/temetro/temetro.git
synced 2026-08-19 22:56:17 +00:00
Build temetro app: dark theme, clinical sidebar, AI chat, settings page
- Linear-inspired dark theme (app/globals.css), forced dark in layout
- Clinical sidebar (components/sidebar-02) with temetro brand
- AI chat UI (components/chat): Cursor-style input, UI-only mock replies
- Settings page (components/settings): Polar-style tabs/sections, UI only
- Route-group app shell: app/(app)/{layout,page,settings}
- shadcn (Base UI) + ai-elements component libraries
- CLAUDE.md: architecture notes + per-folder commit policy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SettingsCard,
|
||||
SettingsSection,
|
||||
whiteButton,
|
||||
} from "@/components/settings/settings-parts";
|
||||
|
||||
export function BillingPanel() {
|
||||
return (
|
||||
<>
|
||||
<SettingsCard className="flex flex-col gap-6 p-6 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-xl font-semibold tracking-tight">Early Member</h3>
|
||||
<Badge className="bg-emerald-500/15 text-emerald-400">Active</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
For our founding community of early members.
|
||||
</p>
|
||||
<Button className={cn("rounded-lg", whiteButton)}>Change plan</Button>
|
||||
</div>
|
||||
<div className="sm:text-right">
|
||||
<p className="text-3xl font-semibold tracking-tight">Free</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
4.00% + $0.40 per transaction
|
||||
</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsSection
|
||||
action={
|
||||
<Button className="rounded-lg" variant="secondary">
|
||||
Add payment method
|
||||
</Button>
|
||||
}
|
||||
description="Cards used to pay for your temetro subscription"
|
||||
title="Payment methods"
|
||||
>
|
||||
<SettingsCard className="flex items-center justify-center p-12">
|
||||
<p className="text-sm text-muted-foreground">No payment methods on file</p>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
action={
|
||||
<Button className="rounded-lg" variant="secondary">
|
||||
Edit
|
||||
</Button>
|
||||
}
|
||||
description="Used on invoices for your temetro subscription"
|
||||
title="Billing address"
|
||||
>
|
||||
<SettingsCard className="p-5">
|
||||
<p className="text-sm">khalid</p>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Copy } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export function SettingsSection({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold tracking-tight">{title}</h2>
|
||||
{description ? (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className="shrink-0">{action}</div> : null}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsCard({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-2xl border border-border bg-card/30", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToggleRow({
|
||||
title,
|
||||
description,
|
||||
defaultChecked = false,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
defaultChecked?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SettingsCard className="flex items-center justify-between gap-4 px-4 py-3.5">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
{description ? (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch defaultChecked={defaultChecked} />
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function CopyField({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
description?: string;
|
||||
value: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{label}</p>
|
||||
{description ? (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex h-9 w-full items-center gap-2 rounded-3xl bg-input/50 pr-1 pl-3 sm:w-80">
|
||||
<span className="flex-1 truncate text-sm text-muted-foreground">
|
||||
{value}
|
||||
</span>
|
||||
<button
|
||||
className="flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:bg-accent"
|
||||
type="button"
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Light/white CTA used for primary actions on the dark settings surface. */
|
||||
export const whiteButton =
|
||||
"bg-foreground text-background hover:bg-foreground/90";
|
||||
|
||||
/** Field label with an optional required asterisk. */
|
||||
export function FieldLabel({
|
||||
children,
|
||||
required,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className="text-sm font-medium">
|
||||
{children}
|
||||
{required ? <span className="text-muted-foreground"> *</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, Plus } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
CopyField,
|
||||
FieldLabel,
|
||||
SettingsCard,
|
||||
SettingsSection,
|
||||
ToggleRow,
|
||||
whiteButton,
|
||||
} from "@/components/settings/settings-parts";
|
||||
|
||||
const customerNotifications = [
|
||||
{
|
||||
title: "Order confirmation",
|
||||
description: "Sent when a customer completes a one-time purchase",
|
||||
},
|
||||
{
|
||||
title: "Subscription confirmation",
|
||||
description: "Sent when a customer starts a new subscription",
|
||||
},
|
||||
{
|
||||
title: "Subscription cycled",
|
||||
description: "Sent when a subscription automatically renews",
|
||||
},
|
||||
{
|
||||
title: "Trial converted",
|
||||
description: "Sent when a trial ends and the subscription becomes paid",
|
||||
},
|
||||
{
|
||||
title: "Renewal reminder",
|
||||
description: "Sent 7 days before a subscription with a long billing cycle renews",
|
||||
},
|
||||
{
|
||||
title: "Trial conversion reminder",
|
||||
description: "Sent before a trial ends and converts to a paid subscription",
|
||||
},
|
||||
{
|
||||
title: "Subscription updated",
|
||||
description: "Sent when a customer changes their subscription to a different product",
|
||||
},
|
||||
];
|
||||
|
||||
export function PreferencesPanel() {
|
||||
return (
|
||||
<>
|
||||
<SettingsSection title="Organization">
|
||||
<SettingsCard className="space-y-6 p-5">
|
||||
<CopyField
|
||||
description="Unique identifier for your organization"
|
||||
label="Identifier"
|
||||
value="62a5278f-91c6-4912-b711-ee1c9c2f0a73"
|
||||
/>
|
||||
<CopyField
|
||||
description="Used for Customer Portal, Transaction Statements, etc."
|
||||
label="Organization Slug"
|
||||
value="khalid"
|
||||
/>
|
||||
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel>Logo</FieldLabel>
|
||||
<Avatar className="size-10 rounded-xl" size="lg">
|
||||
<AvatarFallback className="rounded-xl bg-muted text-sm font-medium">
|
||||
K
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<FieldLabel required>Organization Name</FieldLabel>
|
||||
<Input defaultValue="khalid" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel>Country</FieldLabel>
|
||||
<button
|
||||
className="flex h-9 w-full items-center justify-between rounded-3xl bg-input/50 px-3 text-sm text-muted-foreground transition-colors hover:bg-input/70"
|
||||
type="button"
|
||||
>
|
||||
Select country
|
||||
<ChevronDown className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel required>Website</FieldLabel>
|
||||
<Input placeholder="https://acme.com" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel required>Support Email</FieldLabel>
|
||||
<Input placeholder="support@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="space-y-0.5">
|
||||
<FieldLabel>Social Media</FieldLabel>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your personal social media links are used for identity verification. They
|
||||
will never be shown publicly.
|
||||
</p>
|
||||
</div>
|
||||
<Button className="rounded-lg" size="sm" variant="outline">
|
||||
<Plus className="size-4" />
|
||||
Add Social
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
description="Emails automatically sent to customers for purchases, renewals, and other subscription lifecycle events"
|
||||
title="Customer notifications"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{customerNotifications.map((item) => (
|
||||
<ToggleRow
|
||||
defaultChecked
|
||||
description={item.description}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
description="Emails sent to members of your organization for account and product activity"
|
||||
title="Account notifications"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ToggleRow
|
||||
defaultChecked
|
||||
description="Send a notification when new orders are created"
|
||||
title="New Orders"
|
||||
/>
|
||||
<ToggleRow
|
||||
defaultChecked
|
||||
description="Send a notification when new subscriptions are created"
|
||||
title="New Subscriptions"
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
description="Manage alpha & beta features for your organization"
|
||||
title="Features"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<ToggleRow
|
||||
description="Show translated checkouts to your customers"
|
||||
title="Localized Checkout"
|
||||
/>
|
||||
<ToggleRow
|
||||
description="Enable seat-based pricing for subscription products. Requires the member model to be enabled."
|
||||
title="Seat-Based Billing"
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
description="Manage access tokens to authenticate with the temetro API"
|
||||
title="Developers"
|
||||
>
|
||||
<SettingsCard className="flex flex-col items-start gap-4 p-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You don't have any active organization access tokens.
|
||||
</p>
|
||||
<Button className={cn("rounded-lg", whiteButton)} size="sm">
|
||||
Create token
|
||||
</Button>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection
|
||||
description="Irreversible actions for this organization"
|
||||
title="Danger Zone"
|
||||
>
|
||||
<SettingsCard className="flex items-center justify-between gap-4 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">Delete Organization</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Permanently delete this organization and all associated data. This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<Button className="rounded-lg bg-destructive text-white hover:bg-destructive/90">
|
||||
Delete
|
||||
</Button>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
SettingsCard,
|
||||
SettingsSection,
|
||||
} from "@/components/settings/settings-parts";
|
||||
import { BillingPanel } from "@/components/settings/settings-billing";
|
||||
import { PreferencesPanel } from "@/components/settings/settings-preferences";
|
||||
|
||||
const TABS = [
|
||||
"Preferences",
|
||||
"Billing",
|
||||
"Members",
|
||||
"Webhooks",
|
||||
"Custom Fields",
|
||||
] as const;
|
||||
|
||||
type Tab = (typeof TABS)[number];
|
||||
|
||||
function PlaceholderPanel({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<SettingsSection description={description} title={title}>
|
||||
<SettingsCard className="flex items-center justify-center p-12">
|
||||
<p className="text-sm text-muted-foreground">Nothing here yet.</p>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsView() {
|
||||
const [tab, setTab] = useState<Tab>("Preferences");
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-10">
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{tab}</h1>
|
||||
<nav className="flex flex-wrap items-center gap-1">
|
||||
{TABS.map((item) => (
|
||||
<button
|
||||
className={cn(
|
||||
"rounded-lg px-3 py-1.5 text-sm transition-colors",
|
||||
tab === item
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
key={item}
|
||||
onClick={() => setTab(item)}
|
||||
type="button"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 space-y-12">
|
||||
{tab === "Preferences" && <PreferencesPanel />}
|
||||
{tab === "Billing" && <BillingPanel />}
|
||||
{tab === "Members" && (
|
||||
<PlaceholderPanel
|
||||
description="Invite and manage members of your organization"
|
||||
title="Members"
|
||||
/>
|
||||
)}
|
||||
{tab === "Webhooks" && (
|
||||
<PlaceholderPanel
|
||||
description="Send event notifications to your own endpoints"
|
||||
title="Webhooks"
|
||||
/>
|
||||
)}
|
||||
{tab === "Custom Fields" && (
|
||||
<PlaceholderPanel
|
||||
description="Collect additional information from your customers"
|
||||
title="Custom Fields"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user