feat(setup): final community/thank-you step (#732); fix create-admin button overflow (#730)

#730 — the account step's Create-admin button shared a flex row with Back;
its label + loading spinner exceeded the card width, so the button
overflowed the card outline while submitting (and was fragile for longer
i18n labels). Stack both buttons full-width — the primary always has room
for the spinner now, matching every other wizard step.

#732 — add a final 'community' step, shown once on first-run after
config / no-config, before entering the app. Mission line + four link
cards (report a bug, request a feature, star/share, Buy Me a Coffee),
all target=_blank rel=noopener, and a Finish → Dashboard button. Fully
i18n (en + de). Restore keeps its reload flow (a restored instance is no
longer first-run, so it never reaches this step). Adds .github/FUNDING.yml
so GitHub renders a Sponsor button too.

Verified live: drove the real first-run wizard end to end — stacked
account buttons render inside the card, community step shows the mission
+ all four links, Finish lands on the dashboard.
This commit is contained in:
Paul Nothaft
2026-07-03 12:29:58 +02:00
parent c76877c0c4
commit dadaaeea77
4 changed files with 102 additions and 11 deletions
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
buy_me_a_coffee: theluap
+13
View File
@@ -3572,6 +3572,19 @@
"finish": "Einrichtung abschließen",
"saveFailed": "Einige Einstellungen konnten nicht gespeichert werden — Sie können sie in den Einstellungen abschließen."
},
"community": {
"subtitle": "Alles bereit",
"mission": "PicPeak gibt es, damit Fotografinnen und Fotografen ihre Galerien und Kundendaten selbst besitzen — auf dem eigenen Server, ohne monatliche SaaS-Gebühren. Danke, dass du es ausprobierst.",
"bugTitle": "Fehler gefunden?",
"bugDesc": "Melde ihn und wir sehen ihn uns an.",
"featureTitle": "Wunschfunktion?",
"featureDesc": "Sag uns, was PicPeak für dich besser machen würde.",
"starTitle": "Gefällt es dir? Erzähl davon",
"starDesc": "Empfiehl es anderen Fotografen, teile es und gib dem Projekt einen Stern auf GitHub.",
"supportTitle": "Entwicklung unterstützen",
"supportDesc": "Optional — spendier mir einen Kaffee, um PicPeak am Leben zu halten.",
"finish": "Fertig → Dashboard"
},
"stepOf": "Schritt {{current}} von {{total}}",
"continue": "Weiter",
"back": "Zurück",
+13
View File
@@ -3468,6 +3468,19 @@
"finish": "Finish setup",
"saveFailed": "Some settings could not be saved — you can finish them in Settings."
},
"community": {
"subtitle": "You're all set",
"mission": "PicPeak exists so photographers can own their galleries and client data — on their own server, without monthly SaaS fees. Thanks for giving it a try.",
"bugTitle": "Found a bug?",
"bugDesc": "Open an issue and we'll take a look.",
"featureTitle": "Want a feature?",
"featureDesc": "Tell us what would make PicPeak better for you.",
"starTitle": "Like it? Spread the word",
"starDesc": "Tell other photographers, share it, and star the project on GitHub.",
"supportTitle": "Support development",
"supportDesc": "Optional — buy me a coffee to help keep PicPeak going.",
"finish": "Finish → Dashboard"
},
"stepOf": "Step {{current}} of {{total}}",
"continue": "Continue",
"back": "Back",
+72 -11
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Navigate, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink } from 'lucide-react';
import { Key, Mail, Lock, Eye, EyeOff, AlertCircle, ArrowLeft, ArrowRight, Copy, Check, ExternalLink, Bug, Lightbulb, Star, Coffee } from 'lucide-react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
@@ -19,6 +19,20 @@ import type { AdminUser } from '../types';
const SETUP_DOCS_URL =
'https://github.com/PicPeak/picpeak/blob/main/README.md#first-run--create-your-admin-account';
// Final "own your data" thank-you step (#732). Link cards shown once, on
// first-run only, before entering the app. Every link is optional and opens
// in a new tab; nothing here makes an external call.
const COMMUNITY_LINKS: {
key: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
}[] = [
{ key: 'bug', href: 'https://github.com/PicPeak/picpeak/issues/new?template=bug_report.md', icon: Bug },
{ key: 'feature', href: 'https://github.com/PicPeak/picpeak/issues/new?template=feature_request.md', icon: Lightbulb },
{ key: 'star', href: 'https://github.com/PicPeak/picpeak', icon: Star },
{ key: 'support', href: 'https://www.buymeacoffee.com/theluap', icon: Coffee },
];
// "How will you use PicPeak?" — the opt-in feature groups shown after the admin
// account is created. galleries/analytics/userManagement are always on and not
// listed. Labels/descriptions reuse the existing Settings→Features i18n keys
@@ -53,7 +67,7 @@ export const SetupPage: React.FC = () => {
staleTime: Infinity,
});
const [step, setStep] = useState<'token' | 'account' | 'usage' | 'restore' | 'config'>('token');
const [step, setStep] = useState<'token' | 'account' | 'usage' | 'restore' | 'config' | 'community'>('token');
const [form, setForm] = useState({ token: '', email: '', password: '', confirm: '' });
const [showPassword, setShowPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -234,8 +248,9 @@ export const SetupPage: React.FC = () => {
selectedFeatures.has('reminderEmails') ||
selectedFeatures.has('incomingMail') ||
selectedFeatures.has('whatsapp');
if (needsConfig) setStep('config');
else navigate('/admin/dashboard', { replace: true });
// Both the config branch and the no-config path end on the final
// community/thank-you step (#732), whose Finish button enters the app.
setStep(needsConfig ? 'config' : 'community');
}
};
@@ -267,7 +282,9 @@ export const SetupPage: React.FC = () => {
? t('setup.restoreStepSubtitle')
: step === 'config'
? t('setup.config.subtitle')
: t('setup.usageSubtitle')}
: step === 'community'
? t('setup.community.subtitle')
: t('setup.usageSubtitle')}
</p>
{(step === 'token' || step === 'account' || step === 'usage') && (
<p className="mt-3 text-xs font-medium tracking-wide uppercase" style={{ color: '#171717', opacity: 0.5 }}>
@@ -397,7 +414,15 @@ export const SetupPage: React.FC = () => {
/>
</div>
<div className="flex gap-3">
{/* Stacked full-width (not a side-by-side flex row): the primary
label + loading spinner exceeded the card width when squeezed
next to Back, so the button overflowed the card outline while
submitting (#730). Full-width also keeps longer translations
(e.g. German) inside the button. Matches every other step. */}
<div className="space-y-3">
<Button type="submit" variant="primary" size="lg" isLoading={isSubmitting} className="w-full">
{t('setup.submit')}
</Button>
<Button
type="button"
variant="outline"
@@ -405,12 +430,10 @@ export const SetupPage: React.FC = () => {
onClick={() => { toast.dismiss(); setErrors({}); setStep('token'); }}
disabled={isSubmitting}
leftIcon={<ArrowLeft className="w-4 h-4" />}
className="w-full"
>
{t('setup.back')}
</Button>
<Button type="submit" variant="primary" size="lg" isLoading={isSubmitting} className="flex-1">
{t('setup.submit')}
</Button>
</div>
</form>
) : step === 'usage' ? (
@@ -486,11 +509,49 @@ export const SetupPage: React.FC = () => {
{t('setup.back')}
</Button>
</div>
) : (
) : step === 'config' ? (
<SetupConfigStep
selectedFeatures={selectedFeatures}
onDone={() => navigate('/admin/dashboard', { replace: true })}
onDone={() => setStep('community')}
/>
) : (
<div className="space-y-6">
<p className="text-sm text-neutral-700">{t('setup.community.mission')}</p>
<div className="space-y-3">
{COMMUNITY_LINKS.map(({ key, href, icon: Icon }) => (
<a
key={key}
href={href}
target="_blank"
rel="noopener noreferrer"
className="flex items-start gap-3 rounded-lg border border-neutral-200 p-3 hover:bg-neutral-50 transition-colors"
>
<Icon className="w-5 h-5 flex-shrink-0 mt-0.5" style={{ color: 'var(--color-primary, #5C8762)' }} />
<span className="min-w-0">
<span className="block text-sm font-medium text-neutral-800">
{t(`setup.community.${key}Title`)}
</span>
<span className="block text-xs text-neutral-500">
{t(`setup.community.${key}Desc`)}
</span>
</span>
<ExternalLink className="w-4 h-4 flex-shrink-0 text-neutral-400 self-center" aria-hidden="true" />
</a>
))}
</div>
<Button
type="button"
variant="primary"
size="lg"
className="w-full"
onClick={() => navigate('/admin/dashboard', { replace: true })}
rightIcon={<ArrowRight className="w-4 h-4" />}
>
{t('setup.community.finish')}
</Button>
</div>
)}
</Card>
</div>