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
[ -d dist ] && mv dist dist.prev
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 ' کشِ آروان را برای index.html و config.js پاک کنید اگر تغییری دیده نشد.\n'

View File

@@ -1,9 +1,9 @@
import { useState } from '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 { 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 { digitsOnly, formatJalaliDate, toFa } from '@/lib/format';
@@ -13,26 +13,37 @@ import { digitsOnly, formatJalaliDate, toFa } from '@/lib/format';
* گزارشِ هفتگی را می‌بیند.
*/
export function FamilyScreen() {
const link = useAsync(
() => api.family.link().catch((err) => {
// GET /family/link نمی‌گوید کاربر در این اتصال والد است یا فرزند؛ گزارشِ
// هفتگی فقط برای والد جواب می‌دهد و برای فرزند ۴۰۴ است، پس نقش از همان درمی‌آید.
const state = useAsync(async () => {
const link = await api.family.link().catch((err) => {
if (err instanceof ApiError && err.status === 404) return null;
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 (
<Screen>
<ScreenHeader title="بخشِ والدین" subtitle="پیشرفتِ فرزندت رو هر هفته ببین" color="#FF7A3D" back mascot={{ char: 'sigma', mood: 'happy', size: 56 }} />
<ScreenBody className="flex flex-col gap-4">
{link.loading ? (
{state.loading ? (
<LoadingState />
) : state.error ? (
<ErrorState error={state.error} onRetry={state.reload} />
) : (
<>
{link.data && <LinkStatus link={link.data} />}
{link.data?.status === 'accepted' && <WeeklyReport />}
<InviteChild onDone={link.reload} />
<AcceptInvite onDone={link.reload} />
{data?.link && <LinkStatus link={data.link} role={data.role} />}
{data?.report && <WeeklyReport report={data.report} />}
{data?.role !== 'child' && <InviteChild onDone={state.reload} />}
{!data?.role && <AcceptInvite onDone={state.reload} />}
</>
)}
</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';
return (
<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'} />
<p className="text-sm leading-7 text-ink">
{accepted ? <>به <b>{link.childName || 'فرزندت'}</b> وصلی.</> : 'دعوت فرستاده شده و منتظرِ تأییدِ فرزندته.'}
{!accepted
? 'دعوت فرستاده شده و منتظرِ تأییدِ فرزندته.'
: role === 'child'
? 'حسابت به پدر یا مادرت وصله؛ اون‌ها هر هفته پیشرفتت رو می‌بینن.'
: <>به <b>{link.childName || 'فرزندت'}</b> وصلی.</>}
</p>
</div>
);
}
function WeeklyReport() {
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;
function WeeklyReport({ report: r }: { report: FamilyWeeklyReport }) {
return (
<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>