build: 55712324-398e-4334-a254-fc30c84b2e47
This commit is contained in:
+195
@@ -0,0 +1,195 @@
|
||||
.welcome-content {
|
||||
--background: #f8fafc;
|
||||
}
|
||||
|
||||
.background-shapes {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.shape-1 {
|
||||
position: absolute;
|
||||
top: -15%;
|
||||
right: -20%;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #ec4899 100%);
|
||||
filter: blur(100px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.shape-2 {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
left: -20%;
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2dd4bf 100%);
|
||||
filter: blur(100px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
padding-top: calc(var(--ion-safe-area-top) + 60px);
|
||||
padding-bottom: calc(var(--ion-safe-area-bottom) + 32px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
animation: fadeUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: #0284c7;
|
||||
border-radius: 24px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 44px;
|
||||
line-height: 1.15;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
margin: 0;
|
||||
letter-spacing: -1.5px;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #0284c7 0%, #059669 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
color: #475569;
|
||||
margin: 0;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.subject-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.subject-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px 6px 6px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.subject-pill:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.subject-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.icon-math {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
.icon-physics {
|
||||
background: #ffedd5;
|
||||
color: #ea580c;
|
||||
}
|
||||
.icon-bio {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
.icon-chem {
|
||||
background: #fef08a;
|
||||
color: #ca8a04;
|
||||
}
|
||||
|
||||
.subject-name {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
animation: fadeUp 0.8s ease-out 0.2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
--background: #0f172a;
|
||||
--background-hover: #1e293b;
|
||||
--background-activated: #1e293b;
|
||||
--border-radius: 20px;
|
||||
--box-shadow: 0 10px 25px rgba(15, 23, 42, 0.2);
|
||||
margin: 0;
|
||||
height: 60px;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
--color: #64748b;
|
||||
margin: 0;
|
||||
height: 56px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IonApp,
|
||||
IonContent,
|
||||
IonPage,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
setupIonicReact,
|
||||
} from '@ionic/react';
|
||||
import {
|
||||
star,
|
||||
chevronForward,
|
||||
calculator,
|
||||
flash,
|
||||
leaf,
|
||||
flask,
|
||||
} from 'ionicons/icons';
|
||||
|
||||
import '@ionic/react/css/core.css';
|
||||
import '@ionic/react/css/normalize.css';
|
||||
import '@ionic/react/css/structure.css';
|
||||
import '@ionic/react/css/typography.css';
|
||||
import '@ionic/react/css/padding.css';
|
||||
import '@ionic/react/css/float-elements.css';
|
||||
import '@ionic/react/css/text-alignment.css';
|
||||
import '@ionic/react/css/text-transformation.css';
|
||||
import '@ionic/react/css/flex-utils.css';
|
||||
import '@ionic/react/css/display.css';
|
||||
|
||||
import './theme/variables.css';
|
||||
import './App.css';
|
||||
|
||||
const urlMode = new URLSearchParams(window.location.search).get('mode');
|
||||
setupIonicReact({ mode: (urlMode === 'md' ? 'md' : 'ios') as 'ios' | 'md' });
|
||||
|
||||
const WelcomeScreen: React.FC = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonContent fullscreen className="welcome-content">
|
||||
<div className="background-shapes">
|
||||
<div className="shape-1"></div>
|
||||
<div className="shape-2"></div>
|
||||
</div>
|
||||
|
||||
<div className="content-wrapper">
|
||||
<div className="hero-section">
|
||||
<div className="badge">
|
||||
<span>IGCSE Excellence</span>
|
||||
</div>
|
||||
|
||||
<h1 className="hero-title">
|
||||
Unlock Your
|
||||
<br />
|
||||
<span className="text-gradient">Full Potential</span>
|
||||
</h1>
|
||||
|
||||
<p className="hero-subtitle">
|
||||
Master Mathematics, Sciences, and more with interactive bite-sized
|
||||
lessons tailored for your exams.
|
||||
</p>
|
||||
|
||||
<div className="subject-pills">
|
||||
<div className="subject-pill">
|
||||
<div className="subject-icon icon-math">
|
||||
<IonIcon icon={calculator} />
|
||||
</div>
|
||||
<span className="subject-name">Maths</span>
|
||||
</div>
|
||||
<div className="subject-pill">
|
||||
<div className="subject-icon icon-physics">
|
||||
<IonIcon icon={flash} />
|
||||
</div>
|
||||
<span className="subject-name">Physics</span>
|
||||
</div>
|
||||
<div className="subject-pill">
|
||||
<div className="subject-icon icon-bio">
|
||||
<IonIcon icon={leaf} />
|
||||
</div>
|
||||
<span className="subject-name">Biology</span>
|
||||
</div>
|
||||
<div className="subject-pill">
|
||||
<div className="subject-icon icon-chem">
|
||||
<IonIcon icon={flask} />
|
||||
</div>
|
||||
<span className="subject-name">Chemistry</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-section">
|
||||
<IonButton expand="block" className="start-button">
|
||||
Get Started
|
||||
<IonIcon icon={chevronForward} slot="end" />
|
||||
</IonButton>
|
||||
<IonButton expand="block" fill="clear" className="login-button">
|
||||
I already have an account
|
||||
</IonButton>
|
||||
</div>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => (
|
||||
<IonApp>
|
||||
<WelcomeScreen />
|
||||
</IonApp>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
// Derive the studio parent origin dynamically so this works in both dev
|
||||
// (parent at localhost:3000) and production (parent at appcakes.qqura.com).
|
||||
const studioOrigin = (() => {
|
||||
try {
|
||||
if (document.referrer) return new URL(document.referrer).origin;
|
||||
} catch {}
|
||||
return 'http://localhost:3000';
|
||||
})();
|
||||
|
||||
// ── Safe area bridge ──────────────────────────────────────────────────────────
|
||||
// Inject safe-area insets from URL params (first load) or postMessage (HMR reloads).
|
||||
// Capacitor sets env() natively in compiled apps; these paths cover the browser preview.
|
||||
const _sp = new URLSearchParams(window.location.search);
|
||||
const _sat = _sp.get('sat');
|
||||
const _sab = _sp.get('sab');
|
||||
if (_sat) document.documentElement.style.setProperty('--ion-safe-area-top', `${_sat}px`);
|
||||
if (_sab) document.documentElement.style.setProperty('--ion-safe-area-bottom', `${_sab}px`);
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data?.type !== '__apsuite_insets') return;
|
||||
const { sat, sab } = e.data as { sat?: number; sab?: number };
|
||||
if (sat != null) document.documentElement.style.setProperty('--ion-safe-area-top', `${sat}px`);
|
||||
if (sab != null) document.documentElement.style.setProperty('--ion-safe-area-bottom', `${sab}px`);
|
||||
});
|
||||
|
||||
// ── Session bridge ────────────────────────────────────────────────────────────
|
||||
// Post auth session to the AppSuite parent frame so the AI can test auth-protected
|
||||
// Edge Functions. Uses import.meta.glob so Vite never errors if supabase.ts is absent.
|
||||
(async () => {
|
||||
const mods = import.meta.glob('./supabase.ts', { eager: false });
|
||||
if ('./supabase.ts' in mods) {
|
||||
try {
|
||||
const { supabase } = await mods['./supabase.ts']() as { supabase: any };
|
||||
supabase.auth.onAuthStateChange((_event: unknown, session: any) => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: '__apsuite_session',
|
||||
access_token: session?.access_token ?? null,
|
||||
email: session?.user?.email ?? null,
|
||||
},
|
||||
studioOrigin,
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
// supabase client failed to initialise — session bridge unavailable
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Runtime error reporting ───────────────────────────────────────────────────
|
||||
// Forward uncaught errors to the AppSuite dev server so the AI can read them.
|
||||
function reportError(message: string, stack?: string) {
|
||||
fetch(`${studioOrigin}/api/agent/runtime-error`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, stack }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
window.onerror = (_msg, _src, _line, _col, err) => {
|
||||
reportError(String(_msg), err?.stack);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
const err = e.reason as Error | undefined;
|
||||
reportError(err?.message ?? String(e.reason), err?.stack);
|
||||
});
|
||||
|
||||
// ── App bootstrap ─────────────────────────────────────────────────────────────
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/* Ionic CSS Variables — customise to match your app's brand */
|
||||
:root {
|
||||
--ion-color-primary: #3880ff;
|
||||
--ion-color-primary-rgb: 56, 128, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3171e0;
|
||||
--ion-color-primary-tint: #4c8dff;
|
||||
|
||||
--ion-color-secondary: #3dc2ff;
|
||||
--ion-color-secondary-rgb: 61, 194, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #36abe0;
|
||||
--ion-color-secondary-tint: #50c8ff;
|
||||
|
||||
--ion-color-tertiary: #0ea5e9;
|
||||
--ion-color-tertiary-rgb: 14, 165, 233;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #0284c7;
|
||||
--ion-color-tertiary-tint: #38bdf8;
|
||||
|
||||
/* Custom AppSuite Project Rules */
|
||||
--radius-pill: 9999px;
|
||||
|
||||
--ion-color-success: #2dd36f;
|
||||
--ion-color-success-rgb: 45, 211, 111;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #28ba62;
|
||||
--ion-color-success-tint: #42d77d;
|
||||
|
||||
--ion-color-warning: #ffc409;
|
||||
--ion-color-warning-rgb: 255, 196, 9;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0ac08;
|
||||
--ion-color-warning-tint: #ffca22;
|
||||
|
||||
--ion-color-danger: #eb445a;
|
||||
--ion-color-danger-rgb: 235, 68, 90;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #cf3c4f;
|
||||
--ion-color-danger-tint: #ed576b;
|
||||
|
||||
--ion-color-dark: #222428;
|
||||
--ion-color-dark-rgb: 34, 36, 40;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1e2023;
|
||||
--ion-color-dark-tint: #383a3e;
|
||||
|
||||
--ion-color-medium: #92949c;
|
||||
--ion-color-medium-rgb: 146, 148, 156;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #808289;
|
||||
--ion-color-medium-tint: #9d9fa6;
|
||||
|
||||
--ion-color-light: #f4f5f8;
|
||||
--ion-color-light-rgb: 244, 245, 248;
|
||||
--ion-color-light-contrast: #000000;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-shade: #d7d8da;
|
||||
--ion-color-light-tint: #f5f6f9;
|
||||
}
|
||||
Reference in New Issue
Block a user