Public Access
feat(profile): let users design their public profile
This commit is contained in:
1 parent
a5458b3f86
commit
b55fcfec92
20 files changed
+674
-138
No files matched your search
+23
-29
@@ -23,10 +23,12 @@ import {settingsSchema} from '../utils/validation.js';
|
||||
import {usePreferences} from '../context/PreferencesContext.jsx';
|
||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||
import useSaveAppearance from '../hooks/useSaveAppearance.js';
|
||||
import {ACCENTS, CORNERS} from '../utils/appearance.js';
|
||||
import {AccentPicker, CornersPicker} from '../components/StylePickers.jsx';
|
||||
import ProfileStyleSettings from '../components/ProfileStyleSettings.jsx';
|
||||
import {normalizeProfileStyle} from '../utils/profileStyle.js';
|
||||
import useSaveProfileStyle from '../hooks/useSaveProfileStyle.js';
|
||||
import {coverUrl} from '../utils/games.js';
|
||||
import {saveProfileStats} from '../utils/stats.js';
|
||||
import {cn} from '@/lib/utils';
|
||||
import {apiFetch, refreshSession} from '../utils/api.js';
|
||||
import {hasSession, markSession} from '../utils/auth.js';
|
||||
|
||||
@@ -59,11 +61,13 @@ export default function SettingsPage() {
|
||||
const expireSession = useSessionExpired();
|
||||
const {isDarkMode, setDarkMode, setLanguage, appearance} = usePreferences();
|
||||
const saveAppearance = useSaveAppearance();
|
||||
const saveStyle = useSaveProfileStyle();
|
||||
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState(null);
|
||||
const [savingProfileStats, setSavingProfileStats] = useState(false);
|
||||
const [savingProfileStyle, setSavingProfileStyle] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
// Read once: removing the parameter below must not load the profile again.
|
||||
const [steamOutcome] = useState(() => searchParams.get('steam'));
|
||||
@@ -227,6 +231,15 @@ export default function SettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileStyleChange = async (changes) => {
|
||||
const previous = normalizeProfileStyle(profile?.settings?.profileStyle);
|
||||
const showProfileStyle = (next) => setProfile((p) => ({...p, settings: {...p.settings, profileStyle: next}}));
|
||||
showProfileStyle({...previous, ...changes});
|
||||
setSavingProfileStyle(true);
|
||||
showProfileStyle(await saveStyle(changes, previous) ?? previous);
|
||||
setSavingProfileStyle(false);
|
||||
};
|
||||
|
||||
const copyProfileLink = async (link) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(link);
|
||||
@@ -330,33 +343,8 @@ export default function SettingsPage() {
|
||||
<CardTitle className="flex items-center gap-2"><Palette className="size-4 text-primary"/>{t('SETTINGS.APPEARANCE')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Label id="accent-label">{t('SETTINGS.ACCENT')}</Label>
|
||||
<div role="group" aria-labelledby="accent-label" className="flex flex-wrap gap-2">
|
||||
{ACCENTS.map((accent) => (
|
||||
<button key={accent} type="button" data-accent={accent}
|
||||
aria-label={t(`SETTINGS.ACCENT_OPTIONS.${accent.toUpperCase()}`)}
|
||||
title={t(`SETTINGS.ACCENT_OPTIONS.${accent.toUpperCase()}`)}
|
||||
aria-pressed={appearance.accent === accent}
|
||||
onClick={() => saveAppearance({accent})}
|
||||
className={cn('size-8 rounded-full bg-primary ring-offset-2 ring-offset-background transition hover:scale-110',
|
||||
appearance.accent === accent && 'ring-2 ring-foreground')}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Label id="corners-label">{t('SETTINGS.CORNERS')}</Label>
|
||||
<div role="group" aria-labelledby="corners-label" className="flex gap-2">
|
||||
{CORNERS.map((corners) => (
|
||||
<Button key={corners} type="button" size="sm" data-corners={corners}
|
||||
variant={appearance.corners === corners ? 'default' : 'outline'}
|
||||
aria-pressed={appearance.corners === corners}
|
||||
onClick={() => saveAppearance({corners})}>
|
||||
{t(`SETTINGS.CORNER_OPTIONS.${corners.toUpperCase()}`)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AccentPicker id="accent" value={appearance.accent} onChange={(accent) => saveAppearance({accent})}/>
|
||||
<CornersPicker id="corners" value={appearance.corners} onChange={(corners) => saveAppearance({corners})}/>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="wideLayout">{t('SETTINGS.WIDE_LAYOUT')}</Label>
|
||||
@@ -463,6 +451,12 @@ export default function SettingsPage() {
|
||||
disabled={!publicProfile} saving={savingProfileStats}
|
||||
onChange={handleProfileStatsChange}/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t pt-6">
|
||||
<ProfileStyleSettings style={normalizeProfileStyle(profile?.settings?.profileStyle)}
|
||||
disabled={!publicProfile || savingProfileStyle}
|
||||
onChange={handleProfileStyleChange}/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in new issue
Block a user