build: 55712324-398e-4334-a254-fc30c84b2e47

This commit is contained in:
AppCakes
2026-05-20 09:48:34 +00:00
commit a33818b158
67 changed files with 4277 additions and 0 deletions
+195
View File
@@ -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
View File
@@ -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;
+81
View File
@@ -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>,
);
+68
View File
@@ -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;
}