this repo has no description
0
fork

Configure Feed

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

refactor: extract page header component

+202 -134
+16 -16
app/(creator)/dashboard/page.tsx
··· 4 4 import { createFormAction } from "@/app/(creator)/actions"; 5 5 import { DashboardFormBrowser } from "@/components/dashboard-form-browser"; 6 6 import { EmptyState } from "@/components/empty-state"; 7 + import { PageHeader } from "@/components/ui/page-header"; 7 8 import { Button } from "@/components/ui/button"; 8 9 import { getServerAuthSession } from "@/lib/auth"; 9 10 import { listFormsForWorkspace } from "@/lib/forms"; ··· 38 39 39 40 return forms.length === 0 ? ( 40 41 <div className="space-y-8"> 41 - <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-6 lg:flex-row lg:items-end lg:justify-between"> 42 - <div className="space-y-2"> 43 - <h1 className="font-display text-4xl leading-tight text-[var(--ink)]"> 44 - {workspaceLabel} 45 - </h1> 46 - <p className="max-w-2xl text-sm leading-6 text-[var(--muted)]"> 47 - {t("dashboard.description")} 48 - </p> 49 - </div> 50 - <form action={createFormAction}> 51 - <Button size="lg" type="submit"> 52 - <Plus className="size-4" /> 53 - {t("dashboard.newForm")} 54 - </Button> 55 - </form> 56 - </section> 42 + <PageHeader 43 + title={workspaceLabel} 44 + description={t("dashboard.description")} 45 + descriptionClassName="max-w-2xl" 46 + titleClassName="leading-tight" 47 + className="pb-6" 48 + actions={ 49 + <form action={createFormAction}> 50 + <Button size="lg" type="submit"> 51 + <Plus className="size-4" /> 52 + {t("dashboard.newForm")} 53 + </Button> 54 + </form> 55 + } 56 + /> 57 57 58 58 <EmptyState 59 59 eyebrow={t("dashboard.noFormsEyebrow")}
+16 -21
app/(creator)/forms/[id]/responses/[responseId]/page.tsx
··· 4 4 5 5 import { Badge } from "@/components/ui/badge"; 6 6 import { Button } from "@/components/ui/button"; 7 + import { PageHeader } from "@/components/ui/page-header"; 7 8 import { Card } from "@/components/ui/card"; 8 9 import { AuthoredMarkdown } from "@/components/ui/authored-markdown"; 9 10 import { AGREEMENT_ANSWER_VALUES } from "@/lib/blocks"; ··· 59 60 60 61 return ( 61 62 <div className="space-y-6"> 62 - <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-end lg:justify-between"> 63 - <div> 64 - <h1 className="font-display text-4xl text-[var(--ink)]"> 65 - {t("responseDetail.title", { 66 - number: response.submissionNumber, 67 - date: formatDate(response.submittedAt, locale), 68 - })} 69 - </h1> 70 - <p className="mt-2 text-xs font-medium uppercase text-[var(--muted)]"> 71 - {workspaceLabel} 72 - </p> 73 - <p className="mt-2 text-sm leading-6 text-[var(--muted)]"> 74 - {t("responseDetail.description")} 75 - </p> 76 - </div> 77 - <Link href={`/forms/${id}/responses`}> 78 - <Button variant="secondary"> 79 - {t("responseDetail.backToResponses")} 80 - </Button> 81 - </Link> 82 - </section> 63 + <PageHeader 64 + title={t("responseDetail.title", { 65 + number: response.submissionNumber, 66 + date: formatDate(response.submittedAt, locale), 67 + })} 68 + metadata={workspaceLabel} 69 + description={t("responseDetail.description")} 70 + actions={ 71 + <Link href={`/forms/${id}/responses`}> 72 + <Button variant="secondary"> 73 + {t("responseDetail.backToResponses")} 74 + </Button> 75 + </Link> 76 + } 77 + /> 83 78 84 79 <Card className="p-6"> 85 80 <div className="flex flex-wrap items-center gap-3">
+15 -16
app/(creator)/forms/[id]/responses/page.tsx
··· 5 5 6 6 import { EmptyState } from "@/components/empty-state"; 7 7 import { ResponseExportActions } from "@/components/response-export-actions"; 8 + import { PageHeader } from "@/components/ui/page-header"; 8 9 import { Badge } from "@/components/ui/badge"; 9 10 import { Button } from "@/components/ui/button"; 10 11 import { Card } from "@/components/ui/card"; ··· 99 100 100 101 return ( 101 102 <div className="space-y-5"> 102 - <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-end lg:justify-between"> 103 - <div> 104 - <h1 className="font-display text-4xl text-[var(--ink)]"> 105 - {form.title} 106 - </h1> 107 - <p className="mt-2 text-xs font-medium uppercase text-[var(--muted)]"> 108 - {workspaceLabel} 109 - </p> 110 - </div> 111 - <div className="flex flex-wrap gap-3 lg:justify-end"> 112 - <Link href={`/forms/${form.id}/edit`}> 113 - <Button variant="secondary">{t("responses.backToBuilder")}</Button> 114 - </Link> 115 - <ResponseExportActions formId={form.id} /> 116 - </div> 117 - </section> 103 + <PageHeader 104 + title={form.title} 105 + metadata={workspaceLabel} 106 + actions={ 107 + <> 108 + <Link href={`/forms/${form.id}/edit`}> 109 + <Button variant="secondary"> 110 + {t("responses.backToBuilder")} 111 + </Button> 112 + </Link> 113 + <ResponseExportActions formId={form.id} /> 114 + </> 115 + } 116 + /> 118 117 119 118 <div className="flex flex-wrap items-center justify-between gap-3"> 120 119 <div className="inline-flex h-12 items-center gap-1 rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] p-1">
+47 -47
components/dashboard-form-browser.tsx
··· 15 15 import { useI18n } from "@/components/i18n-provider"; 16 16 import { FormStatusBadge } from "@/components/form-status-badge"; 17 17 import { Button } from "@/components/ui/button"; 18 + import { PageHeader } from "@/components/ui/page-header"; 18 19 import { Card } from "@/components/ui/card"; 19 20 import type { FormListItem } from "@/lib/form-types"; 20 21 import { cn, formatDate } from "@/lib/utils"; ··· 123 124 124 125 return ( 125 126 <div className="space-y-8"> 126 - <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-6 lg:flex-row lg:items-end lg:justify-between"> 127 - <div className="space-y-2"> 128 - <h1 className="font-display text-4xl leading-tight text-[var(--ink)]"> 129 - {workspaceLabel} 130 - </h1> 131 - <p className="max-w-2xl text-sm leading-6 text-[var(--muted)]"> 132 - {t("dashboard.description")} 133 - </p> 134 - </div> 135 - <div className="flex flex-col gap-3 sm:flex-row sm:items-center"> 136 - <div className="inline-flex h-12 items-center gap-1 rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] p-1"> 137 - <button 138 - type="button" 139 - onClick={() => setDashboardView("grid")} 140 - className={cn( 141 - "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 142 - view === "grid" 143 - ? "bg-[var(--ink)] text-[var(--bg)]" 144 - : "text-[var(--muted)] hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]", 145 - )} 146 - aria-pressed={view === "grid"} 147 - > 148 - <LayoutGrid className="size-4" /> 149 - {t("dashboard.viewGrid")} 150 - </button> 151 - <button 152 - type="button" 153 - onClick={() => setDashboardView("table")} 154 - className={cn( 155 - "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 156 - view === "table" 157 - ? "bg-[var(--ink)] text-[var(--bg)]" 158 - : "text-[var(--muted)] hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]", 159 - )} 160 - aria-pressed={view === "table"} 161 - > 162 - <TableProperties className="size-4" /> 163 - {t("dashboard.viewTable")} 164 - </button> 127 + <PageHeader 128 + title={workspaceLabel} 129 + description={t("dashboard.description")} 130 + descriptionClassName="max-w-2xl" 131 + titleClassName="leading-tight" 132 + className="pb-6" 133 + actions={ 134 + <div className="flex flex-col gap-3 sm:flex-row sm:items-center"> 135 + <div className="inline-flex h-12 items-center gap-1 rounded-xl border border-[color:var(--line)] bg-[var(--surface-strong)] p-1"> 136 + <button 137 + type="button" 138 + onClick={() => setDashboardView("grid")} 139 + className={cn( 140 + "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 141 + view === "grid" 142 + ? "bg-[var(--ink)] text-[var(--bg)]" 143 + : "text-[var(--muted)] hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]", 144 + )} 145 + aria-pressed={view === "grid"} 146 + > 147 + <LayoutGrid className="size-4" /> 148 + {t("dashboard.viewGrid")} 149 + </button> 150 + <button 151 + type="button" 152 + onClick={() => setDashboardView("table")} 153 + className={cn( 154 + "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 155 + view === "table" 156 + ? "bg-[var(--ink)] text-[var(--bg)]" 157 + : "text-[var(--muted)] hover:bg-[var(--accent-soft)] hover:text-[var(--ink)]", 158 + )} 159 + aria-pressed={view === "table"} 160 + > 161 + <TableProperties className="size-4" /> 162 + {t("dashboard.viewTable")} 163 + </button> 164 + </div> 165 + <form action={createFormAction}> 166 + <Button size="lg" type="submit"> 167 + <Plus className="size-4" /> 168 + {t("dashboard.newForm")} 169 + </Button> 170 + </form> 165 171 </div> 166 - <form action={createFormAction}> 167 - <Button size="lg" type="submit"> 168 - <Plus className="size-4" /> 169 - {t("dashboard.newForm")} 170 - </Button> 171 - </form> 172 - </div> 173 - </section> 172 + } 173 + /> 174 174 175 175 {view === "grid" ? ( 176 176 <section className="grid gap-5 lg:grid-cols-2">
+6 -8
components/organization-settings-panel.tsx
··· 15 15 import { useLocalToasts } from "@/components/use-local-toasts"; 16 16 import { Badge } from "@/components/ui/badge"; 17 17 import { Button } from "@/components/ui/button"; 18 + import { PageHeader } from "@/components/ui/page-header"; 18 19 import { Input } from "@/components/ui/input"; 19 20 import { 20 21 Select, ··· 69 70 <> 70 71 <ToastViewport toasts={toasts} onDismiss={dismissToast} /> 71 72 <div> 72 - <div> 73 - <h1 className="font-display text-4xl text-[var(--ink)]"> 74 - {t("settings.organizations.title")} 75 - </h1> 76 - <p className="mt-3 max-w-3xl text-sm leading-6 text-[var(--muted)]"> 77 - {t("settings.organizations.description")} 78 - </p> 79 - </div> 73 + <PageHeader 74 + title={t("settings.organizations.title")} 75 + description={t("settings.organizations.description")} 76 + className="border-b-0 pb-0" 77 + /> 80 78 81 79 <div className="mt-8 border-t border-[color:var(--line)] pt-8"> 82 80 <form
+7 -8
components/profile-settings-panel.tsx
··· 8 8 import { UserAvatar } from "@/components/user-avatar"; 9 9 import { Button } from "@/components/ui/button"; 10 10 import { Input } from "@/components/ui/input"; 11 + import { PageHeader } from "@/components/ui/page-header"; 11 12 import { 12 13 Select, 13 14 SelectContent, ··· 44 45 45 46 return ( 46 47 <div> 47 - <div> 48 - <h1 className="font-display text-4xl text-[var(--ink)]"> 49 - {t("settings.profile.title")} 50 - </h1> 51 - <p className="mt-3 max-w-2xl text-sm leading-6 text-[var(--muted)]"> 52 - {t("settings.profile.description")} 53 - </p> 54 - </div> 48 + <PageHeader 49 + title={t("settings.profile.title")} 50 + description={t("settings.profile.description")} 51 + descriptionClassName="max-w-2xl" 52 + className="border-b-0 pb-0" 53 + /> 55 54 56 55 <form action={submitProfile} className="mt-8 space-y-8"> 57 56 <input type="hidden" name="returnSection" value="profile" />
+6 -10
components/settings-shell.tsx
··· 8 8 import { OrganizationSettingsPanel } from "@/components/organization-settings-panel"; 9 9 import { ProfileSettingsPanel } from "@/components/profile-settings-panel"; 10 10 import { ThemeSettingsPanel } from "@/components/theme-settings-panel"; 11 + import { PageHeader } from "@/components/ui/page-header"; 11 12 import { cn } from "@/lib/utils"; 12 13 import type { 13 14 OrganizationSettingsSummary, ··· 57 58 58 59 return ( 59 60 <div className="space-y-6"> 60 - <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-end lg:justify-between"> 61 - <div> 62 - <h1 className="font-display text-4xl text-[var(--ink)]"> 63 - {t("settings.shell.title")} 64 - </h1> 65 - <p className="mt-2 max-w-2xl text-sm leading-6 text-[var(--muted)]"> 66 - {t("settings.shell.description")} 67 - </p> 68 - </div> 69 - </section> 61 + <PageHeader 62 + title={t("settings.shell.title")} 63 + description={t("settings.shell.description")} 64 + descriptionClassName="max-w-2xl" 65 + /> 70 66 71 67 <div className="grid gap-8 lg:grid-cols-[280px_minmax(0,1fr)] lg:items-start"> 72 68 <div className="space-y-2 border-b border-[color:var(--line)] pb-6 lg:border-b-0 lg:border-r lg:pb-0 lg:pr-8">
+7 -8
components/theme-settings-panel.tsx
··· 4 4 5 5 import { useI18n } from "@/components/i18n-provider"; 6 6 import { useThemePreference } from "@/components/theme-provider"; 7 + import { PageHeader } from "@/components/ui/page-header"; 7 8 import { cn } from "@/lib/utils"; 8 9 import type { ThemePreference } from "@/lib/theme"; 9 10 ··· 31 32 32 33 return ( 33 34 <div> 34 - <div> 35 - <h1 className="font-display text-4xl text-[var(--ink)]"> 36 - {t("settings.appearance.title")} 37 - </h1> 38 - <p className="mt-3 max-w-2xl text-sm leading-6 text-[var(--muted)]"> 39 - {t("settings.appearance.description")} 40 - </p> 41 - </div> 35 + <PageHeader 36 + title={t("settings.appearance.title")} 37 + description={t("settings.appearance.description")} 38 + descriptionClassName="max-w-2xl" 39 + className="border-b-0 pb-0" 40 + /> 42 41 43 42 <div className="mt-8 grid gap-4 md:grid-cols-3"> 44 43 {options.map((option) => {
+82
components/ui/page-header.tsx
··· 1 + import type { ReactNode } from "react"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + 5 + type PageHeaderProps = { 6 + title: ReactNode; 7 + description?: ReactNode; 8 + metadata?: ReactNode; 9 + actions?: ReactNode; 10 + as?: "h1" | "h2"; 11 + className?: string; 12 + titleClassName?: string; 13 + descriptionClassName?: string; 14 + metadataClassName?: string; 15 + actionsClassName?: string; 16 + }; 17 + 18 + export function PageHeader({ 19 + title, 20 + description, 21 + metadata, 22 + actions, 23 + as = "h1", 24 + className, 25 + titleClassName, 26 + descriptionClassName, 27 + metadataClassName, 28 + actionsClassName, 29 + }: PageHeaderProps) { 30 + const Title = as; 31 + 32 + return ( 33 + <section 34 + className={cn( 35 + "flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-end lg:justify-between", 36 + className, 37 + )} 38 + > 39 + <div> 40 + <Title 41 + className={cn( 42 + "font-display text-4xl text-[var(--ink)]", 43 + titleClassName, 44 + )} 45 + > 46 + {title} 47 + </Title> 48 + {metadata ? ( 49 + <p 50 + className={cn( 51 + "mt-2 text-xs font-medium uppercase text-[var(--muted)]", 52 + metadataClassName, 53 + )} 54 + > 55 + {metadata} 56 + </p> 57 + ) : null} 58 + {description ? ( 59 + <div 60 + className={cn( 61 + metadata ? "mt-2" : "mt-3", 62 + "max-w-3xl text-sm leading-6 text-[var(--muted)]", 63 + descriptionClassName, 64 + )} 65 + > 66 + {description} 67 + </div> 68 + ) : null} 69 + </div> 70 + {actions ? ( 71 + <div 72 + className={cn( 73 + "flex flex-wrap gap-3 lg:justify-end", 74 + actionsClassName, 75 + )} 76 + > 77 + {actions} 78 + </div> 79 + ) : null} 80 + </section> 81 + ); 82 + }