parents screen: tell the child and the parent apart

A linked child (demo user Ali) was told "you are linked to Ali" and the
screen asked for the parent-only weekly report, which 404s. GET /family/link
does not say which side the caller is on, so the role now comes from the
weekly report: it answers only for the parent. The child sees a line saying
their parent follows their progress and no invite/accept forms. Found on the
live site with the demo accounts. deploy.sh now prints the full API URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 01:26:22 +03:30
parent 0ca7b39e49
commit 33e539e562
2 changed files with 31 additions and 20 deletions

View File

@@ -45,6 +45,6 @@ step "جابه‌جایی"
rm -rf dist.prev rm -rf dist.prev
[ -d dist ] && mv dist dist.prev [ -d dist ] && mv dist dist.prev
mv dist.next dist mv dist.next dist
API="$(grep -o 'https\?://[a-zA-Z0-9.-]*/api' dist/assets/*.js | head -1 | cut -d: -f2-)" API="$(grep -oh 'https\?://[a-zA-Z0-9.-]*/api' dist/assets/*.js | head -1)"
printf ' منتشر شد. آدرسِ API در build: %s\n' "${API:-(هم‌دامنه /api یا config.js)}" printf ' منتشر شد. آدرسِ API در build: %s\n' "${API:-(هم‌دامنه /api یا config.js)}"
printf ' کشِ آروان را برای index.html و config.js پاک کنید اگر تغییری دیده نشد.\n' printf ' کشِ آروان را برای index.html و config.js پاک کنید اگر تغییری دیده نشد.\n'

View File

@@ -1,9 +1,9 @@
import { useState } from 'react'; import { useState } from 'react';
import { BookOpen, Flame, Link2, Star, Trophy, UserPlus } from 'lucide-react'; import { BookOpen, Flame, Link2, Star, Trophy, UserPlus } from 'lucide-react';
import { api, ApiError, type FamilyLink } from '@/api'; import { api, ApiError, type FamilyLink, type FamilyWeeklyReport } from '@/api';
import { useAsync } from '@/lib/useAsync'; import { useAsync } from '@/lib/useAsync';
import { useToast } from '@/components/Toast'; import { useToast } from '@/components/Toast';
import { InlineError, LoadingState, PrimaryButton, Screen, ScreenBody, ScreenHeader } from '@/components/ui'; import { ErrorState, InlineError, LoadingState, PrimaryButton, Screen, ScreenBody, ScreenHeader } from '@/components/ui';
import { friendlyError } from '@/lib/errors'; import { friendlyError } from '@/lib/errors';
import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format'; import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format';
@@ -13,26 +13,37 @@ import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format';
* گزارشِ هفتگی را می‌بیند. * گزارشِ هفتگی را می‌بیند.
*/ */
export function FamilyScreen() { export function FamilyScreen() {
const link = useAsync( // GET /family/link نمی‌گوید کاربر در این اتصال والد است یا فرزند؛ گزارشِ
() => api.family.link().catch((err) => { // هفتگی فقط برای والد جواب می‌دهد و برای فرزند ۴۰۴ است، پس نقش از همان درمی‌آید.
const state = useAsync(async () => {
const link = await api.family.link().catch((err) => {
if (err instanceof ApiError && err.status === 404) return null; if (err instanceof ApiError && err.status === 404) return null;
throw err; throw err;
}), });
[], if (!link || link.status !== 'accepted') return { link, role: null, report: null };
); try {
return { link, role: 'parent' as const, report: await api.family.weeklyReport() };
} catch (err) {
if (err instanceof ApiError && (err.status === 404 || err.status === 403)) return { link, role: 'child' as const, report: null };
throw err;
}
}, []);
const data = state.data;
return ( return (
<Screen> <Screen>
<ScreenHeader title="بخشِ والدین" subtitle="پیشرفتِ فرزندت رو هر هفته ببین" color="#FF7A3D" back mascot={{ char: 'sigma', mood: 'happy', size: 56 }} /> <ScreenHeader title="بخشِ والدین" subtitle="پیشرفتِ فرزندت رو هر هفته ببین" color="#FF7A3D" back mascot={{ char: 'sigma', mood: 'happy', size: 56 }} />
<ScreenBody className="flex flex-col gap-4"> <ScreenBody className="flex flex-col gap-4">
{link.loading ? ( {state.loading ? (
<LoadingState /> <LoadingState />
) : state.error ? (
<ErrorState error={state.error} onRetry={state.reload} />
) : ( ) : (
<> <>
{link.data && <LinkStatus link={link.data} />} {data?.link && <LinkStatus link={data.link} role={data.role} />}
{link.data?.status === 'accepted' && <WeeklyReport />} {data?.report && <WeeklyReport report={data.report} />}
<InviteChild onDone={link.reload} /> {data?.role !== 'child' && <InviteChild onDone={state.reload} />}
<AcceptInvite onDone={link.reload} /> {!data?.role && <AcceptInvite onDone={state.reload} />}
</> </>
)} )}
</ScreenBody> </ScreenBody>
@@ -40,23 +51,23 @@ export function FamilyScreen() {
); );
} }
function LinkStatus({ link }: { link: FamilyLink }) { function LinkStatus({ link, role }: { link: FamilyLink; role: 'parent' | 'child' | null }) {
const accepted = link.status === 'accepted'; const accepted = link.status === 'accepted';
return ( return (
<div className={`flex items-center gap-3 rounded-3xl p-4 ${accepted ? 'bg-[#EAF8F0]' : 'bg-[#FFF8E1]'}`}> <div className={`flex items-center gap-3 rounded-3xl p-4 ${accepted ? 'bg-[#EAF8F0]' : 'bg-[#FFF8E1]'}`}>
<Link2 size={20} className={accepted ? 'text-mint' : 'text-gold'} /> <Link2 size={20} className={accepted ? 'text-mint' : 'text-gold'} />
<p className="text-sm leading-7 text-ink"> <p className="text-sm leading-7 text-ink">
{accepted ? <>به <b>{link.childName || 'فرزندت'}</b> وصلی.</> : 'دعوت فرستاده شده و منتظرِ تأییدِ فرزندته.'} {!accepted
? 'دعوت فرستاده شده و منتظرِ تأییدِ فرزندته.'
: role === 'child'
? 'حسابت به پدر یا مادرت وصله؛ اون‌ها هر هفته پیشرفتت رو می‌بینن.'
: <>به <b>{link.childName || 'فرزندت'}</b> وصلی.</>}
</p> </p>
</div> </div>
); );
} }
function WeeklyReport() { function WeeklyReport({ report: r }: { report: FamilyWeeklyReport }) {
const report = useAsync(() => api.family.weeklyReport(), []);
if (report.loading) return <div className="h-32 animate-pulse rounded-3xl bg-soft" />;
if (!report.data) return null;
const r = report.data;
return ( return (
<section className="rounded-3xl border border-line bg-white p-4"> <section className="rounded-3xl border border-line bg-white p-4">
<h2 className="mb-3 text-sm font-black text-ink">گزارشِ این هفته <span className="text-xs font-medium text-muted">(از {formatJalaliDate(r.weekStart)})</span></h2> <h2 className="mb-3 text-sm font-black text-ink">گزارشِ این هفته <span className="text-xs font-medium text-muted">(از {formatJalaliDate(r.weekStart)})</span></h2>