forked from Tadrisino/MathApp_frontend
One static splash: app icon + name in index.html from the first frame, no companion, no timers
The first second was a blank page, then a splash with the selected companion, then the home screen. Now index.html carries the splash (icon from the Android launcher, name, white to match the native window background) and SplashScreen draws the same markup, so the hand-over from HTML to React shows no change. Not run on a device. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
16
index.html
16
index.html
@@ -6,13 +6,25 @@
|
||||
<meta name="theme-color" content="#ED6956" />
|
||||
<meta name="description" content="تدریسینو — یادگیریِ ریاضیِ پایهی سوم تا دوازدهم، قدمبهقدم و با دستیارِ هوشمند." />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>تدریسینو</title>
|
||||
<!-- تنظیماتِ زمانِ اجرا (آدرسِ API)؛ پیش از خودِ اپ بار میشود. -->
|
||||
<script src="/config.js"></script>
|
||||
<!-- اسپلشِ ثابت: همینجا در HTML است تا از اولین فریم دیده شود (نه صفحهی سفید تا بار شدنِ JS)؛
|
||||
SplashScreen در React عینِ همین را میکشد، پس هنگامِ تحویل چیزی نمیپرد. فونتِ سیستم، چون
|
||||
Vazirmatn بعد از CSS میرسد و عوضشدنِ فونت خودش یک پرش است. -->
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="preload" as="image" href="/app-icon.png" />
|
||||
<style>
|
||||
.boot-splash { position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; gap: 20px; background: #fff; }
|
||||
.boot-splash img { width: 144px; height: 144px; display: block; }
|
||||
.boot-splash span { font: 900 34px/1.2 system-ui, -apple-system, sans-serif; color: #1a1a2e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root">
|
||||
<div class="boot-splash" dir="rtl"><img src="/app-icon.png" alt="" width="144" height="144" /><span>تدریسینو</span></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
public/app-icon.png
Normal file
BIN
public/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
|
||||
import { api, ApiError, http, type Grade, type UserProfile } from '@/api';
|
||||
import type { Prefetch } from '@/state/AppData';
|
||||
@@ -24,22 +24,11 @@ type Flow =
|
||||
| { kind: 'main'; profile: UserProfile; grades: Grade[]; prefetch?: Prefetch };
|
||||
|
||||
/**
|
||||
* اسپلش فقط وقتی نشان داده میشود که سرور دیرتر از SPLASH_DELAY_MS جواب بدهد؛ اگر زودتر رسید، کاربر
|
||||
* اسپلش را نمیبیند و مستقیم به صفحهی بعدی میرود (بهجای چند صد میلیثانیه چشمک).
|
||||
* اگر یک بار نشان داده شد، دستکم SPLASH_MIN_MS میماند تا خودش چشمک نزند.
|
||||
* تا آن لحظه فقط پسزمینهی همرنگِ اسپلش دیده میشود، نه صفحهی سفید.
|
||||
*/
|
||||
const SPLASH_DELAY_MS = 350;
|
||||
const SPLASH_MIN_MS = 700;
|
||||
|
||||
/**
|
||||
* جریانِ ورود: اسپلش → (معرفی، فقط بارِ اول) → ورود → تکمیلِ پروفایل → اپ.
|
||||
* جریانِ ورود: اسپلشِ ثابت (تا بررسیِ نشست) → (معرفی، فقط بارِ اول) → ورود → تکمیلِ پروفایل → اپ.
|
||||
* خودِ اپ (تبها و صفحهها) در MainApp است.
|
||||
*/
|
||||
export function App() {
|
||||
const [flow, setFlow] = useState<Flow>({ kind: 'boot' });
|
||||
const [splashVisible, setSplashVisible] = useState(false);
|
||||
const splashShownAt = useRef<number | null>(null);
|
||||
|
||||
/** پروفایل و پایهها را میگیرد و تصمیم میگیرد کاربر کجا برود. */
|
||||
const enter = useCallback(async () => {
|
||||
@@ -53,12 +42,6 @@ export function App() {
|
||||
|
||||
const boot = useCallback(async () => {
|
||||
setFlow({ kind: 'boot' });
|
||||
splashShownAt.current = null;
|
||||
setSplashVisible(false);
|
||||
const timer = setTimeout(() => {
|
||||
splashShownAt.current = Date.now();
|
||||
setSplashVisible(true);
|
||||
}, SPLASH_DELAY_MS);
|
||||
let next: Flow | null = null;
|
||||
if (api.auth.isSignedIn()) {
|
||||
try {
|
||||
@@ -72,13 +55,6 @@ export function App() {
|
||||
} else {
|
||||
next = store.get(KEYS.onboarded) ? { kind: 'login' } : { kind: 'onboarding' };
|
||||
}
|
||||
clearTimeout(timer);
|
||||
// اسپلش دیده شده؟ فقط در آن صورت تا کمینهی زمانش نگه دار.
|
||||
const shownAt = splashShownAt.current;
|
||||
if (shownAt !== null) {
|
||||
const wait = SPLASH_MIN_MS - (Date.now() - shownAt);
|
||||
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
||||
}
|
||||
if (next) setFlow(next);
|
||||
}, [enter]);
|
||||
|
||||
@@ -110,17 +86,17 @@ export function App() {
|
||||
<MotionConfig reducedMotion="user">
|
||||
<AppShell>
|
||||
<ToastProvider>
|
||||
<AnimatePresence mode="wait">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={flow.kind}
|
||||
className="flex h-full flex-col"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
// اگر اسپلش هرگز دیده نشد، محوشدن هم لازم نیست (وگرنه همان چشمک از راهِ دیگر برمیگردد).
|
||||
transition={{ duration: splashVisible || flow.kind !== 'boot' ? 0.2 : 0 }}
|
||||
// اسپلش بیمحو: با عوضشدنِ صفحه فوراً میرود و صفحهی بعد خودش محو میشود.
|
||||
transition={{ duration: flow.kind === 'boot' ? 0 : 0.2 }}
|
||||
>
|
||||
{flow.kind === 'boot' && <SplashScreen showContent={splashVisible} />}
|
||||
{flow.kind === 'boot' && <SplashScreen />}
|
||||
{flow.kind === 'bootError' && (
|
||||
<div className="flex h-full flex-col justify-center">
|
||||
<ErrorState error={flow.error} onRetry={() => void boot()} />
|
||||
|
||||
@@ -1,45 +1,13 @@
|
||||
import { motion } from 'motion/react';
|
||||
import { SeyarMascot } from '@/mascots/SeyarMascot';
|
||||
import { BrandMark } from '@/components/BrandMark';
|
||||
|
||||
/** نمادهای ریاضی که آرام از پایینِ صفحه بالا میروند؛ فقط CSS (transform/opacity)، بیبارِ اضافه. */
|
||||
const GLYPHS = [
|
||||
{ c: 'π', x: 12, d: 0 }, { c: '+', x: 28, d: 2.6 }, { c: '√', x: 46, d: 1.2 },
|
||||
{ c: '÷', x: 64, d: 3.8 }, { c: '∑', x: 80, d: 0.6 }, { c: '×', x: 90, d: 4.6 },
|
||||
];
|
||||
|
||||
/**
|
||||
* تا وقتی نشستِ قبلی بررسی میشود نمایش داده میشود. `showContent=false` فقط پسزمینه است؛ App
|
||||
* محتوا را فقط وقتی روشن میکند که پاسخِ سرور دیر کرده باشد.
|
||||
* اسپلشِ ثابت: آیکونِ اپ و نام، بیانیمیشن و بیهمراه (همراهِ انتخابی هنوز معلوم نیست و تکرارش
|
||||
* بد است). عینِ `.boot-splash` در index.html است تا تحویل از HTML به React دیده نشود؛ استایلش
|
||||
* هم آنجا است چون باید پیش از رسیدنِ CSS و JS کار کند.
|
||||
*/
|
||||
export function SplashScreen({ showContent }: { showContent: boolean }) {
|
||||
if (!showContent) return <div className="h-full bg-rose" />;
|
||||
export function SplashScreen() {
|
||||
return (
|
||||
<div className="relative flex h-full flex-col items-center justify-center overflow-hidden bg-rose">
|
||||
<div aria-hidden="true" className="pointer-events-none absolute inset-x-0 bottom-0 h-1/2">
|
||||
{GLYPHS.map((g) => (
|
||||
<span key={g.c} className="rise absolute bottom-0 text-3xl font-black text-white/60" style={{ left: `${g.x}%`, animationDelay: `${g.d}s` }}>
|
||||
{g.c}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 220, damping: 18 }}
|
||||
className="relative flex flex-col items-center gap-4"
|
||||
>
|
||||
<div className="rounded-[32px] bg-white/15 p-6 backdrop-blur-sm">
|
||||
<SeyarMascot char="sigma" mood="happy" size={130} />
|
||||
</div>
|
||||
<BrandMark size={44} />
|
||||
<p className="text-sm text-white/80">ریاضی، قدمبهقدم، از روی کتابِ درسی</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1.8, repeat: Infinity, ease: 'linear' }}
|
||||
className="absolute bottom-20 h-6 w-6 rounded-full border-2 border-white/25 border-t-white"
|
||||
/>
|
||||
<div className="boot-splash" dir="rtl">
|
||||
<img src="/app-icon.png" alt="" width={144} height={144} />
|
||||
<span>تدریسینو</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,8 +117,6 @@
|
||||
.shine { animation: shine 3.2s ease-in-out infinite; will-change: transform; }
|
||||
@keyframes spark { 0%, 100% { opacity: .25; transform: scale(.7); } 50% { opacity: 1; transform: scale(1); } }
|
||||
.spark { animation: spark 2.4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
|
||||
@keyframes rise { 0% { transform: translateY(16px); opacity: 0; } 15%, 85% { opacity: .55; } 100% { transform: translateY(-110px); opacity: 0; } }
|
||||
.rise { animation: rise 7s linear infinite; will-change: transform, opacity; }
|
||||
|
||||
/* نوارِ انتظارِ میانِ دو صفحه (MainApp → HoldBar). */
|
||||
@keyframes hold-bar { 0% { transform: translateX(120%); } 100% { transform: translateX(-320%); } }
|
||||
|
||||
Reference in New Issue
Block a user