authentication and calendar-sync passwords keyed (214 -> 163)
Authentication reaches zero -- sign-in, bootstrap, forgot and reset password. These are the first words a new account reads, which is why the brief ranked them above the larger screens. DavAppPasswords had the third toLocaleDateString(undefined, ...) in the shell; it now uses useFormat().date like the other two. DebugPanel's three labels are keyed too. Worth knowing rather than acting on: that panel is rendered behind showDebug, which Background.tsx hardcodes to false, so it never appears. Keyed rather than deleted because removing it is not my call.
This commit is contained in:
+34
-23
@@ -7,6 +7,8 @@ import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { copyToClipboard } from 'helpers/clipboard';
|
||||
import { useFormat } from 'i18n';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
// Per-device credentials for calendar and contacts sync (DAVx5, iOS, macOS, Thunderbird).
|
||||
//
|
||||
@@ -26,19 +28,21 @@ type DavPassword = {
|
||||
|
||||
type Minted = { password: string; username: string; label: string };
|
||||
|
||||
const formatDate = (value: string | null) =>
|
||||
value ? new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : null;
|
||||
|
||||
const copy = async (text: string, what: string) => {
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
toast.success(`${what} copied`);
|
||||
} catch {
|
||||
toast.error('Could not copy — select and copy manually');
|
||||
}
|
||||
};
|
||||
// Dates come from useFormat() below, bound to the app's locale rather than the browser's (D7).
|
||||
|
||||
export const DavAppPasswords = () => {
|
||||
const { t } = useTranslation();
|
||||
const { date: formatDate } = useFormat();
|
||||
|
||||
const copy = async (text: string, what: string) => {
|
||||
try {
|
||||
await copyToClipboard(text);
|
||||
toast.success(t('settings.dav.copied', { what }));
|
||||
} catch {
|
||||
toast.error(t('settings.dav.copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const client = useClient();
|
||||
const [passwords, setPasswords] = useState<DavPassword[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -71,7 +75,7 @@ export const DavAppPasswords = () => {
|
||||
setLabel('');
|
||||
load();
|
||||
} catch {
|
||||
toast.error('Could not create the app password');
|
||||
toast.error(t('settings.dav.createFailed'));
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
@@ -80,10 +84,10 @@ export const DavAppPasswords = () => {
|
||||
const revoke = async (id: number) => {
|
||||
try {
|
||||
await client.post(`/dav/passwords/${id}/revoke`, {});
|
||||
toast.success('Revoked — that device will stop syncing');
|
||||
toast.success(t('settings.dav.revoked'));
|
||||
load();
|
||||
} catch {
|
||||
toast.error('Could not revoke');
|
||||
toast.error(t('settings.dav.revokeFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +96,7 @@ export const DavAppPasswords = () => {
|
||||
await client.delete(`/dav/passwords/${id}`);
|
||||
load();
|
||||
} catch {
|
||||
toast.error('Could not delete');
|
||||
toast.error(t('settings.dav.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,7 +130,12 @@ export const DavAppPasswords = () => {
|
||||
<code className="min-w-0 flex-1 truncate rounded bg-background/70 px-2 py-1.5 font-mono text-xs">
|
||||
{value}
|
||||
</code>
|
||||
<Button variant="ghost" size="sm" onClick={() => copy(value, field)} title={`Copy ${field}`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copy(value, field)}
|
||||
title={t('settings.dav.copyField', { field })}
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -146,29 +155,31 @@ export const DavAppPasswords = () => {
|
||||
|
||||
<form onSubmit={create} className="flex items-end gap-2">
|
||||
<Label className="grid flex-1 gap-2">
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70">New device</span>
|
||||
<span className="text-duck-dark/70 dark:text-foreground/70">{t('settings.dav.newDevice')}</span>
|
||||
<Input
|
||||
className="h-11 bg-background/60 border-duck-dark/20"
|
||||
value={label}
|
||||
onChange={(ev) => setLabel(ev.target.value)}
|
||||
placeholder="Pixel 9, iPad, Thunderbird…"
|
||||
placeholder={t('settings.dav.placeholder')}
|
||||
/>
|
||||
</Label>
|
||||
<Button type="submit" disabled={!label.trim() || isCreating} className="h-11">
|
||||
<Plus className="mr-1.5 h-4 w-4" />
|
||||
Create
|
||||
{t('settings.dav.create')}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{passwords.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-duck-dark/15 dark:border-foreground/15 p-6 text-center text-sm text-duck-dark/40 dark:text-foreground/40">
|
||||
No devices yet.
|
||||
{t('settings.dav.none')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-2">
|
||||
{passwords.map((entry) => {
|
||||
const revoked = !!entry.revokedAt;
|
||||
const lastUsed = formatDate(entry.lastUsedAt);
|
||||
const lastUsed = entry.lastUsedAt
|
||||
? formatDate(entry.lastUsedAt, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
@@ -192,7 +203,7 @@ export const DavAppPasswords = () => {
|
||||
</div>
|
||||
</div>
|
||||
{!revoked && (
|
||||
<Button variant="ghost" size="sm" onClick={() => revoke(entry.id)} title="Revoke">
|
||||
<Button variant="ghost" size="sm" onClick={() => revoke(entry.id)} title={t('settings.dav.revoke')}>
|
||||
<Ban className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -200,7 +211,7 @@ export const DavAppPasswords = () => {
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => remove(entry.id)}
|
||||
title="Delete"
|
||||
title={t('settings.dav.delete')}
|
||||
className="hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
|
||||
Reference in New Issue
Block a user