Add newlyRegisteredToken state to NewsStore and implement account number display and copy functionality in the registration flow. Update UI to show success message upon registration and allow users to copy their account number to clipboard.

This commit is contained in:
2025-12-27 20:32:06 -06:00
parent 2c6bee84b4
commit a626a7cb33
3 changed files with 131 additions and 51 deletions

View File

@@ -53,6 +53,7 @@ class NewsStore {
lastStatusCheck = $state<number>(Date.now());
authInfo = $state<{ required: boolean; mode: string; canReg: boolean } | null>(null);
isAuthenticated = $state(false);
newlyRegisteredToken = $state<string | null>(null);
isWails = typeof window !== 'undefined' && ((window as any).runtime || (window as any).go);
isCapacitor = typeof window !== 'undefined' && Capacitor.isNativePlatform();
@@ -185,7 +186,7 @@ class NewsStore {
const response = await fetch(`${apiBase}/auth/register`, { method: 'POST' });
if (!response.ok) throw new Error('Registration failed');
const data = await response.json();
await this.login(data.accountNumber);
this.newlyRegisteredToken = data.accountNumber;
return data.accountNumber;
} catch {
toast.error('Could not generate account');

View File

@@ -20,6 +20,8 @@
X,
Hash,
ChevronRight,
Copy,
Check,
} from 'lucide-svelte';
import { db } from '$lib/db';
import { toast } from '$lib/toast.svelte';
@@ -115,6 +117,18 @@
let isResizing = $state(false);
let paneWidth = $state(newsStore.settings.paneWidth || 40);
let showDismissHatch = $state(false);
let copied = $state(false);
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
copied = true;
setTimeout(() => (copied = false), 2000);
toast.success('Account number copied to clipboard');
} catch {
toast.error('Failed to copy');
}
}
function startResizing(e: MouseEvent) {
e.preventDefault();
@@ -280,62 +294,118 @@
<div
class="card p-8 w-full max-w-md space-y-8 animate-in fade-in slide-in-from-bottom-8 duration-500"
>
<div class="text-center space-y-2">
<div
class="w-16 h-16 bg-accent-blue rounded-2xl flex items-center justify-center text-white mx-auto shadow-xl shadow-accent-blue/20"
>
<Zap size={32} fill="currentColor" />
</div>
<h1 class="text-3xl font-bold tracking-tight pt-4">Welcome to Web News</h1>
<p class="text-text-secondary">Please enter your account number to continue</p>
</div>
{#if newsStore.newlyRegisteredToken}
<div class="text-center space-y-4">
<div
class="w-16 h-16 bg-green-500 rounded-2xl flex items-center justify-center text-white mx-auto shadow-xl shadow-green-500/20"
>
<Check size={32} />
</div>
<h1 class="text-3xl font-bold tracking-tight pt-4">Account Created!</h1>
<p class="text-text-secondary text-sm">
Save your account number now. You will need it to log back in. <br />
<strong class="text-red-500">We cannot recover this for you.</strong>
</p>
<div class="space-y-4">
<div class="space-y-2">
<label for="token" class="text-sm font-medium">Account Number</label>
<div class="relative">
<Hash
class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
size={18}
/>
<input
id="token"
type="text"
placeholder="1234-5678-abcd-efgh"
class="w-full bg-bg-secondary border border-border-color rounded-xl py-3 pl-10 pr-4 focus:ring-2 focus:ring-accent-blue/20 outline-none transition-all font-mono"
bind:value={loginToken}
onkeydown={(e) => e.key === 'Enter' && newsStore.login(loginToken)}
/>
<div class="relative group">
<button
type="button"
class="w-full bg-accent-blue/5 border-2 border-accent-blue/20 rounded-xl p-4 font-mono text-lg text-accent-blue break-all text-center select-all cursor-pointer group-hover:border-accent-blue/40 transition-all"
onclick={() => copyToClipboard(newsStore.newlyRegisteredToken!)}
>
{newsStore.newlyRegisteredToken}
</button>
<button
type="button"
class="absolute -top-3 -right-3 bg-accent-blue text-white p-2 rounded-lg shadow-lg hover:scale-110 transition-transform"
onclick={() => copyToClipboard(newsStore.newlyRegisteredToken!)}
>
{#if copied}
<Check size={16} />
{:else}
<Copy size={16} />
{/if}
</button>
</div>
<div class="pt-4 space-y-3">
<button
class="w-full btn-primary py-4 rounded-xl text-lg font-bold shadow-xl shadow-accent-blue/20 flex items-center justify-center gap-2"
onclick={() => {
const token = newsStore.newlyRegisteredToken;
newsStore.newlyRegisteredToken = null;
newsStore.login(token!);
}}
>
I've saved it, let's go!
<ChevronRight size={20} />
</button>
<button
class="w-full text-xs text-text-secondary hover:underline"
onclick={() => (newsStore.newlyRegisteredToken = null)}
>
Go back
</button>
</div>
</div>
{:else}
<div class="text-center space-y-2">
<div
class="w-16 h-16 bg-accent-blue rounded-2xl flex items-center justify-center text-white mx-auto shadow-xl shadow-accent-blue/20"
>
<Zap size={32} fill="currentColor" />
</div>
<h1 class="text-3xl font-bold tracking-tight pt-4">Welcome to Web News</h1>
<p class="text-text-secondary">Please enter your account number to continue</p>
</div>
<button
class="w-full btn-primary py-4 rounded-xl text-lg font-bold shadow-xl shadow-accent-blue/20 flex items-center justify-center gap-2"
onclick={() => newsStore.login(loginToken)}
disabled={!loginToken}
>
Access Dashboard
<ChevronRight size={20} />
</button>
{#if newsStore.authInfo?.canReg}
<div class="relative py-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-border-color"></div>
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-bg-primary px-2 text-text-secondary">Or</span>
<div class="space-y-4">
<div class="space-y-2">
<label for="token" class="text-sm font-medium">Account Number</label>
<div class="relative">
<Hash
class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"
size={18}
/>
<input
id="token"
type="text"
placeholder="1234-5678-abcd-efgh"
class="w-full bg-bg-secondary border border-border-color rounded-xl py-3 pl-10 pr-4 focus:ring-2 focus:ring-accent-blue/20 outline-none transition-all font-mono"
bind:value={loginToken}
onkeydown={(e) => e.key === 'Enter' && newsStore.login(loginToken)}
/>
</div>
</div>
<button
class="w-full py-4 border border-border-color rounded-xl font-semibold hover:bg-bg-secondary transition-all flex items-center justify-center gap-2"
onclick={() => newsStore.registerAuth()}
class="w-full btn-primary py-4 rounded-xl text-lg font-bold shadow-xl shadow-accent-blue/20 flex items-center justify-center gap-2"
onclick={() => newsStore.login(loginToken)}
disabled={!loginToken}
>
Generate New Account Number
Access Dashboard
<ChevronRight size={20} />
</button>
{/if}
</div>
{#if newsStore.authInfo?.canReg}
<div class="relative py-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-border-color"></div>
</div>
<div class="relative flex justify-center text-xs uppercase">
<span class="bg-bg-primary px-2 text-text-secondary">Or</span>
</div>
</div>
<button
class="w-full py-4 border border-border-color rounded-xl font-semibold hover:bg-bg-secondary transition-all flex items-center justify-center gap-2"
onclick={() => newsStore.registerAuth()}
>
Generate New Account Number
</button>
{/if}
</div>
{/if}
<p class="text-[10px] text-text-secondary text-center leading-relaxed">
Web News is privacy-focused. We don't use emails or passwords. <br />

View File

@@ -64,12 +64,18 @@
<svelte:head>
<title>{article ? article.title : 'Shared Article on Webnews'}</title>
<meta name="description" content={article ? article.description : 'A shared article on Webnews RSS reader'} />
<meta
name="description"
content={article ? article.description : 'A shared article on Webnews RSS reader'}
/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="article" />
<meta property="og:title" content={article ? article.title : 'Shared Article on Webnews'} />
<meta property="og:description" content={article ? article.description : 'A shared article on Webnews RSS reader'} />
<meta
property="og:description"
content={article ? article.description : 'A shared article on Webnews RSS reader'}
/>
{#if article?.imageUrl}
<meta property="og:image" content={article.imageUrl} />
{:else}
@@ -79,7 +85,10 @@
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content={article ? article.title : 'Shared Article on Webnews'} />
<meta property="twitter:description" content={article ? article.description : 'A shared article on Webnews RSS reader'} />
<meta
property="twitter:description"
content={article ? article.description : 'A shared article on Webnews RSS reader'}
/>
{#if article?.imageUrl}
<meta property="twitter:image" content={article.imageUrl} />
{:else}