Ilo/design/Ilo Auth.html
2026-05-02 20:01:30 +03:30

626 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ایلو · ورود و ثبت‌نام</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css" />
<style>
body { background: var(--bg); }
.auth-shell {
min-height: 100vh;
display: grid;
grid-template-columns: 1fr 1fr;
}
.auth-side {
background: linear-gradient(150deg, oklch(0.94 0.04 40), oklch(0.96 0.02 60));
position: relative;
overflow: hidden;
padding: 48px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.auth-side::before {
content: "";
position: absolute;
inset: -20% -10% auto auto;
width: 480px; height: 480px;
border-radius: 50%;
background: radial-gradient(circle, oklch(0.78 0.10 40 / 0.4), transparent 70%);
filter: blur(40px);
}
.auth-side::after {
content: "";
position: absolute;
inset: auto auto -20% -10%;
width: 380px; height: 380px;
border-radius: 50%;
background: radial-gradient(circle, oklch(0.78 0.08 80 / 0.4), transparent 70%);
filter: blur(40px);
}
.auth-side > * { position: relative; z-index: 1; }
.brand-row {
display: flex; align-items: center; gap: 12px;
}
.brand-mark {
width: 44px; height: 44px;
border-radius: 13px;
background: linear-gradient(140deg, var(--accent), var(--accent-2));
display: grid; place-items: center;
color: white;
font-weight: 700;
font-family: var(--font-en);
font-size: 18px;
letter-spacing: -0.02em;
box-shadow: 0 4px 16px oklch(0.72 0.14 40 / 0.3);
}
.auth-pitch {
max-width: 480px;
}
.auth-pitch h1 {
font-size: 40px;
line-height: 1.25;
letter-spacing: -0.02em;
font-weight: 700;
margin: 0 0 16px;
}
.auth-pitch h1 em {
font-style: normal;
background: linear-gradient(120deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.auth-pitch p {
color: var(--ink-2);
font-size: 16px;
line-height: 1.7;
margin: 0 0 24px;
}
.auth-bullets {
display: flex; flex-direction: column; gap: 14px;
}
.auth-bullet {
display: flex; align-items: center; gap: 12px;
font-size: 14px;
color: var(--ink-2);
}
.auth-bullet .b-ic {
width: 28px; height: 28px;
border-radius: 8px;
background: white;
color: var(--accent);
display: grid; place-items: center;
box-shadow: 0 2px 6px rgba(0,0,0,.06);
flex-shrink: 0;
}
.auth-foot {
display: flex; gap: 24px;
color: var(--ink-3);
font-size: 12.5px;
}
.auth-main {
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
background: var(--surface);
}
.auth-card {
width: 100%;
max-width: 420px;
display: flex; flex-direction: column;
}
.auth-stepper {
display: flex; align-items: center; gap: 8px;
margin-bottom: 32px;
}
.step-dot {
width: 26px; height: 26px;
border-radius: 50%;
display: grid; place-items: center;
font-size: 12px; font-weight: 600;
background: var(--surface-2);
color: var(--ink-3);
border: 1px solid var(--line);
}
.step-dot.active {
background: var(--accent);
color: white;
border-color: transparent;
}
.step-dot.done {
background: var(--accent-soft);
color: var(--accent-ink);
border-color: transparent;
}
.step-line {
flex: 1; height: 2px;
background: var(--line);
border-radius: 999px;
}
.step-line.done { background: var(--accent); }
.auth-h1 {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.015em;
margin: 0 0 8px;
}
.auth-sub {
color: var(--ink-3);
font-size: 14px;
margin: 0 0 28px;
}
.field-stack { display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px; }
.field-lbl {
font-size: 12.5px;
font-weight: 500;
color: var(--ink-2);
margin-bottom: 6px;
display: block;
}
.phone-inp {
display: flex;
border: 1px solid var(--line);
background: var(--surface);
border-radius: var(--r-sm);
overflow: hidden;
transition: border-color .15s, box-shadow .15s;
}
.phone-inp:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.phone-cc {
padding: 12px 14px;
background: var(--surface-2);
border-inline-end: 1px solid var(--line);
display: flex; align-items: center; gap: 8px;
color: var(--ink-2);
font-family: var(--font-num);
font-weight: 600;
font-size: 14px;
}
.phone-inp input {
flex: 1;
border: 0; outline: 0; background: transparent;
padding: 12px 14px;
font: inherit; font-size: 16px;
letter-spacing: 0.5px;
color: var(--ink);
direction: ltr;
text-align: start;
font-family: var(--font-num);
}
.role-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 24px;
}
.role-card {
border: 1.5px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
padding: 20px;
cursor: pointer;
transition: all .15s;
text-align: start;
display: flex; flex-direction: column; gap: 12px;
}
.role-card:hover {
border-color: var(--ink-4);
background: var(--surface-2);
}
.role-card.selected {
border-color: var(--accent);
background: var(--accent-soft);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.role-ic {
width: 36px; height: 36px;
border-radius: 10px;
background: color-mix(in oklch, var(--accent) 18%, transparent);
color: var(--accent);
display: grid; place-items: center;
}
.role-card.selected .role-ic {
background: var(--accent);
color: white;
}
.role-card h3 { margin: 0; font-size: 15px; font-weight: 600; }
.role-card p { margin: 0; font-size: 12px; color: var(--ink-3); line-height: 1.55; }
.channel-row {
display: flex; gap: 8px;
padding: 4px;
background: var(--surface-2);
border-radius: var(--r-sm);
margin-bottom: 24px;
}
.channel-btn {
flex: 1;
appearance: none;
border: 0;
background: transparent;
padding: 10px;
border-radius: 6px;
font: inherit;
font-size: 13px;
font-weight: 500;
color: var(--ink-3);
cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.channel-btn.active {
background: var(--surface);
color: var(--ink);
box-shadow: 0 1px 2px rgba(0,0,0,.06);
}
.otp-row {
display: flex; gap: 8px;
direction: ltr;
margin-bottom: 8px;
}
.otp-cell {
flex: 1 1 0;
min-width: 0;
width: 0;
height: 64px;
border: 1.5px solid var(--line);
background: var(--surface);
border-radius: var(--r-sm);
font: inherit;
font-size: 22px;
font-weight: 700;
text-align: center;
color: var(--ink);
outline: none;
transition: all .15s;
font-family: var(--font-num);
}
.otp-cell:focus,
.otp-cell.filled {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.resend-row {
display: flex; align-items: center; justify-content: space-between;
font-size: 12.5px;
color: var(--ink-3);
margin-bottom: 24px;
}
.resend-row b { color: var(--ink); font-weight: 600; font-family: var(--font-num); }
.resend-btn {
appearance: none; border: 0; background: 0;
color: var(--accent);
font: inherit; font-weight: 600; font-size: 13px;
cursor: pointer;
}
.resend-btn:disabled { color: var(--ink-4); cursor: default; }
.btn.full { width: 100%; justify-content: center; padding: 12px 16px; font-size: 14px; font-weight: 600; }
.legal {
font-size: 11.5px;
color: var(--ink-3);
text-align: center;
margin-top: 24px;
line-height: 1.6;
}
.legal a { color: var(--ink-2); text-decoration: underline; }
.switch-row {
text-align: center;
font-size: 13px;
color: var(--ink-3);
margin-top: 28px;
}
.switch-row a {
color: var(--accent);
text-decoration: none;
font-weight: 600;
cursor: pointer;
}
.success-icon {
width: 64px; height: 64px;
border-radius: 50%;
background: oklch(0.94 0.05 155);
color: oklch(0.55 0.14 155);
display: grid; place-items: center;
margin: 0 auto 24px;
}
@media (max-width: 900px) {
.auth-shell { grid-template-columns: 1fr; }
.auth-side { display: none; }
}
</style>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="icons.jsx"></script>
<script type="text/babel">
function Stepper({ step, total }) {
return (
<div className="auth-stepper">
{Array.from({ length: total }).map((_, i) => (
<React.Fragment key={i}>
<div className={`step-dot ${i === step ? 'active' : i < step ? 'done' : ''}`}>
{i < step ? <Icon name="check" className="ic-sm" /> : String(i+1).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d])}
</div>
{i < total - 1 && <div className={`step-line ${i < step ? 'done' : ''}`} />}
</React.Fragment>
))}
</div>
);
}
// OTP digit input row
function OtpInput({ value, onChange }) {
const refs = React.useRef([]);
const handle = (i, v) => {
const digit = v.replace(/\D/g, '').slice(-1);
const next = (value + '').padEnd(5, ' ').split('');
next[i] = digit || ' ';
const joined = next.join('').replace(/ /g, '');
onChange(joined);
if (digit && i < 4) refs.current[i + 1]?.focus();
};
const handleKey = (i, e) => {
if (e.key === 'Backspace' && !value[i] && i > 0) refs.current[i - 1]?.focus();
};
const handlePaste = (e) => {
const text = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, 5);
if (text) { e.preventDefault(); onChange(text); refs.current[Math.min(text.length, 4)]?.focus(); }
};
return (
<div className="otp-row" onPaste={handlePaste}>
{[0,1,2,3,4].map(i => (
<input key={i} ref={el => refs.current[i] = el}
className={`otp-cell ${value[i] ? 'filled' : ''}`}
inputMode="numeric" maxLength={1}
value={value[i] || ''}
onChange={e => handle(i, e.target.value)}
onKeyDown={e => handleKey(i, e)} />
))}
</div>
);
}
function App() {
const [step, setStep] = React.useState(0); // 0=role, 1=phone, 2=otp, 3=done
const [mode, setMode] = React.useState('signup'); // signup | signin
const [role, setRole] = React.useState('instructor');
const [channel, setChannel] = React.useState('sms');
const [phone, setPhone] = React.useState('');
const [otp, setOtp] = React.useState('');
const [resendIn, setResendIn] = React.useState(0);
React.useEffect(() => {
if (resendIn > 0) {
const t = setTimeout(() => setResendIn(resendIn - 1), 1000);
return () => clearTimeout(t);
}
}, [resendIn]);
const phoneOk = phone.length === 10 && phone.startsWith('9');
const otpOk = otp.length === 5;
const sendOtp = () => {
setOtp('');
setResendIn(60);
setStep(2);
};
const fa = (s) => String(s).replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d]);
// — Step 0: welcome / role —
const StepRole = (
<>
<h1 className="auth-h1">{mode === 'signup' ? 'به ایلو خوش اومدی 👋' : 'دوباره خوش اومدی'}</h1>
<p className="auth-sub">
{mode === 'signup' ? 'برای شروع، نقش خودت رو انتخاب کن.' : 'وارد حساب کاربری‌ات شو.'}
</p>
{mode === 'signup' && (
<div className="role-grid">
<button className={`role-card ${role === 'instructor' ? 'selected' : ''}`}
onClick={() => setRole('instructor')}>
<div className="role-ic"><Icon name="trophy" /></div>
<h3>مدرس هستم</h3>
<p>میخوام دوره بسازم و بفروشم</p>
</button>
<button className={`role-card ${role === 'student' ? 'selected' : ''}`}
onClick={() => setRole('student')}>
<div className="role-ic"><Icon name="book" /></div>
<h3>دانشجو هستم</h3>
<p>میخوام چیز جدیدی یاد بگیرم</p>
</button>
</div>
)}
<button className="btn primary full" onClick={() => setStep(1)}>
ادامه
<Icon name="chevron" className="ic-sm flipx" />
</button>
<div className="switch-row">
{mode === 'signup' ? 'قبلاً ثبت‌نام کردی؟ ' : 'حساب نداری؟ '}
<a onClick={() => setMode(mode === 'signup' ? 'signin' : 'signup')}>
{mode === 'signup' ? 'وارد شو' : 'ثبت‌نام کن'}
</a>
</div>
</>
);
// — Step 1: phone —
const StepPhone = (
<>
<h1 className="auth-h1">شماره موبایلت رو وارد کن</h1>
<p className="auth-sub">
یک کد ۵ رقمی برای تأیید برات ارسال میکنیم.
</p>
<div className="channel-row">
<button className={`channel-btn ${channel === 'sms' ? 'active' : ''}`}
onClick={() => setChannel('sms')}>
<Icon name="phone" className="ic-sm" />
پیامک
</button>
<button className={`channel-btn ${channel === 'bale' ? 'active' : ''}`}
onClick={() => setChannel('bale')}>
<Icon name="send" className="ic-sm" />
بله
</button>
</div>
<div className="field-stack">
<div>
<label className="field-lbl">شماره موبایل</label>
<div className="phone-inp">
<div className="phone-cc">
<span>🇮🇷</span>
<span>+۹۸</span>
</div>
<input value={phone} onChange={e => setPhone(e.target.value.replace(/\D/g,'').slice(0,10))}
placeholder="9XX XXX XXXX" inputMode="numeric" autoFocus />
</div>
</div>
</div>
<button className="btn primary full" disabled={!phoneOk}
style={{ opacity: phoneOk ? 1 : 0.5, cursor: phoneOk ? 'pointer' : 'not-allowed' }}
onClick={sendOtp}>
ارسال کد تأیید
<Icon name="chevron" className="ic-sm flipx" />
</button>
<div className="legal">
با ادامه دادن، با <a>قوانین استفاده</a> و <a>سیاست حریم خصوصی</a> ایلو موافقت میکنی.
</div>
</>
);
// — Step 2: OTP —
const StepOtp = (
<>
<h1 className="auth-h1">کد تأیید</h1>
<p className="auth-sub">
کد ۵ رقمی به {channel === 'sms' ? 'پیامک' : 'پیام بله'} شماره <b className="num" style={{ direction: 'ltr', display: 'inline-block' }}>+۹۸ {fa(phone)}</b> ارسال شد.
</p>
<OtpInput value={otp} onChange={setOtp} />
<div className="resend-row" style={{ marginTop: 16 }}>
{resendIn > 0
? <span>ارسال مجدد در <b>{fa(resendIn)} ثانیه</b></span>
: <span>کد رو دریافت نکردی؟</span>}
<button className="resend-btn" disabled={resendIn > 0} onClick={() => setResendIn(60)}>
ارسال مجدد
</button>
</div>
<button className="btn primary full" disabled={!otpOk}
style={{ opacity: otpOk ? 1 : 0.5, cursor: otpOk ? 'pointer' : 'not-allowed' }}
onClick={() => setStep(3)}>
تأیید و ورود
<Icon name="check" className="ic-sm" />
</button>
<div className="switch-row">
<a onClick={() => setStep(1)}> تغییر شماره موبایل</a>
</div>
</>
);
// — Step 3: done —
const StepDone = (
<div style={{ textAlign: 'center' }}>
<div className="success-icon">
<Icon name="check" style={{ width: 32, height: 32, strokeWidth: 2.5 }} />
</div>
<h1 className="auth-h1">حساب با موفقیت ساخته شد</h1>
<p className="auth-sub" style={{ marginBottom: 32 }}>
{role === 'instructor'
? 'حالا می‌تونی اولین دوره‌ات رو بسازی و دانشجوها رو دعوت کنی.'
: 'به دنیای ایلو خوش اومدی! بیا یه دوره عالی پیدا کنیم.'}
</p>
<a className="btn primary full"
href={role === 'instructor' ? 'Ilo Instructor Panel.html' : 'Ilo Student.html'}
style={{ textDecoration: 'none' }}>
{role === 'instructor' ? 'ورود به پنل مدرس' : 'مشاهده‌ی دوره‌ها'}
<Icon name="chevron" className="ic-sm flipx" />
</a>
</div>
);
return (
<div className="auth-shell">
<aside className="auth-side">
<div className="brand-row">
<div className="brand-mark">il</div>
<div>
<div style={{ fontSize: 18, fontWeight: 700 }}>ایلو</div>
<div style={{ fontSize: 11.5, color: 'var(--ink-3)' }} className="en">platform for teachers</div>
</div>
</div>
<div className="auth-pitch">
<h1>پلتفرمی برای <em>مدرسها</em>،<br/>که میخوان حرفهایتر یاد بدن.</h1>
<p>محتوات رو آپلود کن، مسیر یادگیری بساز، با دانشجوها در ارتباط باش و درآمدت رو رشد بده همه در یکجا.</p>
<div className="auth-bullets">
<div className="auth-bullet"><div className="b-ic"><Icon name="upload" className="ic-sm" /></div>آپلود راحت ویدیو، PDF و فایل صوتی</div>
<div className="auth-bullet"><div className="b-ic"><Icon name="users" className="ic-sm" /></div>مدیریت دانشجوها و پیگیری پیشرفت</div>
<div className="auth-bullet"><div className="b-ic"><Icon name="store" className="ic-sm" /></div>سایت اختصاصی با نام و برند خودت</div>
<div className="auth-bullet"><div className="b-ic"><Icon name="chart" className="ic-sm" /></div>تحلیل درآمد و رفتار دانشجویان</div>
</div>
</div>
<div className="auth-foot">
<span>© ۱۴۰۳ ایلو</span>
<span>·</span>
<span>پشتیبانی: <span className="num" dir="ltr">۰۲۱-۹۱۰۰۰۰۰۰</span></span>
</div>
</aside>
<main className="auth-main">
<div className="auth-card">
{step < 3 && mode === 'signup' && <Stepper step={step} total={3} />}
{step < 3 && mode === 'signin' && <Stepper step={Math.max(0, step - 1)} total={2} />}
{step === 0 && StepRole}
{step === 1 && StepPhone}
{step === 2 && StepOtp}
{step === 3 && StepDone}
</div>
</main>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>