this repo has no description
0
fork

Configure Feed

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

feat: refine builder ui and add site footer

+296 -92
+1 -2
app/(creator)/forms/[id]/responses/page.tsx
··· 41 41 <div> 42 42 <h1 className="font-display text-4xl text-[var(--ink)]">{form.title}</h1> 43 43 <p className="mt-2 text-xs font-medium uppercase text-[var(--muted)]">{workspaceLabel}</p> 44 - <p className="mt-2 text-sm leading-6 text-[var(--muted)]">{t("responses.description")}</p> 45 44 </div> 46 45 <div className="flex flex-wrap gap-3 lg:justify-end"> 47 46 <Badge>{t("responses.submissionsCount", { count: responses.length })}</Badge> 48 - <ResponseExportActions formId={form.id} /> 49 47 <Link href={`/forms/${form.id}/edit`}> 50 48 <Button variant="secondary">{t("responses.backToBuilder")}</Button> 51 49 </Link> 50 + <ResponseExportActions formId={form.id} /> 52 51 </div> 53 52 </section> 54 53
+1 -1
app/(creator)/layout.tsx
··· 20 20 const workspaceState = await getActiveWorkspaceForUser(session.user.id); 21 21 22 22 return ( 23 - <main className="mx-auto min-h-screen w-full max-w-7xl px-6 py-8 lg:px-10"> 23 + <main className="mx-auto flex w-full max-w-7xl flex-1 flex-col px-6 py-8 lg:px-10"> 24 24 <header className="mb-8 flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-center lg:justify-between"> 25 25 <div className="flex items-center gap-4"> 26 26 <Link
+28 -10
app/error.tsx
··· 2 2 3 3 import { useEffect } from "react"; 4 4 5 + import { SiteFooter } from "@/components/site-footer"; 5 6 import { Button } from "@/components/ui/button"; 6 7 import { Card } from "@/components/ui/card"; 7 8 import { DEFAULT_LOCALE, normalizeLocale, type AppLocale } from "@/lib/i18n"; 8 9 9 - const errorMessages: Record<AppLocale, { eyebrow: string; title: string; description: string; tryAgain: string }> = { 10 + const errorMessages: Record<AppLocale, { eyebrow: string; title: string; description: string; tryAgain: string; footer: { poweredBy: string; appName: string; licensedUnder: string; sourceCode: string; reportBug: string } }> = { 10 11 en: { 11 12 eyebrow: "Something broke", 12 13 title: "The forms hit an unexpected error.", 13 14 description: "Try the action again. If it keeps happening, check your environment variables, Google OAuth setup, and local Postgres container.", 14 15 tryAgain: "Try again", 16 + footer: { 17 + poweredBy: "Built with", 18 + appName: "Lively Forms", 19 + licensedUnder: "Licensed under", 20 + sourceCode: "Source code", 21 + reportBug: "Report a bug", 22 + }, 15 23 }, 16 24 ru: { 17 25 eyebrow: "Что-то сломалось", 18 26 title: "В формах произошла непредвиденная ошибка.", 19 27 description: "Попробуйте выполнить действие ещё раз. Если это повторяется, проверьте переменные окружения, настройку Google OAuth и локальный контейнер Postgres.", 20 28 tryAgain: "Попробовать снова", 29 + footer: { 30 + poweredBy: "Сделано с помощью", 31 + appName: "Живые формы", 32 + licensedUnder: "Лицензия", 33 + sourceCode: "Исходный код", 34 + reportBug: "Сообщить об ошибке", 35 + }, 21 36 }, 22 37 }; 23 38 ··· 33 48 return ( 34 49 <html lang={locale}> 35 50 <body> 36 - <main className="mx-auto flex min-h-screen max-w-3xl items-center px-6 py-16"> 37 - <Card className="w-full px-8 py-10 text-center"> 38 - <h1 className="font-display text-4xl text-[var(--ink)]">{copy.title}</h1> 39 - <p className="mx-auto mt-3 max-w-lg text-sm leading-7 text-[var(--muted)]">{copy.description}</p> 40 - <div className="mt-6 flex justify-center"> 41 - <Button onClick={() => reset()}>{copy.tryAgain}</Button> 42 - </div> 43 - </Card> 44 - </main> 51 + <div className="flex min-h-screen flex-col"> 52 + <main className="mx-auto flex max-w-3xl flex-1 items-center px-6 py-16"> 53 + <Card className="w-full px-8 py-10 text-center"> 54 + <h1 className="font-display text-4xl text-[var(--ink)]">{copy.title}</h1> 55 + <p className="mx-auto mt-3 max-w-lg text-sm leading-7 text-[var(--muted)]">{copy.description}</p> 56 + <div className="mt-6 flex justify-center"> 57 + <Button onClick={() => reset()}>{copy.tryAgain}</Button> 58 + </div> 59 + </Card> 60 + </main> 61 + <SiteFooter copy={copy.footer} /> 62 + </div> 45 63 </body> 46 64 </html> 47 65 );
+1 -1
app/f/[slug]/page.tsx
··· 12 12 } 13 13 14 14 return ( 15 - <main className="mx-auto flex min-h-screen w-full max-w-6xl items-center justify-center px-6 py-8 lg:px-10 lg:py-10"> 15 + <main className="mx-auto flex w-full max-w-6xl flex-1 items-center justify-center px-6 py-8 lg:px-10 lg:py-10"> 16 16 <PublicFormRunner form={form} /> 17 17 </main> 18 18 );
+1 -1
app/join/[token]/page.tsx
··· 20 20 const invite = await getOrganizationInviteState(session.user.id, token); 21 21 22 22 return ( 23 - <main className="mx-auto flex min-h-screen w-full max-w-3xl items-center px-6 py-10 lg:px-10"> 23 + <main className="mx-auto flex w-full max-w-3xl flex-1 items-center px-6 py-10 lg:px-10"> 24 24 <Card className="w-full p-8 lg:p-10"> 25 25 {invite ? ( 26 26 <>
+17 -3
app/layout.tsx
··· 2 2 import { Inter, Kurale } from "next/font/google"; 3 3 4 4 import { I18nProvider } from "@/components/i18n-provider"; 5 + import { SiteFooter } from "@/components/site-footer"; 5 6 import { ThemeProvider } from "@/components/theme-provider"; 6 7 import { getRequestI18n } from "@/lib/i18n-server"; 7 8 import { themeInitScript } from "@/lib/theme"; ··· 32 33 }: Readonly<{ 33 34 children: React.ReactNode; 34 35 }>) { 35 - const { locale, messages } = await getRequestI18n(); 36 + const { locale, messages, t } = await getRequestI18n(); 36 37 37 38 return ( 38 39 <html lang={locale} suppressHydrationWarning> 39 40 <head> 40 41 <script dangerouslySetInnerHTML={{ __html: themeInitScript }} /> 41 42 </head> 42 - <body className={`${kurale.variable} ${inter.variable}`}> 43 + <body className={`${kurale.variable} ${inter.variable} min-h-screen`}> 43 44 <ThemeProvider> 44 - <I18nProvider locale={locale} messages={messages}>{children}</I18nProvider> 45 + <I18nProvider locale={locale} messages={messages}> 46 + <div className="flex min-h-screen flex-col"> 47 + <div className="flex flex-1 flex-col">{children}</div> 48 + <SiteFooter 49 + copy={{ 50 + poweredBy: t("footer.poweredBy"), 51 + appName: t("app.name"), 52 + licensedUnder: t("footer.licensedUnder"), 53 + sourceCode: t("footer.sourceCode"), 54 + reportBug: t("footer.reportBug"), 55 + }} 56 + /> 57 + </div> 58 + </I18nProvider> 45 59 </ThemeProvider> 46 60 </body> 47 61 </html>
+1 -1
app/not-found.tsx
··· 7 7 export default async function NotFound() { 8 8 const { t } = await getRequestI18n(); 9 9 return ( 10 - <main className="mx-auto flex min-h-screen max-w-3xl items-center px-6 py-16"> 10 + <main className="mx-auto flex max-w-3xl flex-1 items-center px-6 py-16"> 11 11 <Card className="w-full px-8 py-10 text-center"> 12 12 <h1 className="font-display text-4xl text-[var(--ink)]">{t("notFound.title")}</h1> 13 13 <p className="mx-auto mt-3 max-w-lg text-sm leading-7 text-[var(--muted)]">
+1 -1
app/page.tsx
··· 14 14 const appName = t("app.name"); 15 15 16 16 return ( 17 - <main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col px-6 py-8 lg:px-10 lg:py-10"> 17 + <main className="mx-auto flex w-full max-w-3xl flex-1 flex-col px-6 py-8 lg:px-10 lg:py-10"> 18 18 <section className="flex flex-1 items-center justify-center py-16 lg:py-24"> 19 19 <Card className="w-full max-w-3xl p-8 lg:p-10"> 20 20 <div className="max-w-2xl">
+114 -58
components/form-builder-panels.tsx
··· 10 10 } from "@dnd-kit/core"; 11 11 import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; 12 12 import { CSS } from "@dnd-kit/utilities"; 13 - import { Copy, GripVertical, Link as LinkIcon, LoaderCircle, MessagesSquare, Plus, Save, Settings2, Trash2, type LucideIcon } from "lucide-react"; 13 + import { ChevronDown, Copy, ExternalLink, EyeOff, Globe, GripVertical, Link as LinkIcon, LoaderCircle, MessagesSquare, Plus, Save, Settings2, Trash2, Upload, type LucideIcon } from "lucide-react"; 14 14 import Link from "next/link"; 15 + import * as React from "react"; 15 16 import type { Dispatch, SetStateAction } from "react"; 16 17 17 18 import { useI18n } from "@/components/i18n-provider"; 18 19 import { FormStatusBadge } from "@/components/form-status-badge"; 19 20 import { Button } from "@/components/ui/button"; 21 + import { Checkbox } from "@/components/ui/checkbox"; 20 22 import { Input } from "@/components/ui/input"; 23 + import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; 21 24 import { 22 25 Select, 23 26 SelectContent, ··· 75 78 form, 76 79 settingsSelected, 77 80 busy, 81 + shareHref, 78 82 onOpenSettings, 79 - onTogglePublished, 83 + onPublish, 84 + onUnpublish, 85 + onCopyShareLink, 80 86 }: { 81 87 form: BuilderForm; 82 88 settingsSelected: boolean; 83 89 busy: string | null; 90 + shareHref: string; 84 91 onOpenSettings: () => void; 85 - onTogglePublished: () => void; 92 + onPublish: () => void; 93 + onUnpublish: () => void; 94 + onCopyShareLink: () => void; 86 95 }) { 87 96 const { t } = useI18n(); 88 97 const workspaceLabel = form.workspace.kind === "personal" ? t("workspace.personal") : form.workspace.label; 98 + const [isPublicationMenuOpen, setIsPublicationMenuOpen] = React.useState(false); 99 + const isPublished = form.status === "PUBLISHED"; 100 + const isPublishBusy = busy === "publish"; 89 101 90 102 return ( 91 103 <section className="flex flex-col gap-4 border-b border-[color:var(--line)] pb-5 lg:flex-row lg:items-end lg:justify-between"> ··· 95 107 <FormStatusBadge status={form.status} /> 96 108 </div> 97 109 <p className="mt-2 text-xs font-medium uppercase text-[var(--muted)]">{workspaceLabel}</p> 98 - <p className="mt-2 text-sm leading-6 text-[var(--muted)]">{t("builder.description")}</p> 99 110 </div> 100 111 <div className="flex flex-wrap items-center gap-3 lg:justify-end"> 101 112 <Button variant={settingsSelected ? "default" : "secondary"} onClick={onOpenSettings}> ··· 108 119 {t("builder.responses", { count: form.responseCount })} 109 120 </Button> 110 121 </Link> 111 - <Button variant={form.status === "PUBLISHED" ? "secondary" : "default"} onClick={onTogglePublished}> 112 - {busy === "publish" ? <LoaderCircle className="size-4 animate-spin" /> : <LinkIcon className="size-4" />} 113 - {form.status === "PUBLISHED" ? t("builder.unpublish") : t("builder.publish")} 114 - </Button> 122 + <Popover open={isPublicationMenuOpen} onOpenChange={setIsPublicationMenuOpen}> 123 + <PopoverTrigger asChild> 124 + <Button variant={isPublished ? "default" : "secondary"} aria-haspopup="menu" aria-expanded={isPublicationMenuOpen}> 125 + {isPublishBusy ? <LoaderCircle className="size-4 animate-spin" /> : <Globe className="size-4" />} 126 + {t("builder.publicationMenu")} 127 + <ChevronDown className="size-4" /> 128 + </Button> 129 + </PopoverTrigger> 130 + <PopoverContent align="end" className="w-64 p-2"> 131 + <div className="grid gap-1"> 132 + <button 133 + type="button" 134 + className="flex w-full items-center gap-2 rounded-[12px] px-3 py-2 text-left text-sm font-medium text-[var(--ink)] transition hover:bg-[var(--accent-soft)] disabled:cursor-not-allowed disabled:text-[var(--muted)] disabled:hover:bg-transparent" 135 + disabled={isPublishBusy} 136 + onClick={() => { 137 + setIsPublicationMenuOpen(false); 138 + if (isPublished) { 139 + onUnpublish(); 140 + return; 141 + } 142 + 143 + onPublish(); 144 + }} 145 + > 146 + {isPublished ? <EyeOff className="size-4" /> : <Upload className="size-4" />} 147 + {t(isPublished ? "builder.unpublish" : "builder.publish")} 148 + </button> 149 + <div className="my-1 h-px bg-[color:var(--line)]" /> 150 + <button 151 + type="button" 152 + className="flex w-full items-center gap-2 rounded-[12px] px-3 py-2 text-left text-sm font-medium text-[var(--ink)] transition hover:bg-[var(--accent-soft)]" 153 + onClick={() => { 154 + setIsPublicationMenuOpen(false); 155 + onCopyShareLink(); 156 + }} 157 + > 158 + <Copy className="size-4" /> 159 + {t("builder.copyLink")} 160 + </button> 161 + <Link 162 + href={shareHref} 163 + target="_blank" 164 + className="flex items-center gap-2 rounded-[12px] px-3 py-2 text-sm font-medium text-[var(--ink)] transition hover:bg-[var(--accent-soft)]" 165 + onClick={() => setIsPublicationMenuOpen(false)} 166 + > 167 + <ExternalLink className="size-4" /> 168 + {t("builder.openRunner")} 169 + </Link> 170 + </div> 171 + </PopoverContent> 172 + </Popover> 115 173 </div> 116 174 </section> 117 175 ); ··· 149 207 </label> 150 208 </div> 151 209 152 - <div className="space-y-5 border-t border-[color:var(--line)] pt-6"> 153 - <div> 154 - <p className="text-xs font-semibold uppercase text-[var(--accent)]">{t("builder.afterSubmission")}</p> 155 - <p className="mt-2 text-sm leading-6 text-[var(--muted)]"> 156 - {t("builder.afterSubmissionDescription")} 157 - </p> 158 - </div> 159 - 160 - <div className="grid gap-5"> 161 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 162 - <span className="font-medium text-[var(--ink)]">{t("builder.completionTitle")}</span> 163 - <Input value={metadataDraft.completionTitle} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionTitle: event.target.value }))} /> 164 - </label> 165 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 166 - <span className="font-medium text-[var(--ink)]">{t("builder.completionMessage")}</span> 167 - <Textarea value={metadataDraft.completionMessage} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionMessage: event.target.value }))} /> 168 - </label> 169 - <div className="grid gap-5 lg:grid-cols-2"> 170 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 171 - <span className="font-medium text-[var(--ink)]">{t("builder.followUpLabel")}</span> 172 - <Input value={metadataDraft.completionLinkLabel} placeholder={t("builder.followUpLabelPlaceholder")} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkLabel: event.target.value }))} /> 173 - </label> 174 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 175 - <span className="font-medium text-[var(--ink)]">{t("builder.followUpUrl")}</span> 176 - <Input value={metadataDraft.completionLinkUrl} placeholder={t("builder.followUpUrlPlaceholder")} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkUrl: event.target.value }))} /> 177 - </label> 178 - </div> 179 - </div> 180 - </div> 181 - 182 210 <div className="grid gap-5 border-t border-[color:var(--line)] pt-6"> 183 211 <div> 184 212 <p className="text-xs font-semibold uppercase text-[var(--accent)]">{t("builder.publicRoute")}</p> ··· 209 237 </Button> 210 238 </Link> 211 239 </div> 240 + </div> 241 + </div> 242 + </div> 243 + 244 + <div className="space-y-5 border-t border-[color:var(--line)] pt-6"> 245 + <div> 246 + <p className="text-xs font-semibold uppercase text-[var(--accent)]">{t("builder.afterSubmission")}</p> 247 + <p className="mt-2 text-sm leading-6 text-[var(--muted)]"> 248 + {t("builder.afterSubmissionDescription")} 249 + </p> 250 + </div> 251 + 252 + <div className="grid gap-5"> 253 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 254 + <span className="font-medium text-[var(--ink)]">{t("builder.completionTitle")}</span> 255 + <Input value={metadataDraft.completionTitle} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionTitle: event.target.value }))} /> 256 + </label> 257 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 258 + <span className="font-medium text-[var(--ink)]">{t("builder.completionMessage")}</span> 259 + <Textarea value={metadataDraft.completionMessage} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionMessage: event.target.value }))} /> 260 + </label> 261 + <div className="grid gap-5 lg:grid-cols-2"> 262 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 263 + <span className="font-medium text-[var(--ink)]">{t("builder.followUpLabel")}</span> 264 + <Input value={metadataDraft.completionLinkLabel} placeholder={t("builder.followUpLabelPlaceholder")} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkLabel: event.target.value }))} /> 265 + </label> 266 + <label className="grid gap-2 text-sm text-[var(--muted)]"> 267 + <span className="font-medium text-[var(--ink)]">{t("builder.followUpUrl")}</span> 268 + <Input value={metadataDraft.completionLinkUrl} placeholder={t("builder.followUpUrlPlaceholder")} onChange={(event) => setMetadataDraft((current) => ({ ...current, completionLinkUrl: event.target.value }))} /> 269 + </label> 212 270 </div> 213 271 </div> 214 272 </div> ··· 590 648 </div> 591 649 592 650 <div className="grid gap-5"> 593 - <label className="grid gap-2 text-sm text-[var(--muted)]"> 594 - <span className="font-medium text-[var(--ink)]">{t(blockDraft.type === "TEXT" ? "builder.headingField" : "builder.prompt")}</span> 651 + <div className="grid gap-2 text-sm text-[var(--muted)]"> 652 + {blockDraft.type === "TEXT" ? ( 653 + <span className="font-medium text-[var(--ink)]">{t("builder.headingField")}</span> 654 + ) : ( 655 + <div className="flex flex-wrap items-center justify-between gap-3"> 656 + <span className="font-medium text-[var(--ink)]">{t("builder.prompt")}</span> 657 + <label className="inline-flex items-center gap-2 text-sm font-medium text-[var(--ink)]"> 658 + <Checkbox 659 + checked={blockDraft.required} 660 + onChange={(event) => setBlockDraft((current) => (current ? { ...current, required: event.target.checked } : current))} 661 + /> 662 + {t("builder.requiredToggle")} 663 + </label> 664 + </div> 665 + )} 595 666 <Input value={blockDraft.title} onChange={(event) => setBlockDraft((current) => (current ? { ...current, title: event.target.value } : current))} /> 596 - </label> 667 + </div> 597 668 598 669 {blockDraft.type !== "TEXT" ? ( 599 670 <label className="grid gap-2 text-sm text-[var(--muted)]"> ··· 624 695 625 696 <div className="grid gap-3 rounded-[18px] border border-[color:var(--line)] bg-[var(--bg-strong)] px-4 py-4 text-sm text-[var(--muted)]"> 626 697 <label className="flex items-center gap-3 text-[var(--ink)]"> 627 - <input 698 + <Checkbox 628 699 checked={(blockDraft.config as ShortTextBlockConfig | LongTextBlockConfig).validationRegex !== null} 629 - className="size-4 rounded border-[color:var(--line-strong)]" 630 - type="checkbox" 631 700 onChange={(event) => updateConfig({ validationRegex: event.target.checked ? "" : null })} 632 701 /> 633 702 {t("builder.regexValidation")} ··· 660 729 </label> 661 730 662 731 <label className="flex items-center gap-3 text-[var(--ink)]"> 663 - <input 732 + <Checkbox 664 733 checked={(blockDraft.config as NumberBlockConfig).allowFloat} 665 - className="size-4 rounded border-[color:var(--line-strong)]" 666 - type="checkbox" 667 734 onChange={(event) => updateConfig({ allowFloat: event.target.checked })} 668 735 /> 669 736 {t("builder.allowFloats")} ··· 826 893 </div> 827 894 ) : null} 828 895 829 - {isQuestionBlock(blockDraft.type) ? ( 830 - <label className="flex items-center gap-3 rounded-xl bg-[var(--bg-strong)] px-4 py-3 text-sm text-[var(--ink)]"> 831 - <input 832 - checked={blockDraft.required} 833 - className="size-4 rounded border-[color:var(--line-strong)]" 834 - type="checkbox" 835 - onChange={(event) => setBlockDraft((current) => (current ? { ...current, required: event.target.checked } : current))} 836 - /> 837 - {t("builder.requiredToggle")} 838 - </label> 839 - ) : null} 840 896 </div> 841 897 842 898 <div className="flex flex-wrap items-center justify-between gap-3 border-t border-[color:var(--line)] pt-6">
+11 -7
components/form-builder.tsx
··· 160 160 transition, 161 161 }} 162 162 className={cn( 163 - "group flex items-center gap-2 rounded-[14px] px-2.5 py-2", 163 + "group flex cursor-pointer items-center gap-2 rounded-[14px] px-2.5 py-2", 164 164 selected 165 165 ? "bg-[var(--surface-strong)] shadow-[var(--shadow-elevated)]" 166 166 : "hover:bg-[var(--surface)]", 167 167 )} 168 + onClick={() => onSelect(block.id)} 168 169 > 169 170 <BlockRowInner 170 171 block={block} ··· 174 175 className="relative inline-flex size-6 shrink-0 items-center justify-center rounded-full border border-[color:var(--line)] text-[var(--muted)] transition hover:bg-[var(--accent-soft)]" 175 176 type="button" 176 177 aria-label={t("builder.dragBlockAria", { label: preview })} 178 + onClick={(event) => event.stopPropagation()} 177 179 {...attributes} 178 180 {...listeners} 179 181 > ··· 200 202 return ( 201 203 <div 202 204 className={cn( 203 - "group flex items-center gap-2 rounded-[14px] px-2.5 py-2", 205 + "group flex cursor-pointer items-center gap-2 rounded-[14px] px-2.5 py-2", 204 206 selected 205 207 ? "bg-[var(--surface-strong)] shadow-[var(--shadow-elevated)]" 206 208 : "hover:bg-[var(--surface)]", 207 209 )} 210 + onClick={() => onSelect(block.id)} 208 211 > 209 212 <BlockRowInner 210 213 block={block} ··· 421 424 }); 422 425 } 423 426 424 - async function togglePublished() { 427 + async function setPublished(published: boolean) { 425 428 await withTask("publish", async () => { 426 429 const payload = await fetchJson<{ form: BuilderForm }>(`/api/forms/${form.id}/publish`, { 427 430 method: "POST", 428 - body: JSON.stringify({ 429 - published: form.status !== "PUBLISHED", 430 - }), 431 + body: JSON.stringify({ published }), 431 432 }); 432 433 433 434 setForm(payload.form); ··· 510 511 form={form} 511 512 settingsSelected={selection.kind === "form"} 512 513 busy={busy} 514 + shareHref={shareHref} 513 515 onOpenSettings={() => setSelection({ kind: "form" })} 514 - onTogglePublished={togglePublished} 516 + onPublish={() => setPublished(true)} 517 + onUnpublish={() => setPublished(false)} 518 + onCopyShareLink={copyShareLink} 515 519 /> 516 520 517 521 {branchingAnalysis.blockers.length ? (
+64
components/site-footer.tsx
··· 1 + "use client"; 2 + 3 + import Link from "next/link"; 4 + import { usePathname } from "next/navigation"; 5 + 6 + import { PROJECT_LICENSE, PROJECT_URLS } from "@/lib/project"; 7 + 8 + export type SiteFooterCopy = { 9 + poweredBy: string; 10 + appName: string; 11 + licensedUnder: string; 12 + sourceCode: string; 13 + reportBug: string; 14 + }; 15 + 16 + export function SiteFooter({ copy }: { copy: SiteFooterCopy }) { 17 + const pathname = usePathname(); 18 + const showPoweredBy = pathname?.startsWith("/f/"); 19 + 20 + return ( 21 + <footer> 22 + <div className="mx-auto flex w-full max-w-7xl flex-col gap-2 px-6 py-4 text-xs text-[var(--muted)] opacity-70 lg:flex-row lg:items-center lg:justify-between lg:px-10"> 23 + <p className="flex flex-wrap items-center gap-x-2 gap-y-1"> 24 + {showPoweredBy ? ( 25 + <> 26 + <Link href="/" className="transition hover:opacity-90"> 27 + {copy.appName} 28 + </Link> 29 + <span aria-hidden="true">·</span> 30 + </> 31 + ) : null} 32 + <span>{copy.licensedUnder}</span> 33 + <Link 34 + href={PROJECT_URLS.license} 35 + target="_blank" 36 + rel="noreferrer" 37 + className="transition hover:opacity-90" 38 + > 39 + {PROJECT_LICENSE} 40 + </Link> 41 + </p> 42 + 43 + <nav className="flex flex-wrap items-center gap-x-4 gap-y-1"> 44 + <Link 45 + href={PROJECT_URLS.source} 46 + target="_blank" 47 + rel="noreferrer" 48 + className="transition hover:opacity-90" 49 + > 50 + {copy.sourceCode} 51 + </Link> 52 + <Link 53 + href={PROJECT_URLS.issues} 54 + target="_blank" 55 + rel="noreferrer" 56 + className="transition hover:opacity-90" 57 + > 58 + {copy.reportBug} 59 + </Link> 60 + </nav> 61 + </div> 62 + </footer> 63 + ); 64 + }
+24
components/ui/checkbox.tsx
··· 1 + import * as React from "react"; 2 + import { Check } from "lucide-react"; 3 + 4 + import { cn } from "@/lib/utils"; 5 + 6 + export const Checkbox = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>( 7 + ({ className, ...props }, ref) => { 8 + return ( 9 + <span className="relative inline-flex size-5 shrink-0 items-center justify-center"> 10 + <input 11 + ref={ref} 12 + type="checkbox" 13 + className={cn( 14 + "peer size-5 cursor-pointer appearance-none rounded-[7px] border border-[color:var(--line)] bg-[var(--surface-strong)] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)] outline-none transition-all duration-200 hover:border-[color:var(--line-strong)] hover:bg-[var(--surface)] checked:border-[var(--accent)] checked:bg-[var(--accent-soft)] checked:shadow-[var(--shadow-accent)] focus-visible:ring-2 focus-visible:ring-[var(--accent-soft)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)] disabled:cursor-not-allowed disabled:opacity-50", 15 + className, 16 + )} 17 + {...props} 18 + /> 19 + <Check className="pointer-events-none absolute size-3.5 text-[var(--accent)] opacity-0 transition-opacity duration-200 peer-checked:opacity-100" /> 20 + </span> 21 + ); 22 + }, 23 + ); 24 + Checkbox.displayName = "Checkbox";
+7
lib/project.ts
··· 1 + export const PROJECT_LICENSE = "AGPL-3.0-only"; 2 + 3 + export const PROJECT_URLS = { 4 + source: "https://codeberg.org/chernigin/lively-forms", 5 + license: "https://codeberg.org/chernigin/lively-forms/src/branch/main/LICENSE", 6 + issues: "https://codeberg.org/chernigin/lively-forms/issues", 7 + } as const;
+7 -3
locales/en.yml
··· 5 5 home: 6 6 description: "Like a seed in good soil, every thoughtful question has the power to make something grow. Build forms that feel alive—gentle invitations to reflect, respond, and uncover what matters most." 7 7 openDashboard: Open dashboard 8 + footer: 9 + poweredBy: Built with 10 + licensedUnder: Licensed under 11 + sourceCode: Source code 12 + reportBug: Report a bug 8 13 auth: 9 14 continueWithGoogle: Continue with Google 10 15 workspace: ··· 108 113 updated: Updated 109 114 open: Open 110 115 builder: 111 - description: Edit blocks, update settings, and review responses. 112 116 settings: Settings 113 117 responses: "Responses ({count})" 118 + publicationMenu: Publication 114 119 publish: Publish form 115 120 unpublish: Unpublish 116 121 blocks: Blocks ··· 203 208 branchAgreementValues: 204 209 agreed: Agree 205 210 not_agreed: Do not agree 206 - requiredToggle: Mark this question as required 211 + requiredToggle: Required 207 212 deleteBlock: Delete block 208 213 saveBlock: Save block 209 214 nothingSelected: Nothing selected ··· 275 280 continue: Continue 276 281 submit: Submit 277 282 responses: 278 - description: Review anonymous submissions in form order. 279 283 submissionsCount: "{count} submissions" 280 284 backToBuilder: Back to builder 281 285 noSubmissionsEyebrow: No submissions yet
+7 -3
locales/ru.yml
··· 5 5 home: 6 6 description: "Как семя в хорошей почве, каждый продуманный вопрос может помочь чему-то вырасти. Создавайте формы, которые ощущаются живыми — мягкими приглашениями подумать, ответить и понять, что действительно важно." 7 7 openDashboard: Открыть дашборд 8 + footer: 9 + poweredBy: Сделано с помощью 10 + licensedUnder: Лицензия 11 + sourceCode: Исходный код 12 + reportBug: Сообщить об ошибке 8 13 auth: 9 14 continueWithGoogle: Продолжить с Google 10 15 workspace: ··· 108 113 updated: Обновлено 109 114 open: Открыть 110 115 builder: 111 - description: Редактируйте блоки, меняйте настройки и просматривайте ответы. 112 116 settings: Настройки 113 117 responses: "Ответы ({count})" 118 + publicationMenu: Публикация 114 119 publish: Опубликовать форму 115 120 unpublish: Снять с публикации 116 121 blocks: Блоки ··· 203 208 branchAgreementValues: 204 209 agreed: Согласен 205 210 not_agreed: Не согласен 206 - requiredToggle: Сделать этот вопрос обязательным 211 + requiredToggle: Обязательный 207 212 deleteBlock: Удалить блок 208 213 saveBlock: Сохранить блок 209 214 nothingSelected: Ничего не выбрано ··· 275 280 continue: Далее 276 281 submit: Отправить 277 282 responses: 278 - description: Просматривайте анонимные ответы в порядке формы. 279 283 submissionsCount: "{count} ответов" 280 284 backToBuilder: Назад к редактору 281 285 noSubmissionsEyebrow: Пока нет ответов
+11 -1
package.json
··· 1 1 { 2 - "name": "the-forms", 2 + "name": "lively-forms", 3 3 "version": "0.1.0", 4 4 "private": true, 5 + "description": "A conversational form builder inspired by Typeform.", 5 6 "license": "AGPL-3.0-only", 7 + "author": "Michael Chernigin <michaelchernigin@gmail.com>", 8 + "homepage": "https://codeberg.org/chernigin/lively-forms", 9 + "bugs": { 10 + "url": "https://codeberg.org/chernigin/lively-forms/issues" 11 + }, 12 + "repository": { 13 + "type": "git", 14 + "url": "git+https://codeberg.org/chernigin/lively-forms.git" 15 + }, 6 16 "scripts": { 7 17 "dev": "next dev", 8 18 "build": "node scripts/validate-translations.mjs && next build",