Merge pull request #741 from PicPeak/feat/setup-final-step-and-button-fix
feat(setup): final community step (#732) + fix create-admin button overflow (#730)
This commit is contained in:
@@ -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
|
||||||
@@ -3572,6 +3572,19 @@
|
|||||||
"finish": "Einrichtung abschließen",
|
"finish": "Einrichtung abschließen",
|
||||||
"saveFailed": "Einige Einstellungen konnten nicht gespeichert werden — Sie können sie in den Einstellungen 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}}",
|
"stepOf": "Schritt {{current}} von {{total}}",
|
||||||
"continue": "Weiter",
|
"continue": "Weiter",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
|
|||||||
@@ -3468,6 +3468,19 @@
|
|||||||
"finish": "Finish setup",
|
"finish": "Finish setup",
|
||||||
"saveFailed": "Some settings could not be saved — you can finish them in Settings."
|
"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}}",
|
"stepOf": "Step {{current}} of {{total}}",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
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 { toast } from 'react-toastify';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -19,6 +19,20 @@ import type { AdminUser } from '../types';
|
|||||||
const SETUP_DOCS_URL =
|
const SETUP_DOCS_URL =
|
||||||
'https://github.com/PicPeak/picpeak/blob/main/README.md#first-run--create-your-admin-account';
|
'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
|
// "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
|
// account is created. galleries/analytics/userManagement are always on and not
|
||||||
// listed. Labels/descriptions reuse the existing Settings→Features i18n keys
|
// listed. Labels/descriptions reuse the existing Settings→Features i18n keys
|
||||||
@@ -53,7 +67,7 @@ export const SetupPage: React.FC = () => {
|
|||||||
staleTime: Infinity,
|
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 [form, setForm] = useState({ token: '', email: '', password: '', confirm: '' });
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -234,8 +248,9 @@ export const SetupPage: React.FC = () => {
|
|||||||
selectedFeatures.has('reminderEmails') ||
|
selectedFeatures.has('reminderEmails') ||
|
||||||
selectedFeatures.has('incomingMail') ||
|
selectedFeatures.has('incomingMail') ||
|
||||||
selectedFeatures.has('whatsapp');
|
selectedFeatures.has('whatsapp');
|
||||||
if (needsConfig) setStep('config');
|
// Both the config branch and the no-config path end on the final
|
||||||
else navigate('/admin/dashboard', { replace: true });
|
// community/thank-you step (#732), whose Finish button enters the app.
|
||||||
|
setStep(needsConfig ? 'config' : 'community');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -267,6 +282,8 @@ export const SetupPage: React.FC = () => {
|
|||||||
? t('setup.restoreStepSubtitle')
|
? t('setup.restoreStepSubtitle')
|
||||||
: step === 'config'
|
: step === 'config'
|
||||||
? t('setup.config.subtitle')
|
? t('setup.config.subtitle')
|
||||||
|
: step === 'community'
|
||||||
|
? t('setup.community.subtitle')
|
||||||
: t('setup.usageSubtitle')}
|
: t('setup.usageSubtitle')}
|
||||||
</p>
|
</p>
|
||||||
{(step === 'token' || step === 'account' || step === 'usage') && (
|
{(step === 'token' || step === 'account' || step === 'usage') && (
|
||||||
@@ -397,7 +414,15 @@ export const SetupPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -405,12 +430,10 @@ export const SetupPage: React.FC = () => {
|
|||||||
onClick={() => { toast.dismiss(); setErrors({}); setStep('token'); }}
|
onClick={() => { toast.dismiss(); setErrors({}); setStep('token'); }}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
leftIcon={<ArrowLeft className="w-4 h-4" />}
|
leftIcon={<ArrowLeft className="w-4 h-4" />}
|
||||||
|
className="w-full"
|
||||||
>
|
>
|
||||||
{t('setup.back')}
|
{t('setup.back')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" variant="primary" size="lg" isLoading={isSubmitting} className="flex-1">
|
|
||||||
{t('setup.submit')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : step === 'usage' ? (
|
) : step === 'usage' ? (
|
||||||
@@ -486,11 +509,49 @@ export const SetupPage: React.FC = () => {
|
|||||||
{t('setup.back')}
|
{t('setup.back')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : step === 'config' ? (
|
||||||
<SetupConfigStep
|
<SetupConfigStep
|
||||||
selectedFeatures={selectedFeatures}
|
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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user