diff --git a/app/(app)/(tabs)/_layout.tsx b/app/(app)/(tabs)/_layout.tsx index 7c9d4c0..3fa821f 100644 --- a/app/(app)/(tabs)/_layout.tsx +++ b/app/(app)/(tabs)/_layout.tsx @@ -1,30 +1,85 @@ +/** + * (tabs)/_layout.tsx β Bottom Tab Navigator + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + import { Tabs } from 'expo-router'; +import { Text } from 'react-native'; import { theme } from '@/design/theme'; -import { fontSizes, fontWeights, spacing } from '@/design/tokens'; +import { fontSizes, fontWeights, palette, spacing } from '@/design/tokens'; + +type TabIconProps = { focused: boolean; label: string; emoji: string }; + +function TabIcon({ focused, emoji }: TabIconProps) { + return ( + + {emoji} + + ); +} export default function TabsLayout() { return ( - - - - + , + }} + /> + ( + + ), + }} + /> + , + }} + /> + ( + + ), + }} + /> ); } diff --git a/app/(app)/(tabs)/index.tsx b/app/(app)/(tabs)/index.tsx index 18ac520..353c853 100644 --- a/app/(app)/(tabs)/index.tsx +++ b/app/(app)/(tabs)/index.tsx @@ -1,22 +1,616 @@ +/** + * index.tsx β Home Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + import { router } from 'expo-router'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; -import { SessionPlaceholderScreen } from '@/components/SessionPlaceholderScreen'; -import { Button } from '@/components/ui/Button'; +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; import { useSession } from '@/providers/SessionProvider'; +const QUICK_ACTIONS = [ + { + id: 'learn', + emoji: 'π', + label: 'Learn', + sub: '12 lessons', + route: '/learn' as const, + accent: palette.brandGreen, + }, + { + id: 'puzzles', + emoji: 'π§©', + label: 'Puzzles', + sub: 'Daily challenge', + route: '/puzzles' as const, + accent: palette.accentYellow, + }, + { + id: 'play', + emoji: 'β', + label: 'Play', + sub: 'vs Computer', + route: '/play' as const, + accent: palette.ink, + }, + { + id: 'profile', + emoji: 'π', + label: 'Profile', + sub: 'Your progress', + route: '/profile' as const, + accent: '#9B59B6', + }, +]; + +const ACTIVITY = [ + { + id: '1', + icon: 'π', + text: 'Completed Knight lesson', + time: '2h ago', + color: palette.brandGreen, + }, + { + id: '2', + icon: 'π§©', + text: 'Solved puzzle #4820', + time: 'Yesterday', + color: palette.accentYellow, + }, + { + id: '3', + icon: 'β', + text: 'Won game vs Beginner', + time: '2d ago', + color: palette.ink, + }, + { + id: '4', + icon: 'π±', + text: 'Joined YSTEM and Chess', + time: '1 week ago', + color: palette.brandGreenSoft, + }, +]; + export default function HomeRoute() { const { session } = useSession(); - - const isMentor = - session.status === 'authenticated' && session.user.role === 'mentor'; + const displayName = + session.status === 'authenticated' ? session.user.displayName : 'there'; + const firstName = displayName.split(' ')[0]; + const hour = new Date().getHours(); + const greeting = + hour < 12 ? 'Good morning' : hour < 17 ? 'Good afternoon' : 'Good evening'; return ( - - {isMentor ? ( - router.push('/mentor')} variant="secondary"> - Open mentor area - - ) : null} - + + + {/* ββ Header βββββββββββββββββββββββββββββββ */} + + + {greeting} + {firstName} π + + + + {firstName.charAt(0).toUpperCase()} + + + + + {/* ββ Progress Hero Card βββββββββββββββββββ */} + + + + YOUR PROGRESS + 250 XP + + + Lvl 3 + + + + {/* XP Progress Bar */} + + + 150 XP to next level + + + + {[ + { value: '5', label: 'Streak', icon: 'π₯' }, + { value: '12', label: 'Puzzles', icon: 'π§©' }, + { value: '3', label: 'Wins', icon: 'β' }, + ].map((stat) => ( + + {stat.icon} + {stat.value} + {stat.label} + + ))} + + + + {/* ββ Quick Actions ββββββββββββββββββββββββ */} + + Quick Actions + + + {QUICK_ACTIONS.map((action) => ( + router.push(action.route as never)} + style={({ pressed }) => [ + s.actionCard, + pressed && s.actionCardPressed, + ]} + > + + {action.emoji} + {action.label} + {action.sub} + + ))} + + + {/* ββ Continue Learning ββββββββββββββββββββ */} + + Continue Learning + router.push('/learn' as never)}> + See all β + + + + router.push({ + pathname: '/lesson-detail', + params: { piece: 'rook', id: '2' }, + } as never) + } + style={({ pressed }) => [ + s.continueCard, + pressed && s.continueCardPressed, + ]} + > + + + β + + + Rook Lesson + The Power of Open Files + + + + + Step 2 of 4 + + + + βΊ + + + {/* ββ Today's Challenge ββββββββββββββββββββ */} + + Todayβs Challenge + + router.push('/puzzles' as never)} + style={({ pressed }) => [ + s.challengeCard, + pressed && s.challengeCardPressed, + ]} + > + + π§© + + Daily Puzzle + Rating 1287 Β· Fork theme + + + + +10 XP + + + + {/* ββ Recent Activity ββββββββββββββββββββββ */} + + Recent Activity + + + {ACTIVITY.map((item, i) => ( + + + + {item.icon} + + + {item.text} + + {item.time} + + ))} + + + {/* ββ Donate CTA βββββββββββββββββββββββββββ */} + + + β€οΈ + + Support YSTEM + Help a student learn chess & STEM + + + [ + s.donateBtn, + pressed && s.donateBtnPressed, + ]} + > + Donate + + + + {session.status === 'authenticated' && + session.user.role === 'mentor' && ( + router.push('/mentor')} + style={({ pressed }) => [ + s.mentorButton, + pressed && s.mentorButtonPressed, + ]} + > + Open mentor area + + )} + + + + ); } + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + scroll: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg, + gap: spacing.md, + }, + + /* Header */ + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.xs, + }, + headerLeft: { gap: 2 }, + greetingSub: { fontSize: fontSizes.caption, color: palette.muted }, + greetingName: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: -0.5, + }, + avatar: { + width: 44, + height: 44, + borderRadius: radii.pill, + backgroundColor: palette.brandGreen, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.white, + }, + + /* Hero Card */ + hero: { + backgroundColor: palette.ink, + borderRadius: radii.xl, + padding: spacing.lg, + gap: spacing.md, + ...SHADOW, + }, + heroTop: { + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + }, + heroLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: 'rgba(255,255,255,0.5)', + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + heroXP: { + fontSize: fontSizes.display, + fontWeight: fontWeights.bold, + color: palette.white, + letterSpacing: -1, + marginTop: 2, + }, + levelBadge: { + backgroundColor: palette.brandGreen, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + levelText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + xpTrack: { gap: 6 }, + xpFill: { + height: 6, + width: '62.5%', + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + /* Parent track β use overflow hidden on a wrapper */ + }, + xpLabel: { fontSize: 10, color: 'rgba(255,255,255,0.5)' }, + heroStats: { + flexDirection: 'row', + borderTopWidth: 1, + borderTopColor: 'rgba(255,255,255,0.1)', + paddingTop: spacing.md, + }, + heroStat: { flex: 1, alignItems: 'center', gap: 2 }, + heroStatIcon: { fontSize: 16 }, + heroStatValue: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.white, + }, + heroStatLabel: { fontSize: 10, color: 'rgba(255,255,255,0.5)' }, + + /* Section headers */ + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: spacing.xs, + }, + sectionLabel: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + sectionLink: { + fontSize: fontSizes.caption, + color: palette.brandGreen, + fontWeight: fontWeights.semibold, + }, + + /* Quick Actions Grid */ + grid: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm }, + actionCard: { + width: '47.5%', + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.md, + gap: spacing.xxs, + overflow: 'hidden', + position: 'relative', + ...SHADOW, + }, + actionCardPressed: { opacity: 0.85, transform: [{ scale: 0.98 }] }, + actionAccent: { position: 'absolute', top: 0, left: 0, right: 0, height: 3 }, + actionEmoji: { fontSize: 26, marginTop: spacing.xs }, + actionLabel: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + actionSub: { fontSize: fontSizes.caption, color: palette.muted }, + + /* Continue Card */ + continueCard: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.md, + flexDirection: 'row', + alignItems: 'center', + ...SHADOW, + }, + continueCardPressed: { opacity: 0.85 }, + continueLeft: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + }, + continueIcon: { + width: 52, + height: 52, + borderRadius: radii.md, + backgroundColor: palette.backgroundSoft, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: palette.brandGreenSoft, + }, + continueInfo: { flex: 1, gap: 2 }, + continueName: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + continueSub: { fontSize: fontSizes.caption, color: palette.muted }, + continueProgress: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + marginTop: spacing.xs, + }, + continueBar: { + flex: 1, + height: 4, + backgroundColor: palette.border, + borderRadius: radii.pill, + overflow: 'hidden', + }, + continueBarFill: { + height: '100%', + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + }, + continueBarLabel: { fontSize: 10, color: palette.muted }, + continueArrow: { + fontSize: fontSizes.title, + color: palette.muted, + marginLeft: spacing.sm, + }, + + /* Challenge Card */ + challengeCard: { + backgroundColor: `${palette.accentYellow}22`, + borderWidth: 1, + borderColor: palette.accentYellow, + borderRadius: radii.lg, + padding: spacing.md, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + challengeCardPressed: { opacity: 0.85 }, + challengeLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + }, + challengeEmoji: { fontSize: 28 }, + challengeTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + challengeSub: { fontSize: fontSizes.caption, color: palette.gray }, + challengeBadge: { + backgroundColor: palette.accentYellow, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + challengeBadgeText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + + /* Activity */ + activityCard: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + ...SHADOW, + }, + activityRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm + 2, + }, + activityRowBorder: { + borderBottomWidth: 1, + borderBottomColor: palette.border, + }, + activityDot: { width: 7, height: 7, borderRadius: radii.pill }, + activityIcon: { width: 30 }, + activityText: { flex: 1, fontSize: fontSizes.caption, color: palette.ink }, + activityTime: { fontSize: 10, color: palette.muted }, + + /* Donate */ + donateCard: { + backgroundColor: `${palette.brandGreen}15`, + borderWidth: 1, + borderColor: `${palette.brandGreen}40`, + borderRadius: radii.lg, + padding: spacing.md, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + donateLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + flex: 1, + }, + donateTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + donateSub: { fontSize: fontSizes.caption, color: palette.gray }, + donateBtn: { + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, + }, + donateBtnPressed: { backgroundColor: palette.brandGreenPressed }, + donateBtnText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + mentorButton: { + alignItems: 'center', + backgroundColor: palette.ink, + borderRadius: radii.md, + paddingVertical: spacing.md, + }, + mentorButtonPressed: { opacity: 0.8 }, + mentorButtonText: { + color: palette.white, + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + }, +}); diff --git a/app/(app)/(tabs)/learn.tsx b/app/(app)/(tabs)/learn.tsx index ab7faa3..4bf6f03 100644 --- a/app/(app)/(tabs)/learn.tsx +++ b/app/(app)/(tabs)/learn.tsx @@ -1,5 +1,605 @@ -import { SessionPlaceholderScreen } from '@/components/SessionPlaceholderScreen'; +/** + * learn.tsx β Chess Lessons Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { router } from 'expo-router'; +import { useRef, useState } from 'react'; +import { + Animated, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; + +type LessonStatus = 'done' | 'available' | 'locked'; + +type Piece = { + id: string; + emoji: string; + name: string; + mastered: number; + total: number; +}; + +type Lesson = { + id: string; + title: string; + duration: string; + status: LessonStatus; + xp: number; +}; + +const PIECES: Piece[] = [ + { id: 'pawn', emoji: 'β', name: 'Pawn', mastered: 3, total: 3 }, + { id: 'knight', emoji: 'β', name: 'Knight', mastered: 2, total: 4 }, + { id: 'bishop', emoji: 'β', name: 'Bishop', mastered: 0, total: 3 }, + { id: 'rook', emoji: 'β', name: 'Rook', mastered: 0, total: 3 }, + { id: 'queen', emoji: 'β', name: 'Queen', mastered: 0, total: 4 }, + { id: 'king', emoji: 'β', name: 'King', mastered: 0, total: 2 }, +]; + +const LESSONS_BY_PIECE: Record = { + pawn: [ + { + id: 'p1', + title: "The Pawn's First Move", + duration: '3 min', + status: 'done', + xp: 25, + }, + { + id: 'p2', + title: 'Pawn Captures', + duration: '4 min', + status: 'done', + xp: 25, + }, + { + id: 'p3', + title: 'En Passant & Promotion', + duration: '5 min', + status: 'done', + xp: 50, + }, + ], + knight: [ + { + id: 'k1', + title: 'The L-Shape Move', + duration: '4 min', + status: 'done', + xp: 25, + }, + { + id: 'k2', + title: 'Knight Forks', + duration: '5 min', + status: 'done', + xp: 30, + }, + { + id: 'k3', + title: 'Knight Outposts', + duration: '6 min', + status: 'available', + xp: 35, + }, + { + id: 'k4', + title: 'Knight vs Bishop', + duration: '7 min', + status: 'locked', + xp: 40, + }, + ], + bishop: [ + { + id: 'b1', + title: 'Diagonal Power', + duration: '3 min', + status: 'available', + xp: 25, + }, + { + id: 'b2', + title: 'The Bishop Pair', + duration: '5 min', + status: 'locked', + xp: 30, + }, + { + id: 'b3', + title: 'Good vs Bad Bishop', + duration: '6 min', + status: 'locked', + xp: 35, + }, + ], + rook: [ + { + id: 'r1', + title: 'Open File Strategy', + duration: '4 min', + status: 'available', + xp: 25, + }, + { + id: 'r2', + title: 'The Seventh Rank', + duration: '5 min', + status: 'locked', + xp: 30, + }, + { + id: 'r3', + title: 'Rook Endgames', + duration: '8 min', + status: 'locked', + xp: 50, + }, + ], + queen: [ + { + id: 'q1', + title: 'The Most Powerful Piece', + duration: '4 min', + status: 'available', + xp: 25, + }, + { + id: 'q2', + title: 'Queen Attacks', + duration: '5 min', + status: 'locked', + xp: 30, + }, + { + id: 'q3', + title: 'Queen & Pawn Endgames', + duration: '7 min', + status: 'locked', + xp: 40, + }, + { + id: 'q4', + title: 'The Queen Sacrifice', + duration: '6 min', + status: 'locked', + xp: 50, + }, + ], + king: [ + { + id: 'kg1', + title: 'King Safety & Castling', + duration: '5 min', + status: 'available', + xp: 30, + }, + { + id: 'kg2', + title: 'King in the Endgame', + duration: '7 min', + status: 'locked', + xp: 40, + }, + ], +}; + +const STATUS_META: Record< + LessonStatus, + { icon: string; color: string; bg: string } +> = { + done: { icon: 'β', color: palette.brandGreen, bg: palette.backgroundSoft }, + available: { icon: 'βΆ', color: palette.ink, bg: palette.surface }, + locked: { icon: 'π', color: palette.muted, bg: palette.surface }, +}; export default function LearnRoute() { - return ; + const [selectedPiece, setSelectedPiece] = useState('knight'); + const scrollRef = useRef(null); + + const lessons = LESSONS_BY_PIECE[selectedPiece] ?? []; + const totalMastered = PIECES.filter((p) => p.mastered === p.total).length; + const totalPieces = PIECES.length; + + return ( + + {/* ββ Header βββββββββββββββββββββββββββββββ */} + + + CHESS ACADEMY + Your Learning Path + + + + {totalMastered}/{totalPieces} mastered + + + + + {/* ββ Progress Bar βββββββββββββββββββββββββ */} + + + + + + {/* ββ Piece Selector βββββββββββββββββββββββ */} + Choose a piece to study + + {PIECES.map((piece) => { + const isSelected = selectedPiece === piece.id; + const isDone = piece.mastered === piece.total; + const progress = piece.total > 0 ? piece.mastered / piece.total : 0; + + return ( + setSelectedPiece(piece.id)} + style={[s.pieceCard, isSelected && s.pieceCardSelected]} + > + {/* Completion ring indicator */} + {isDone && ( + + β + + )} + + {piece.emoji} + + + {piece.name} + + + + + + + {piece.mastered}/{piece.total} + + + + ); + })} + + + {/* ββ Lesson List ββββββββββββββββββββββββββ */} + + + {PIECES.find((p) => p.id === selectedPiece)?.name} Lessons + + + {lessons.filter((l) => l.status === 'done').length}/{lessons.length}{' '} + done + + + + + {lessons.map((lesson, i) => { + const meta = STATUS_META[lesson.status]; + const isAvailable = lesson.status !== 'locked'; + + return ( + + router.push({ + pathname: '/lesson-detail', + params: { id: lesson.id, piece: selectedPiece }, + } as never) + } + style={({ pressed }) => [ + s.lessonRow, + i < lessons.length - 1 && s.lessonRowBorder, + !isAvailable && s.lessonRowLocked, + pressed && isAvailable && s.lessonRowPressed, + ]} + > + {/* Step number / status indicator */} + + + {meta.icon} + + + + + + {lesson.title} + + + β± {lesson.duration} + + +{lesson.xp} XP + + + + + {isAvailable && βΊ} + + ); + })} + + + + + + {/* ββ CTA Footer βββββββββββββββββββββββββββ */} + {(() => { + const nextLesson = lessons.find((l) => l.status === 'available'); + if (!nextLesson) return null; + return ( + + + router.push({ + pathname: '/lesson-detail', + params: { id: nextLesson.id, piece: selectedPiece }, + } as never) + } + style={({ pressed }) => [s.ctaBtn, pressed && s.ctaBtnPressed]} + > + Continue: {nextLesson.title} β + + + ); + })()} + + ); } + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + scroll: { paddingHorizontal: spacing.lg, gap: spacing.md }, + + /* Header */ + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg, + paddingBottom: spacing.sm, + }, + headerSup: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + headerTitle: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: -0.5, + }, + progressPill: { + backgroundColor: `${palette.brandGreen}20`, + borderWidth: 1, + borderColor: `${palette.brandGreen}40`, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + progressPillText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + + /* Progress bar */ + progressTrack: { + height: 3, + backgroundColor: palette.border, + marginHorizontal: spacing.lg, + marginBottom: spacing.md, + borderRadius: radii.pill, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + }, + + subLabel: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + + /* Piece cards */ + pieces: { gap: spacing.sm, paddingBottom: spacing.xs }, + pieceCard: { + width: 88, + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 2, + borderColor: palette.border, + padding: spacing.sm, + alignItems: 'center', + gap: 4, + position: 'relative', + overflow: 'hidden', + ...SHADOW, + }, + pieceCardSelected: { + borderColor: palette.brandGreen, + backgroundColor: palette.backgroundSoft, + }, + pieceCheck: { + position: 'absolute', + top: 5, + right: 5, + width: 18, + height: 18, + borderRadius: radii.pill, + backgroundColor: palette.brandGreen, + alignItems: 'center', + justifyContent: 'center', + }, + pieceCheckIcon: { + fontSize: 9, + color: palette.white, + fontWeight: fontWeights.bold, + }, + pieceEmoji: { fontSize: 28 }, + pieceName: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.semibold, + color: palette.muted, + }, + pieceNameSelected: { color: palette.ink }, + pieceMeta: { width: '100%', gap: 2, alignItems: 'center' }, + pieceBar: { + width: '100%', + height: 3, + backgroundColor: palette.border, + borderRadius: radii.pill, + overflow: 'hidden', + }, + pieceBarFill: { height: '100%', borderRadius: radii.pill }, + pieceCount: { fontSize: 9, color: palette.muted }, + pieceCountSelected: { color: palette.gray }, + + /* Lesson list */ + lessonsHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginTop: spacing.xs, + }, + lessonsCount: { fontSize: fontSizes.caption, color: palette.muted }, + lessonList: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + ...SHADOW, + }, + lessonRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + padding: spacing.md, + }, + lessonRowBorder: { borderBottomWidth: 1, borderBottomColor: palette.border }, + lessonRowLocked: { opacity: 0.5 }, + lessonRowPressed: { backgroundColor: palette.backgroundSoft }, + lessonStatus: { + width: 36, + height: 36, + borderRadius: radii.pill, + borderWidth: 2, + alignItems: 'center', + justifyContent: 'center', + }, + lessonStatusIcon: { fontSize: 13, fontWeight: fontWeights.bold }, + lessonContent: { flex: 1, gap: 3 }, + lessonTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + lessonTitleLocked: { color: palette.muted }, + lessonMeta: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + lessonDuration: { fontSize: fontSizes.caption, color: palette.muted }, + lessonXPBadge: { + backgroundColor: `${palette.accentYellow}33`, + paddingVertical: 1, + paddingHorizontal: spacing.xs, + borderRadius: radii.pill, + }, + lessonXPText: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + lessonArrow: { fontSize: fontSizes.heading, color: palette.muted }, + + /* Footer CTA */ + footer: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + paddingBottom: spacing.lg, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: palette.border, + }, + ctaBtn: { + backgroundColor: palette.ink, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + ctaBtnPressed: { opacity: 0.85 }, + ctaBtnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.white, + }, +}); diff --git a/app/(app)/(tabs)/play.tsx b/app/(app)/(tabs)/play.tsx index 08fc19f..44c9d34 100644 --- a/app/(app)/(tabs)/play.tsx +++ b/app/(app)/(tabs)/play.tsx @@ -1,5 +1,480 @@ -import { SessionPlaceholderScreen } from '@/components/SessionPlaceholderScreen'; +/** + * play.tsx β Play vs Computer Setup Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { router } from 'expo-router'; +import { useState } from 'react'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; + +type PieceColor = 'white' | 'black' | 'random'; +type Difficulty = 'beginner' | 'easy' | 'medium' | 'hard' | 'master'; + +const COLORS: { + id: PieceColor; + emoji: string; + label: string; + sub: string; + bg: string; + border: string; + textColor: string; +}[] = [ + { + id: 'white', + emoji: 'β', + label: 'White', + sub: 'Moves first', + bg: '#FAFAFA', + border: '#E0E0E0', + textColor: palette.ink, + }, + { + id: 'black', + emoji: 'β', + label: 'Black', + sub: 'Moves second', + bg: '#2A2A2A', + border: '#444444', + textColor: palette.white, + }, + { + id: 'random', + emoji: 'β', + label: 'Random', + sub: 'Surprise me!', + bg: palette.backgroundSoft, + border: palette.brandGreenSoft, + textColor: palette.ink, + }, +]; + +const DIFFICULTIES: { + id: Difficulty; + emoji: string; + label: string; + desc: string; + level: number; + color: string; + bg: string; +}[] = [ + { + id: 'beginner', + emoji: 'π±', + label: 'Beginner', + desc: 'Learn the basics', + level: 1, + color: palette.brandGreen, + bg: `${palette.brandGreen}1A`, + }, + { + id: 'easy', + emoji: 'π', + label: 'Easy', + desc: 'Gentle challenge', + level: 4, + color: palette.brandGreenSoft, + bg: `${palette.brandGreenSoft}33`, + }, + { + id: 'medium', + emoji: 'π€', + label: 'Medium', + desc: 'Real thinking', + level: 8, + color: palette.accentYellow, + bg: `${palette.accentYellow}22`, + }, + { + id: 'hard', + emoji: 'π€', + label: 'Hard', + desc: 'Strong tactics', + level: 14, + color: '#FF8C42', + bg: 'rgba(255,140,66,0.12)', + }, + { + id: 'master', + emoji: 'π', + label: 'Master', + desc: 'Near-perfect play', + level: 20, + color: palette.error, + bg: `${palette.error}1A`, + }, +]; export default function PlayRoute() { - return ; + const [color, setColor] = useState('white'); + const [difficulty, setDifficulty] = useState('beginner'); + + const chosen = DIFFICULTIES.find((d) => d.id === difficulty)!; + const chosenColor = COLORS.find((c) => c.id === color)!; + + return ( + + {/* ββ Header βββββββββββββββββββββββββββββββ */} + + SINGLE PLAYER + Play vs Computer + + + + {/* ββ Color Picker ββββββββββββββββββββββββ */} + + Choose your side + + {COLORS.map((c) => ( + setColor(c.id)} + style={[ + s.colorCard, + { backgroundColor: c.bg, borderColor: c.border }, + color === c.id && s.colorCardSelected, + ]} + > + {color === c.id && ( + + + β + + + )} + {c.emoji} + + {c.label} + + + {c.sub} + + + ))} + + + + {/* ββ Difficulty ββββββββββββββββββββββββββ */} + + + Difficulty + + + {chosen.emoji} {chosen.label} + + + + + {DIFFICULTIES.map((d) => ( + setDifficulty(d.id)} + style={[s.diffRow, difficulty === d.id && s.diffRowSelected]} + > + {/* Color accent bar on left */} + + + {d.emoji} + + + + {d.label} + + {d.desc} + + + {/* Level dots */} + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + + {difficulty === d.id && ( + β + )} + + ))} + + + {/* ββ Game Summary ββββββββββββββββββββββββ */} + + Game Summary + + {[ + { + label: 'Playing as', + value: `${chosenColor.emoji} ${chosenColor.label}`, + }, + { label: 'Opponent', value: `π€ Computer (${chosen.label})` }, + { + label: 'Engine Lv', + value: `${chosen.emoji} Level ${chosen.level}`, + }, + ].map((row, i, arr) => ( + + {row.label} + {row.value} + + ))} + + + + + + + {/* ββ CTA Footer βββββββββββββββββββββββββββ */} + + + router.push({ + pathname: '/play-game', + params: { color, difficulty }, + } as never) + } + style={({ pressed }) => [s.ctaBtn, pressed && s.ctaBtnPressed]} + > + Letβs Play β + + + + ); } + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + header: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg, + paddingBottom: spacing.xs, + }, + headerSup: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + headerTitle: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: -0.5, + }, + scroll: { paddingHorizontal: spacing.lg, gap: spacing.xl }, + + subLabel: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + marginBottom: spacing.sm, + }, + section: { gap: spacing.xs }, + + /* Color picker */ + colorRow: { flexDirection: 'row', gap: spacing.sm }, + colorCard: { + flex: 1, + borderRadius: radii.xl, + borderWidth: 2, + padding: spacing.md, + alignItems: 'center', + gap: spacing.xs, + position: 'relative', + overflow: 'hidden', + minHeight: 110, + justifyContent: 'center', + ...SHADOW, + }, + colorCardSelected: { borderColor: palette.brandGreen }, + colorCheck: { + position: 'absolute', + top: spacing.xs, + right: spacing.xs, + width: 20, + height: 20, + borderRadius: radii.pill, + alignItems: 'center', + justifyContent: 'center', + }, + colorEmoji: { fontSize: 36 }, + colorLabel: { fontSize: fontSizes.label, fontWeight: fontWeights.bold }, + colorSub: { fontSize: fontSizes.caption }, + + /* Difficulty list */ + diffTitleRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: spacing.xs, + }, + diffChosenBadge: { + paddingVertical: 3, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + diffChosenText: { fontSize: fontSizes.caption, fontWeight: fontWeights.bold }, + diffRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1.5, + borderColor: palette.border, + padding: spacing.md, + overflow: 'hidden', + position: 'relative', + marginBottom: spacing.sm, + ...SHADOW, + }, + diffRowSelected: { + borderColor: palette.brandGreen, + backgroundColor: palette.backgroundSoft, + }, + diffAccentBar: { position: 'absolute', left: 0, top: 0, bottom: 0, width: 3 }, + diffEmoji: { fontSize: 22 }, + diffInfo: { flex: 1, gap: 2 }, + diffLabel: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.muted, + }, + diffDesc: { fontSize: fontSizes.caption, color: palette.muted }, + diffDots: { flexDirection: 'row', gap: 3 }, + diffDot: { width: 6, height: 6, borderRadius: radii.pill }, + diffCheck: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + marginLeft: spacing.xs, + }, + + /* Summary */ + summary: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + ...SHADOW, + }, + summaryTitle: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + paddingHorizontal: spacing.md, + paddingTop: spacing.md, + paddingBottom: spacing.sm, + }, + summaryRows: {}, + summaryRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm + 2, + }, + summaryRowBorder: { borderBottomWidth: 1, borderBottomColor: palette.border }, + summaryLabel: { fontSize: fontSizes.caption, color: palette.muted }, + summaryValue: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + + /* Footer */ + footer: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + paddingBottom: spacing.lg, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: palette.border, + }, + ctaBtn: { + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + ctaBtnPressed: { backgroundColor: palette.brandGreenPressed }, + ctaBtnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: 0.3, + }, +}); diff --git a/app/(app)/(tabs)/profile.tsx b/app/(app)/(tabs)/profile.tsx index 3e313b7..f620b6a 100644 --- a/app/(app)/(tabs)/profile.tsx +++ b/app/(app)/(tabs)/profile.tsx @@ -1,15 +1,483 @@ -import { SessionPlaceholderScreen } from '@/components/SessionPlaceholderScreen'; -import { Button } from '@/components/ui/Button'; +/** + * profile.tsx β Profile Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; import { useSession } from '@/providers/SessionProvider'; +type Badge = { + id: string; + emoji: string; + label: string; + desc: string; + earned: boolean; +}; +type StatItem = { label: string; value: string; icon: string }; +type MenuItem = { label: string; emoji: string; sub: string }; + +const STATS: StatItem[] = [ + { label: 'Lessons', value: '5', icon: 'π' }, + { label: 'Puzzles', value: '12', icon: 'π§©' }, + { label: 'Wins', value: '3', icon: 'β' }, + { label: 'Streak', value: '5', icon: 'π₯' }, +]; + +const BADGES: Badge[] = [ + { + id: '1', + emoji: 'π±', + label: 'First Lesson', + desc: 'Complete your first lesson', + earned: true, + }, + { + id: '2', + emoji: 'π§©', + label: 'Puzzle Solver', + desc: 'Solve 10 puzzles', + earned: true, + }, + { + id: '3', + emoji: 'β', + label: 'First Win', + desc: 'Win your first game', + earned: true, + }, + { + id: '4', + emoji: 'π₯', + label: '5-Day Streak', + desc: 'Practice 5 days in a row', + earned: true, + }, + { + id: '5', + emoji: 'π', + label: 'Quick Learner', + desc: 'Complete 5 lessons in one week', + earned: true, + }, + { + id: '6', + emoji: 'π', + label: 'Champion', + desc: 'Win 10 games', + earned: false, + }, + { + id: '7', + emoji: 'π', + label: 'Mastermind', + desc: 'Solve a 1600+ rated puzzle', + earned: false, + }, + { + id: '8', + emoji: 'β‘', + label: 'Speed Solver', + desc: 'Solve a puzzle in under 30 seconds', + earned: false, + }, + { + id: '9', + emoji: 'π―', + label: 'Perfectionist', + desc: '10 correct puzzles in a row', + earned: false, + }, +]; + +const MENU_SECTIONS: { title: string; items: MenuItem[] }[] = [ + { + title: 'Account', + items: [ + { label: 'Edit Profile', emoji: 'βοΈ', sub: 'Name, avatar, bio' }, + { + label: 'Notification Settings', + emoji: 'π', + sub: 'Alerts & reminders', + }, + ], + }, + { + title: 'Privacy', + items: [ + { + label: 'Privacy & Security', + emoji: 'π', + sub: 'Data & account protection', + }, + { label: 'Connected Accounts', emoji: 'π', sub: 'Google, Apple' }, + ], + }, + { + title: 'Support', + items: [ + { label: 'Help & Support', emoji: 'β', sub: 'FAQs & contact us' }, + { label: 'About Y STEM', emoji: 'βΉοΈ', sub: 'Mission & team' }, + ], + }, +]; + export default function ProfileRoute() { - const { signOut } = useSession(); + const { session, signOut } = useSession(); + const displayName = + session.status === 'authenticated' ? session.user.displayName : 'Student'; + const role = + session.status === 'authenticated' ? session.user.role : 'student'; + const initial = displayName.charAt(0).toUpperCase(); + const earnedCount = BADGES.filter((b) => b.earned).length; return ( - - - Sign out - - + + + {/* ββ Hero βββββββββββββββββββββββββββββββββ */} + + + + {initial} + + + {displayName} + + {role === 'mentor' ? 'π' : 'π'} + + {role === 'mentor' ? 'Mentor' : 'Student'} + + + + + 250 XP + + Β· + Level 3 + + + + {/* ββ Stats ββββββββββββββββββββββββββββββββ */} + + {STATS.map((stat, i) => ( + + {stat.icon} + {stat.value} + {stat.label} + + ))} + + + {/* ββ Badges βββββββββββββββββββββββββββββββ */} + + + Badges + + {earnedCount}/{BADGES.length} earned + + + + {BADGES.map((badge) => ( + [ + s.badgeCard, + !badge.earned && s.badgeCardLocked, + pressed && badge.earned && s.badgeCardPressed, + ]} + > + + {badge.emoji} + + + {badge.label} + + {!badge.earned && ( + + π + + )} + + ))} + + + + {/* ββ Settings Sections ββββββββββββββββββββ */} + {MENU_SECTIONS.map((section) => ( + + {section.title} + + {section.items.map((item, i) => ( + [ + s.menuRow, + i < section.items.length - 1 && s.menuRowBorder, + pressed && s.menuRowPressed, + ]} + > + + {item.emoji} + + + {item.label} + {item.sub} + + βΊ + + ))} + + + ))} + + {/* ββ Sign Out βββββββββββββββββββββββββββββ */} + [ + s.signOutBtn, + pressed && s.signOutBtnPressed, + ]} + > + Sign out + + + + + ); } + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + scroll: { + paddingHorizontal: spacing.lg, + paddingTop: spacing.lg, + gap: spacing.xl, + }, + + /* Hero */ + hero: { alignItems: 'center', gap: spacing.sm, paddingBottom: spacing.md }, + avatarRing: { + width: 92, + height: 92, + borderRadius: radii.pill, + backgroundColor: `${palette.brandGreen}25`, + padding: 4, + }, + avatar: { + flex: 1, + borderRadius: radii.pill, + backgroundColor: palette.brandGreen, + alignItems: 'center', + justifyContent: 'center', + }, + avatarText: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.white, + }, + displayName: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + roleBadge: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xxs, + backgroundColor: `${palette.brandGreen}20`, + borderWidth: 1, + borderColor: `${palette.brandGreen}40`, + borderRadius: radii.pill, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + }, + roleIcon: { fontSize: 13 }, + roleText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + xpRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + xpPill: { + backgroundColor: `${palette.accentYellow}33`, + paddingVertical: 2, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + xpText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + xpSep: { color: palette.muted }, + xpLevel: { fontSize: fontSizes.caption, color: palette.muted }, + + /* Stats */ + statsCard: { + flexDirection: 'row', + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + ...SHADOW, + }, + statItem: { + flex: 1, + alignItems: 'center', + paddingVertical: spacing.md, + gap: 2, + }, + statItemBorder: { borderRightWidth: 1, borderRightColor: palette.border }, + statIcon: { fontSize: 16, marginBottom: 2 }, + statValue: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + statLabel: { fontSize: fontSizes.caption - 1, color: palette.muted }, + + /* Section */ + section: { gap: spacing.md }, + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + sectionTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + sectionSub: { fontSize: fontSizes.caption, color: palette.muted }, + + /* Badges */ + badgesGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm }, + badgeCard: { + width: '30%', + flexGrow: 1, + backgroundColor: theme.colors.surface, + borderRadius: radii.md, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.sm, + alignItems: 'center', + gap: spacing.xxs, + position: 'relative', + overflow: 'hidden', + ...SHADOW, + }, + badgeCardLocked: { opacity: 0.6 }, + badgeCardPressed: { backgroundColor: palette.backgroundSoft }, + badgeEmoji: { fontSize: 26, marginBottom: 2 }, + badgeLabel: { + fontSize: fontSizes.caption - 1, + fontWeight: fontWeights.semibold, + color: palette.gray, + textAlign: 'center', + }, + badgeLockOverlay: { + position: 'absolute', + top: spacing.xxs, + right: spacing.xxs, + }, + badgeLockIcon: { fontSize: 10 }, + + /* Menu */ + menuSectionTitle: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + menuCard: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + ...SHADOW, + }, + menuRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + paddingVertical: spacing.md, + paddingHorizontal: spacing.md, + }, + menuRowBorder: { borderBottomWidth: 1, borderBottomColor: palette.border }, + menuRowPressed: { backgroundColor: palette.backgroundSoft }, + menuIconWrap: { + width: 36, + height: 36, + borderRadius: radii.sm, + backgroundColor: theme.colors.background, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: palette.border, + }, + menuEmoji: { fontSize: 17 }, + menuContent: { flex: 1, gap: 1 }, + menuLabel: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + menuSub: { fontSize: fontSizes.caption, color: palette.muted }, + menuArrow: { fontSize: fontSizes.heading, color: palette.muted }, + + /* Sign out */ + signOutBtn: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + ...SHADOW, + }, + signOutBtnPressed: { backgroundColor: palette.errorBackground }, + signOutText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.error, + }, +}); diff --git a/app/(app)/lesson-detail.tsx b/app/(app)/lesson-detail.tsx new file mode 100644 index 0000000..08b39f1 --- /dev/null +++ b/app/(app)/lesson-detail.tsx @@ -0,0 +1,626 @@ +/** + * lesson-detail.tsx β Lesson Step-Through Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { router, useLocalSearchParams } from 'expo-router'; +import { useState } from 'react'; +import { + Animated, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; + +type Step = { + id: string; + title: string; + instruction: string; + tip?: string; + highlight?: string; + piece?: string; +}; + +const STEPS_BY_PIECE: Record = { + knight: [ + { + id: '1', + title: 'The L-Shape Move', + instruction: + 'The Knight is the only piece that can jump over other pieces. It moves in an "L" shape: two squares in one direction, then one square perpendicular.', + tip: 'Knights always land on a different color square than they started on!', + highlight: 'g1', + piece: 'β', + }, + { + id: '2', + title: 'Power of the Center', + instruction: + 'From e4, the Knight can jump to f6, g5, g3, f2, d2, c3, c5, or d6 β up to 8 squares from the center!', + tip: 'Center Knights are more powerful β they control more squares.', + highlight: 'e4', + piece: 'β', + }, + { + id: '3', + title: 'Knights at the Edge', + instruction: + 'At the edge of the board, the Knight loses options. A Knight on a1 can only reach b3 or c2 β just 2 squares!', + tip: '"A Knight on the rim is dim."', + highlight: 'a1', + piece: 'β', + }, + { + id: '4', + title: 'The Fork', + instruction: + 'Knights excel at creating "forks" β attacking two pieces at once. Because of their L-shape, opponents often miss the threat!', + tip: 'Look for Knight forks against the King and Rook β a "royal fork".', + piece: 'β', + }, + ], + pawn: [ + { + id: '1', + title: 'The First Move', + instruction: + 'Pawns normally move one square forward, but on their first move they can advance two squares. They capture diagonally.', + tip: 'Moving two squares on the first move can help control the center faster.', + highlight: 'e2', + piece: 'β', + }, + { + id: '2', + title: 'Pawn Captures', + instruction: + 'A pawn captures diagonally one square forward. This means a pawn on e4 can capture on d5 or f5.', + tip: 'Unlike other pieces, pawns cannot capture straight forward!', + highlight: 'e4', + piece: 'β', + }, + { + id: '3', + title: 'En Passant', + instruction: + "If an opponent's pawn advances two squares and lands beside your pawn, you can capture it as if it had only moved one square. This is called en passant.", + tip: "En passant must be made immediately β you can't wait for the next move.", + piece: 'β', + }, + ], + rook: [ + { + id: '1', + title: 'Open File Strategy', + instruction: + 'Rooks are most powerful on open files β columns with no pawns blocking them. Rooks on the 7th rank are especially strong.', + tip: 'Connect your rooks by castling and clearing the back rank.', + highlight: 'a1', + piece: 'β', + }, + { + id: '2', + title: 'The 7th Rank', + instruction: + "A rook on the 7th rank attacks the opponent's pawns and restricts their king. Two rooks on the 7th rank can even deliver checkmate!", + tip: 'Rooks on the 7th rank are often worth more than a bishop or knight.', + highlight: 'a7', + piece: 'β', + }, + ], +}; + +const DEFAULT_STEPS: Step[] = [ + { + id: '1', + title: 'Introduction', + instruction: + 'This lesson will walk you through the key concepts step by step. Follow along on the board above.', + tip: 'Practice each concept before moving on for best results.', + piece: 'β', + }, +]; + +function ChessBoard({ + highlight, + piece, +}: { + highlight?: string; + piece?: string; +}) { + const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + const ranks = [8, 7, 6, 5, 4, 3, 2, 1]; + + return ( + + {/* Rank labels */} + + {ranks.map((r) => ( + + {r} + + ))} + + + {ranks.map((rank) => ( + + {files.map((file, fi) => { + const sq = `${file}${rank}`; + const isHl = sq === highlight; + return ( + + {isHl && piece && {piece}} + + ); + })} + + ))} + {/* File labels row */} + + {files.map((f) => ( + + {f} + + ))} + + + + ); +} + +export default function LessonDetailRoute() { + const { piece = 'knight' } = useLocalSearchParams<{ + piece: string; + id: string; + }>(); + const steps = STEPS_BY_PIECE[piece as string] ?? DEFAULT_STEPS; + const [step, setStep] = useState(0); + const [completed, setCompleted] = useState(false); + + const current = steps[step]; + const isLast = step === steps.length - 1; + const progress = (step + 1) / steps.length; + + const [fadeAnim] = useState(() => new Animated.Value(1)); + + const navigate = (nextStep: number) => { + Animated.sequence([ + Animated.timing(fadeAnim, { + toValue: 0, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + setStep(nextStep); + }; + + const handleNext = () => { + if (isLast) { + setCompleted(true); + } else { + navigate(step + 1); + } + }; + + const handleBack = () => { + if (step === 0) { + router.back(); + } else { + navigate(step - 1); + } + }; + + const pieceName = + (piece as string).charAt(0).toUpperCase() + (piece as string).slice(1); + + return ( + + {/* ββ Top Bar ββββββββββββββββββββββββββββββ */} + + router.back()} + style={({ pressed }) => [s.backBtn, pressed && { opacity: 0.6 }]} + > + βΉ + + + {pieceName} Lesson + + {current?.title} + + + + + {step + 1}/{steps.length} + + + + + {/* ββ Progress Bar βββββββββββββββββββββββββ */} + + + + + {/* ββ Step Dots ββββββββββββββββββββββββββββ */} + + {steps.map((_, i) => ( + + ))} + + + + {/* ββ Board ββββββββββββββββββββββββββββββββ */} + + + + + {/* ββ Content ββββββββββββββββββββββββββββββ */} + + {!completed ? ( + <> + + {current?.piece ?? 'β'} + {pieceName} + + Step {step + 1} + + + {current?.instruction} + {current?.tip && ( + + π‘ TIP + {current.tip} + + )} + > + ) : ( + + π + Lesson Complete! + + Youβve mastered the {pieceName} fundamentals. Keep practicing! + + + +50 XP + + + )} + + + + + + {/* ββ Footer Controls ββββββββββββββββββββββ */} + + {!completed ? ( + + [ + s.backButton, + pressed && { opacity: 0.7 }, + ]} + > + + {step === 0 ? 'Exit' : 'βΉ Back'} + + + [ + s.nextButton, + pressed && { opacity: 0.85 }, + ]} + > + + {isLast ? 'Finish β' : 'Next βΊ'} + + + + ) : ( + router.back()} + style={({ pressed }) => [ + s.doneButton, + pressed && { opacity: 0.85 }, + ]} + > + Back to Lessons β + + )} + + + ); +} + +const b = StyleSheet.create({ + board: { flexDirection: 'row', flex: 1 }, + labels: { + justifyContent: 'space-around', + paddingVertical: 1, + paddingRight: 4, + paddingBottom: 18, + }, + rankLabel: { + fontSize: 9, + color: palette.muted, + fontWeight: fontWeights.bold, + textAlign: 'center', + height: '12.5%' as any, + lineHeight: 0, + }, + grid: { flex: 1 }, + row: { flex: 1, flexDirection: 'row' }, + square: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + light: { backgroundColor: '#F0D9B5' }, + dark: { backgroundColor: '#B58863' }, + highlight: { backgroundColor: 'rgba(127,204,38,0.55)' }, + piece: { fontSize: 20 }, + fileRow: { flexDirection: 'row', height: 16 }, + fileLabel: { + flex: 1, + fontSize: 9, + color: palette.muted, + fontWeight: fontWeights.bold, + textAlign: 'center', + }, +}); + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.07, + shadowRadius: 10, + elevation: 3, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + + /* Top Bar */ + topBar: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + }, + backBtn: { + width: 40, + height: 40, + borderRadius: radii.pill, + backgroundColor: theme.colors.surface, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: palette.border, + }, + backArrow: { fontSize: fontSizes.heading, color: palette.ink, marginTop: -2 }, + topCenter: { flex: 1, alignItems: 'center', gap: 1 }, + topSup: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + topTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + stepBadge: { + backgroundColor: palette.ink, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + stepText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.white, + }, + + /* Progress */ + progressTrack: { + height: 4, + backgroundColor: palette.border, + marginHorizontal: spacing.lg, + borderRadius: radii.pill, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + }, + + /* Dots */ + dots: { + flexDirection: 'row', + justifyContent: 'center', + gap: spacing.xs, + paddingVertical: spacing.sm, + }, + dot: { + width: 6, + height: 6, + borderRadius: radii.pill, + backgroundColor: palette.border, + }, + dotActive: { width: 20, backgroundColor: palette.brandGreen }, + dotDone: { backgroundColor: palette.brandGreenSoft }, + + /* Board */ + scroll: { paddingHorizontal: spacing.lg, gap: spacing.md }, + boardWrap: { + width: '100%', + aspectRatio: 1, + borderRadius: radii.md, + overflow: 'hidden', + borderWidth: 1, + borderColor: palette.border, + ...SHADOW, + }, + + /* Panel */ + panel: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.lg, + gap: spacing.md, + ...SHADOW, + }, + pieceBadge: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + pieceBadgeEmoji: { fontSize: 22 }, + pieceBadgeName: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + pieceBadgePill: { + backgroundColor: `${palette.brandGreen}1A`, + paddingVertical: 2, + paddingHorizontal: spacing.xs, + borderRadius: radii.pill, + marginLeft: 'auto', + }, + pieceBadgePillText: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + instruction: { fontSize: fontSizes.body, color: palette.ink, lineHeight: 26 }, + tip: { + backgroundColor: `${palette.accentYellow}22`, + borderRadius: radii.md, + borderLeftWidth: 3, + borderLeftColor: palette.accentYellow, + padding: spacing.md, + gap: spacing.xs, + }, + tipHeader: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.ink, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + tipText: { fontSize: fontSizes.caption, color: palette.ink, lineHeight: 20 }, + + /* Completion */ + completion: { + alignItems: 'center', + gap: spacing.md, + paddingVertical: spacing.md, + }, + completionTrophy: { fontSize: 64 }, + completionTitle: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + completionBody: { + fontSize: fontSizes.body, + color: palette.gray, + textAlign: 'center', + lineHeight: 24, + }, + xpBadge: { + backgroundColor: palette.accentYellow, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.xl, + borderRadius: radii.pill, + }, + xpText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + + /* Footer */ + footer: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + paddingBottom: spacing.lg, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: palette.border, + }, + controls: { flexDirection: 'row', gap: spacing.sm }, + backButton: { + flex: 1, + backgroundColor: theme.colors.surface, + borderWidth: 1.5, + borderColor: palette.border, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + backButtonText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + nextButton: { + flex: 2, + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + nextButtonText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + doneButton: { + backgroundColor: palette.ink, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + doneButtonText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.white, + }, +}); diff --git a/app/(app)/play-game.tsx b/app/(app)/play-game.tsx new file mode 100644 index 0000000..27c15ff --- /dev/null +++ b/app/(app)/play-game.tsx @@ -0,0 +1,789 @@ +/** + * play-game.tsx β Active Chess Game Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { router, useLocalSearchParams } from 'expo-router'; +import { useEffect, useState } from 'react'; +import { + Animated, + Modal, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; + +type GameResult = 'win' | 'lose' | 'draw' | null; + +const MOVES = [ + { n: 1, w: 'e4', b: 'e5' }, + { n: 2, w: 'Nf3', b: 'Nc6' }, + { n: 3, w: 'Bb5', b: 'a6' }, + { n: 4, w: 'Ba4', b: 'Nf6' }, + { n: 5, w: 'O-O', b: 'Be7' }, + { n: 6, w: 'Re1', b: 'b5' }, + { n: 7, w: 'Bb3' }, +]; + +const DIFFICULTY_LABEL: Record = { + beginner: 'π± Beginner', + easy: 'π Easy', + medium: 'π€ Medium', + hard: 'π€ Hard', + master: 'π Master', +}; + +function ChessBoard({ playerColor }: { playerColor: string }) { + const files = + playerColor === 'white' + ? ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + : ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a']; + const ranks = + playerColor === 'white' + ? [8, 7, 6, 5, 4, 3, 2, 1] + : [1, 2, 3, 4, 5, 6, 7, 8]; + const coords = + playerColor === 'white' + ? ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + : ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a']; + + return ( + + {/* Rank labels */} + + {ranks.map((r) => ( + + {r} + + ))} + + + {ranks.map((rank) => ( + + {files.map((file, fi) => { + const isHl = + (file === 'e' && rank === 4) || (file === 'e' && rank === 5); + return ( + + ); + })} + + ))} + {/* File labels */} + + {coords.map((f) => ( + + {f} + + ))} + + + + ); +} + +function PlayerRow({ + label, + emoji, + isActive, + time, + captured, +}: { + label: string; + emoji: string; + isActive: boolean; + time: string; + captured?: string; +}) { + return ( + + + {emoji} + + + + {label} + + {captured ? ( + {captured} + ) : ( + β + )} + + {isActive && } + + {time} + + + ); +} + +function GameOverModal({ + result, + onNewGame, + onExit, +}: { + result: GameResult; + onNewGame: () => void; + onExit: () => void; +}) { + const [scale] = useState(() => new Animated.Value(0.85)); + + useEffect(() => { + if (result === null) { + scale.setValue(0.85); + return; + } + + const animation = Animated.spring(scale, { + toValue: 1, + useNativeDriver: true, + speed: 20, + bounciness: 14, + }); + animation.start(); + + return () => animation.stop(); + }, [result, scale]); + + const meta = { + win: { + emoji: 'π', + title: 'You Win!', + body: 'Excellent play! You defeated the computer.', + xp: '+50 XP', + }, + lose: { + emoji: 'π', + title: 'You Lose', + body: "Don't give up β every loss is a lesson!", + xp: '+10 XP', + }, + draw: { + emoji: 'π€', + title: 'Draw!', + body: 'A hard-fought game. Well played by both sides.', + xp: '+20 XP', + }, + }; + + const m = result ? meta[result] : null; + + return ( + + + + {m && ( + <> + {m.emoji} + {m.title} + {m.body} + + {m.xp} + + + [ + mo.btn, + mo.btnPrimary, + pressed && { opacity: 0.85 }, + ]} + > + + Play Again + + + [ + mo.btn, + mo.btnSecondary, + pressed && { opacity: 0.7 }, + ]} + > + Exit + + + > + )} + + + + ); +} + +export default function PlayGameRoute() { + const { color = 'white', difficulty = 'beginner' } = useLocalSearchParams<{ + color: string; + difficulty: string; + }>(); + const [tutorOpen, setTutorOpen] = useState(true); + const [gameResult, setGameResult] = useState(null); + const isPlayerTurn = true; + + const diffLabel = DIFFICULTY_LABEL[difficulty as string] ?? difficulty; + + return ( + + {/* ββ Dark Header ββββββββββββββββββββββββββ */} + + router.back()} + style={({ pressed }) => [ + s.headerBackBtn, + pressed && { opacity: 0.6 }, + ]} + > + βΉ + + + vs Computer + {diffLabel} + + + +0.3 + + + + + {/* ββ Opponent Row βββββββββββββββββββββββββ */} + + + {/* ββ Board ββββββββββββββββββββββββββββββββ */} + + + + + {/* ββ Player Row βββββββββββββββββββββββββββ */} + + + {/* ββ Tutor Panel ββββββββββββββββββββββββββ */} + + setTutorOpen((v) => !v)} + style={s.tutorHeader} + > + + + π + + Tutor + + +0.3 + + + {tutorOpen ? 'β' : 'β'} + + {tutorOpen && ( + + Ruy Lopez β Main Line + + Consider castling kingside to protect your king before launching + an attack. Your d-pawn controls the center. + + + π‘ Best move: + + d4 + + + + )} + + + {/* ββ Move History βββββββββββββββββββββββββ */} + + Move History + + {MOVES.map((m) => ( + + {m.n}. + + {m.w} + + {m.b && ( + + + {m.b} + + + )} + + ))} + + + + + + + {/* ββ Controls Footer ββββββββββββββββββββββ */} + + {[ + { + icon: 'β©', + label: 'Undo', + onPress: () => {}, + variant: 'normal' as const, + }, + { + icon: 'β ', + label: 'Flip', + onPress: () => {}, + variant: 'normal' as const, + }, + { + icon: 'π€', + label: 'Draw', + onPress: () => setGameResult('draw'), + variant: 'normal' as const, + }, + { + icon: 'π³', + label: 'Resign', + onPress: () => setGameResult('lose'), + variant: 'danger' as const, + }, + ].map((item) => ( + [ + s.ctrlBtn, + item.variant === 'danger' && s.ctrlBtnDanger, + pressed && s.ctrlBtnPressed, + ]} + > + {item.icon} + + {item.label} + + + ))} + + + { + setGameResult(null); + router.back(); + }} + onExit={() => { + setGameResult(null); + router.back(); + }} + /> + + ); +} + +/* ββ Board styles ββββββββββββββββββββββββββ */ +const b = StyleSheet.create({ + wrap: { width: '100%', aspectRatio: 1, flexDirection: 'row' }, + rankLabels: { + justifyContent: 'space-around', + paddingRight: 4, + paddingBottom: 18, + }, + rankLabel: { + fontSize: 9, + color: 'rgba(255,255,255,0.5)', + fontWeight: fontWeights.bold, + textAlign: 'center', + }, + row: { flex: 1, flexDirection: 'row' }, + square: { flex: 1 }, + light: { backgroundColor: '#F0D9B5' }, + dark: { backgroundColor: '#B58863' }, + highlighted: { backgroundColor: 'rgba(127,204,38,0.5)' }, + fileRow: { flexDirection: 'row', height: 18 }, + fileLabel: { + flex: 1, + fontSize: 9, + color: 'rgba(255,255,255,0.5)', + fontWeight: fontWeights.bold, + textAlign: 'center', + paddingTop: 4, + }, +}); + +/* ββ Player row styles βββββββββββββββββββββ */ +const p = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + borderRadius: radii.md, + }, + rowActive: { backgroundColor: `${palette.brandGreen}18` }, + avatar: { + width: 40, + height: 40, + borderRadius: radii.pill, + backgroundColor: theme.colors.surface, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1.5, + borderColor: palette.border, + }, + avatarActive: { borderColor: palette.brandGreen }, + info: { flex: 1, gap: 1 }, + name: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + captured: { + fontSize: fontSizes.caption, + color: palette.muted, + letterSpacing: 2, + }, + capturedEmpty: { fontSize: fontSizes.caption, color: palette.border }, + activeDot: { + width: 7, + height: 7, + borderRadius: radii.pill, + backgroundColor: palette.brandGreen, + }, + timer: { + backgroundColor: palette.ink, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + borderRadius: radii.sm, + minWidth: 64, + alignItems: 'center', + }, + timerActive: { backgroundColor: palette.brandGreen }, + timerText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: 'rgba(255,255,255,0.45)', + letterSpacing: 1, + }, + timerTextActive: { color: palette.ink }, +}); + +/* ββ Modal styles ββββββββββββββββββββββββββ */ +const mo = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(31,31,31,0.65)', + alignItems: 'center', + justifyContent: 'center', + padding: spacing.lg, + }, + card: { + width: '100%', + maxWidth: 360, + backgroundColor: theme.colors.surfaceStrong, + borderRadius: radii.xl, + padding: spacing.xl, + alignItems: 'center', + gap: spacing.md, + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.2, + shadowRadius: 24, + elevation: 10, + }, + emoji: { fontSize: 60 }, + title: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: -0.5, + }, + body: { + fontSize: fontSizes.body, + color: palette.gray, + textAlign: 'center', + lineHeight: 24, + }, + xpPill: { + backgroundColor: `${palette.accentYellow}33`, + borderWidth: 1, + borderColor: palette.accentYellow, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.lg, + borderRadius: radii.pill, + }, + xpText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + buttons: { width: '100%', gap: spacing.sm }, + btn: { + width: '100%', + minHeight: 52, + borderRadius: radii.pill, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1.5, + }, + btnPrimary: { + backgroundColor: palette.brandGreen, + borderColor: palette.brandGreen, + }, + btnSecondary: { backgroundColor: 'transparent', borderColor: palette.border }, + btnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + btnTextPrimary: { color: palette.ink }, +}); + +/* ββ Screen styles βββββββββββββββββββββββββ */ +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + + /* Header */ + header: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + backgroundColor: palette.ink, + }, + headerBackBtn: { + width: 36, + height: 36, + borderRadius: radii.pill, + backgroundColor: 'rgba(255,255,255,0.1)', + alignItems: 'center', + justifyContent: 'center', + }, + headerBackArrow: { + fontSize: fontSizes.heading, + color: palette.white, + marginTop: -1, + }, + headerCenter: { flex: 1, alignItems: 'center', gap: 1 }, + headerTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.white, + }, + headerSub: { + fontSize: fontSizes.caption - 1, + color: 'rgba(255,255,255,0.5)', + }, + evalChip: { + backgroundColor: `${palette.brandGreen}22`, + borderWidth: 1, + borderColor: `${palette.brandGreen}50`, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + }, + evalText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + + /* Scroll */ + scroll: { + paddingHorizontal: spacing.md, + paddingTop: spacing.sm, + gap: spacing.sm, + }, + boardWrap: { + borderRadius: radii.sm, + overflow: 'hidden', + backgroundColor: palette.ink, + }, + + /* Tutor */ + tutorCard: { + backgroundColor: theme.colors.surface, + borderRadius: radii.md, + borderWidth: 1, + borderColor: palette.border, + overflow: 'hidden', + }, + tutorHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + padding: spacing.md, + backgroundColor: `${palette.accentYellow}18`, + }, + tutorLeft: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + tutorIconWrap: { + width: 30, + height: 30, + borderRadius: radii.sm, + backgroundColor: `${palette.accentYellow}30`, + alignItems: 'center', + justifyContent: 'center', + }, + tutorTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + tutorEvalBadge: { + backgroundColor: `${palette.brandGreen}1A`, + paddingVertical: 2, + paddingHorizontal: spacing.xs, + borderRadius: radii.pill, + }, + tutorEvalText: { + fontSize: 10, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + tutorChevron: { fontSize: fontSizes.label, color: palette.muted }, + tutorBody: { padding: spacing.md, gap: spacing.sm }, + tutorOpening: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1, + }, + tutorTip: { fontSize: fontSizes.caption, color: palette.ink, lineHeight: 20 }, + tutorSuggestion: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + marginTop: spacing.xxs, + }, + tutorSugLabel: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.semibold, + color: palette.muted, + }, + tutorSugMove: { + backgroundColor: palette.ink, + paddingVertical: 3, + paddingHorizontal: spacing.sm, + borderRadius: radii.sm, + }, + tutorSugMoveText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.white, + }, + + /* Move history */ + movesSection: { gap: spacing.xs }, + movesLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + movesList: { + gap: spacing.xs, + paddingVertical: spacing.xs, + alignItems: 'center', + backgroundColor: theme.colors.surface, + borderRadius: radii.sm, + paddingHorizontal: spacing.sm, + borderWidth: 1, + borderColor: palette.border, + }, + moveGroup: { flexDirection: 'row', alignItems: 'center', gap: spacing.xxs }, + moveNum: { + fontSize: fontSizes.caption, + color: palette.muted, + minWidth: 20, + textAlign: 'right', + }, + moveChip: { + paddingVertical: 3, + paddingHorizontal: spacing.xs, + backgroundColor: theme.colors.background, + borderRadius: radii.sm, + borderWidth: 1, + borderColor: palette.border, + }, + moveChipBlack: { backgroundColor: palette.ink, borderColor: palette.ink }, + moveChipText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + + /* Footer */ + footer: { + flexDirection: 'row', + gap: spacing.xs, + paddingHorizontal: spacing.md, + paddingVertical: spacing.md, + paddingBottom: spacing.lg, + backgroundColor: palette.ink, + }, + ctrlBtn: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: spacing.xxs, + paddingVertical: spacing.sm, + backgroundColor: 'rgba(255,255,255,0.07)', + borderRadius: radii.md, + minHeight: 56, + }, + ctrlBtnDanger: { backgroundColor: 'rgba(214,69,69,0.15)' }, + ctrlBtnPressed: { opacity: 0.7 }, + ctrlLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: 'rgba(255,255,255,0.45)', + textTransform: 'uppercase', + letterSpacing: 0.8, + }, + ctrlLabelDanger: { color: palette.error }, +}); diff --git a/app/(app)/puzzles.tsx b/app/(app)/puzzles.tsx new file mode 100644 index 0000000..fc286d9 --- /dev/null +++ b/app/(app)/puzzles.tsx @@ -0,0 +1,579 @@ +/** + * puzzles.tsx β Daily Puzzles Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { useState } from 'react'; +import { + Animated, + Pressable, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; + +type PuzzleState = 'solving' | 'correct' | 'incorrect'; + +type Puzzle = { + id: number; + rating: number; + themes: string[]; + hint: string; + description: string; + solution: string; +}; + +const PUZZLE: Puzzle = { + id: 4821, + rating: 1287, + themes: ['Fork', 'Knight', 'Intermediate'], + hint: 'Look for a Knight move that attacks two pieces at once.', + description: 'White to move. Find the Knight fork that wins material.', + solution: 'Nd5 β forks the Queen on c7 and Rook on e7', +}; + +function ChessBoard({ state }: { state: PuzzleState }) { + const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + const ranks = [8, 7, 6, 5, 4, 3, 2, 1]; + const overlayColor = + state === 'correct' + ? 'rgba(127,204,38,0.18)' + : state === 'incorrect' + ? 'rgba(214,69,69,0.18)' + : null; + + return ( + + {/* Grid */} + {ranks.map((rank) => ( + + {files.map((file, fi) => ( + + ))} + + ))} + + {/* Result overlay */} + {overlayColor && ( + + {state === 'correct' ? 'β' : 'β'} + + )} + + {/* Turn indicator */} + + + White to move + + + ); +} + +export default function PuzzlesRoute() { + const [state, setState] = useState('solving'); + const [hintShown, setHintShown] = useState(false); + const [streak, setStreak] = useState(5); + const [solutionShown, setSolutionShown] = useState(false); + + const [resultScale] = useState(() => new Animated.Value(0)); + + const showResult = (newState: 'correct' | 'incorrect') => { + setState(newState); + Animated.spring(resultScale, { + toValue: 1, + useNativeDriver: true, + speed: 20, + bounciness: 12, + }).start(); + if (newState === 'correct') { + setStreak((n) => n + 1); + } else { + setStreak(0); + } + }; + + const handleNew = () => { + resultScale.setValue(0); + setState('solving'); + setHintShown(false); + setSolutionShown(false); + }; + + return ( + + {/* ββ Header βββββββββββββββββββββββββββββββ */} + + + PUZZLE + #{PUZZLE.id} + + Puzzles + + π₯ + {streak} + + + + + {/* ββ Board ββββββββββββββββββββββββββββββββ */} + + + + + {/* ββ Result Banner ββββββββββββββββββββββββ */} + {state !== 'solving' && ( + + + {state === 'correct' ? 'π' : 'π'} + + + + {state === 'correct' ? 'Brilliant!' : 'Not quite'} + + + {state === 'correct' + ? `+10 bonus XP Β· Streak: ${streak} π₯` + : 'Keep going β every attempt teaches you something.'} + + + + )} + + {/* ββ Solution (after wrong) βββββββββββββββ */} + {state === 'incorrect' && !solutionShown && ( + setSolutionShown(true)} + style={({ pressed }) => [ + s.solutionToggle, + pressed && { opacity: 0.7 }, + ]} + > + Show Solution + + )} + {state === 'incorrect' && solutionShown && ( + + β SOLUTION + {PUZZLE.solution} + + )} + + {/* ββ Info Panel βββββββββββββββββββββββββββ */} + + {/* Rating + Description */} + + + Rating + {PUZZLE.rating} + + {PUZZLE.description} + + + + + {/* Theme pills */} + + {PUZZLE.themes.map((t) => ( + + {t} + + ))} + + + + + {/* Hint */} + {hintShown ? ( + + π‘ HINT + {PUZZLE.hint} + + ) : ( + setHintShown(true)} + style={({ pressed }) => [s.hintBtn, pressed && { opacity: 0.7 }]} + > + π‘ Show Hint (β5 pts) + + )} + + + {/* ββ Dev Simulate Row βββββββββββββββββββββ */} + {state === 'solving' && ( + + + + DEV ONLY + + + + showResult('correct')} + style={({ pressed }) => [ + s.devBtn, + s.devBtnCorrect, + pressed && { opacity: 0.7 }, + ]} + > + β Correct + + showResult('incorrect')} + style={({ pressed }) => [ + s.devBtn, + s.devBtnWrong, + pressed && { opacity: 0.7 }, + ]} + > + + β Wrong + + + + + )} + + + + + {/* ββ Footer CTA βββββββββββββββββββββββββββ */} + + [s.ctaBtn, pressed && s.ctaBtnPressed]} + > + + {state === 'solving' ? 'Skip βΊ' : 'Next Puzzle βΊ'} + + + + + ); +} + +const b = StyleSheet.create({ + board: { width: '100%', aspectRatio: 1, position: 'relative' }, + row: { flex: 1, flexDirection: 'row' }, + light: { flex: 1, backgroundColor: '#F0D9B5' }, + dark: { flex: 1, backgroundColor: '#B58863' }, + overlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + alignItems: 'center', + justifyContent: 'center', + }, + overlayIcon: { fontSize: 72, opacity: 0.8 }, + turnChip: { + position: 'absolute', + bottom: spacing.sm, + right: spacing.sm, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xxs, + backgroundColor: 'rgba(0,0,0,0.45)', + borderRadius: radii.pill, + paddingVertical: 4, + paddingHorizontal: spacing.xs, + }, + turnDot: { + width: 9, + height: 9, + borderRadius: radii.pill, + backgroundColor: palette.white, + borderWidth: 1.5, + borderColor: 'rgba(0,0,0,0.3)', + }, + turnText: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.white, + textTransform: 'uppercase', + letterSpacing: 1, + }, +}); + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 2, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + + /* Header */ + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + }, + headerSup: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + headerTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + headingCenter: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + streakChip: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xxs, + backgroundColor: `${palette.accentYellow}33`, + borderWidth: 1, + borderColor: palette.accentYellow, + borderRadius: radii.pill, + paddingVertical: spacing.xxs, + paddingHorizontal: spacing.sm, + }, + streakFire: { fontSize: 15 }, + streakCount: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + + /* Board */ + scroll: { paddingHorizontal: spacing.lg, gap: spacing.md }, + boardWrap: { + borderRadius: radii.md, + overflow: 'hidden', + borderWidth: 1, + borderColor: palette.border, + ...SHADOW, + }, + + /* Result banner */ + resultBanner: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + borderRadius: radii.lg, + padding: spacing.md, + borderWidth: 1, + }, + resultBannerCorrect: { + backgroundColor: `${palette.brandGreen}1A`, + borderColor: palette.brandGreen, + }, + resultBannerWrong: { + backgroundColor: palette.errorBackground, + borderColor: palette.error, + }, + resultIcon: { fontSize: 28 }, + resultContent: { flex: 1, gap: 2 }, + resultTitle: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + resultSub: { fontSize: fontSizes.caption, color: palette.gray }, + + solutionToggle: { alignItems: 'center', paddingVertical: spacing.xs }, + solutionToggleText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.semibold, + color: palette.brandGreen, + }, + solutionCard: { + backgroundColor: `${palette.brandGreen}15`, + borderRadius: radii.md, + borderLeftWidth: 3, + borderLeftColor: palette.brandGreen, + padding: spacing.md, + gap: spacing.xs, + }, + solutionHeader: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + solutionText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + + /* Info card */ + infoCard: { + backgroundColor: theme.colors.surface, + borderRadius: radii.lg, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.md, + gap: spacing.md, + ...SHADOW, + }, + infoTop: { flexDirection: 'row', alignItems: 'center', gap: spacing.md }, + ratingBox: { + backgroundColor: theme.colors.background, + borderRadius: radii.md, + padding: spacing.md, + alignItems: 'center', + minWidth: 70, + gap: 2, + borderWidth: 1, + borderColor: palette.border, + }, + ratingLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1, + }, + ratingValue: { + fontSize: fontSizes.title, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + desc: { + flex: 1, + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + lineHeight: 22, + }, + divider: { height: 1, backgroundColor: palette.border }, + themes: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs }, + themePill: { + backgroundColor: `${palette.brandGreen}1A`, + paddingVertical: 3, + paddingHorizontal: spacing.sm, + borderRadius: radii.pill, + borderWidth: 1, + borderColor: `${palette.brandGreen}40`, + }, + themeText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.bold, + color: palette.brandGreen, + }, + hintCard: { + backgroundColor: `${palette.accentYellow}22`, + borderRadius: radii.md, + borderLeftWidth: 3, + borderLeftColor: palette.accentYellow, + padding: spacing.md, + gap: spacing.xs, + }, + hintHeader: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.ink, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + hintText: { fontSize: fontSizes.caption, color: palette.ink, lineHeight: 20 }, + hintBtn: { + backgroundColor: `${palette.accentYellow}1A`, + borderRadius: radii.md, + borderWidth: 1, + borderColor: `${palette.accentYellow}80`, + paddingVertical: spacing.sm, + alignItems: 'center', + }, + hintBtnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + + /* Dev section */ + devRow: { gap: spacing.sm }, + devDivider: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + devLine: { flex: 1, height: 1, backgroundColor: palette.border }, + devLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + devBtns: { flexDirection: 'row', gap: spacing.sm }, + devBtn: { + flex: 1, + borderRadius: radii.md, + borderWidth: 1, + paddingVertical: spacing.sm, + alignItems: 'center', + borderStyle: 'dashed', + }, + devBtnCorrect: { + borderColor: palette.brandGreen, + backgroundColor: `${palette.brandGreen}10`, + }, + devBtnWrong: { + borderColor: palette.error, + backgroundColor: palette.errorBackground, + }, + devBtnText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.semibold, + color: palette.ink, + }, + + /* Footer */ + footer: { + paddingHorizontal: spacing.lg, + paddingVertical: spacing.md, + paddingBottom: spacing.lg, + backgroundColor: theme.colors.background, + borderTopWidth: 1, + borderTopColor: palette.border, + }, + ctaBtn: { + backgroundColor: palette.brandGreen, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + ctaBtnPressed: { backgroundColor: palette.brandGreenPressed }, + ctaBtnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.ink, + }, +}); diff --git a/app/(public)/sign-in.tsx b/app/(public)/sign-in.tsx index faf63be..f3c3dfe 100644 --- a/app/(public)/sign-in.tsx +++ b/app/(public)/sign-in.tsx @@ -1,22 +1,362 @@ -import { PlaceholderScreen } from '@/components/PlaceholderScreen'; -import { Button } from '@/components/ui/Button'; +/** + * sign-in.tsx β Authentication Screen + * UI/UX design by Sandith Hewage (Y STEM and Chess) + */ + +import { useState } from 'react'; +import { + Animated, + Image, + KeyboardAvoidingView, + Platform, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { theme } from '@/design/theme'; +import { + fontSizes, + fontWeights, + palette, + radii, + spacing, +} from '@/design/tokens'; import { useSession } from '@/providers/SessionProvider'; +const chessPiecePattern = require('@/assets/images/chess-piece-pattern.png'); + export default function SignInRoute() { const { mockSignInAs } = useSession(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [usernameFocused, setUsernameFocused] = useState(false); + const [passwordFocused, setPasswordFocused] = useState(false); + const [showPassword, setShowPassword] = useState(false); + + const [btnScale] = useState(() => new Animated.Value(1)); + const pressIn = () => + Animated.spring(btnScale, { + toValue: 0.97, + useNativeDriver: true, + speed: 50, + bounciness: 4, + }).start(); + const pressOut = () => + Animated.spring(btnScale, { + toValue: 1, + useNativeDriver: true, + speed: 50, + bounciness: 4, + }).start(); return ( - - mockSignInAs('student')} variant="brand"> - Continue as student - - - mockSignInAs('mentor')} variant="secondary"> - Continue as mentor - - + + {/* ββ Patterned Background ββββββββββββββββ */} + + + + + + {/* ββ Brand Mark ββββββββββββββββββββββββββ */} + + + β + + Y STEM + and Chess + + + {/* ββ Form Card βββββββββββββββββββββββββββ */} + + + Welcome back + Sign in to continue your journey + + + + + π€ + setUsernameFocused(false)} + onChangeText={setUsername} + onFocus={() => setUsernameFocused(true)} + placeholder="Username" + placeholderTextColor={palette.muted} + returnKeyType="next" + style={s.fieldInput} + value={username} + /> + + + + π + setPasswordFocused(false)} + onChangeText={setPassword} + onFocus={() => setPasswordFocused(true)} + placeholder="Password" + placeholderTextColor={palette.muted} + returnKeyType="done" + secureTextEntry={!showPassword} + style={[s.fieldInput, { flex: 1 }]} + value={password} + /> + setShowPassword((v) => !v)} + style={s.eyeBtn} + > + {showPassword ? 'π' : 'π'} + + + + + + Forgot password? + + + + + Sign In β + + + + + New here? + + Create account + + + + + {/* ββ Dev Quick-Login βββββββββββββββββββββ */} + + + + DEV ONLY + + + + {(['student', 'mentor'] as const).map((role) => ( + mockSignInAs(role)} + style={({ pressed }) => [ + s.devBtn, + pressed && s.devBtnPressed, + ]} + > + + {role === 'student' ? 'π' : 'π'} + + + {role === 'student' ? 'Student' : 'Mentor'} + + + ))} + + + + + ); } + +const SHADOW = { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.08, + shadowRadius: 16, + elevation: 4, +} as const; + +const s = StyleSheet.create({ + safe: { flex: 1, backgroundColor: theme.colors.background }, + pattern: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + width: '100%', + height: '100%', + opacity: 0.3, + }, + patternFade: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '60%', + backgroundColor: theme.colors.background, + opacity: 0.9, + }, + kav: { flex: 1 }, + scroll: { + flexGrow: 1, + justifyContent: 'center', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.xxl, + gap: spacing.lg, + }, + + brand: { alignItems: 'center', gap: spacing.xs }, + brandCircle: { + width: 72, + height: 72, + borderRadius: radii.pill, + backgroundColor: palette.ink, + alignItems: 'center', + justifyContent: 'center', + ...SHADOW, + }, + brandIcon: { fontSize: 34 }, + brandTitle: { + fontSize: fontSizes.display, + fontWeight: fontWeights.bold, + color: palette.ink, + letterSpacing: -1, + }, + brandSub: { + fontSize: fontSizes.label, + color: palette.gray, + fontWeight: fontWeights.medium, + marginTop: -4, + }, + + card: { + backgroundColor: theme.colors.surfaceStrong, + borderRadius: radii.xl, + borderWidth: 1, + borderColor: palette.border, + padding: spacing.lg, + gap: spacing.md, + ...SHADOW, + }, + cardHeader: { gap: 2 }, + cardTitle: { + fontSize: fontSizes.heading, + fontWeight: fontWeights.bold, + color: palette.ink, + }, + cardSub: { fontSize: fontSizes.caption, color: palette.muted }, + + fields: { gap: spacing.sm }, + field: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + borderWidth: 1.5, + borderColor: palette.border, + borderRadius: radii.md, + backgroundColor: theme.colors.background, + paddingHorizontal: spacing.md, + minHeight: 52, + }, + fieldFocused: { + borderColor: palette.brandGreen, + backgroundColor: palette.backgroundSoft, + }, + fieldIcon: { fontSize: 16 }, + fieldInput: { + flex: 1, + fontSize: fontSizes.body, + color: palette.ink, + paddingVertical: spacing.sm, + }, + eyeBtn: { padding: spacing.xs }, + eyeIcon: { fontSize: 15 }, + + forgotRow: { alignSelf: 'flex-end' }, + forgotText: { + fontSize: fontSizes.caption, + color: palette.brandGreen, + fontWeight: fontWeights.semibold, + }, + + enterBtn: { + backgroundColor: palette.ink, + borderRadius: radii.pill, + minHeight: 52, + alignItems: 'center', + justifyContent: 'center', + }, + enterBtnText: { + fontSize: fontSizes.label, + fontWeight: fontWeights.bold, + color: palette.white, + letterSpacing: 0.3, + }, + + signupRow: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + signupHint: { fontSize: fontSizes.caption, color: palette.muted }, + signupLink: { + fontSize: fontSizes.caption, + color: palette.brandGreen, + fontWeight: fontWeights.bold, + }, + + dev: { gap: spacing.sm }, + devDivider: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm }, + devLine: { flex: 1, height: 1, backgroundColor: palette.border }, + devLabel: { + fontSize: 9, + fontWeight: fontWeights.bold, + color: palette.muted, + textTransform: 'uppercase', + letterSpacing: 1.5, + }, + devRow: { flexDirection: 'row', gap: spacing.sm }, + devBtn: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: spacing.xs, + borderRadius: radii.md, + borderWidth: 1, + borderColor: palette.border, + borderStyle: 'dashed', + paddingVertical: spacing.sm, + }, + devBtnPressed: { backgroundColor: palette.backgroundSoft }, + devBtnIcon: { fontSize: 15 }, + devBtnText: { + fontSize: fontSizes.caption, + fontWeight: fontWeights.semibold, + color: palette.gray, + }, +}); diff --git a/docs/screen-preview.html b/docs/screen-preview.html new file mode 100644 index 0000000..3b3b519 --- /dev/null +++ b/docs/screen-preview.html @@ -0,0 +1,2717 @@ + + + + + + Y STEM and Chess β Mobile Screen Preview + + + + + + + Y STEM and Chess + Mobile App β All Screens + design/sandith-ui-screens Β· React Native + Expo + + + + D1 β Authentication + + + + Sign In + + + + + + β + + + + β + Y STEM + and Chess Β· Sign in to continue + + + Username + student + + + Password + β’β’β’β’β’β’β’β’ + + Enter β + + Forgot Password + Β· + Sign Up + + + + + + + + + + D1 β Home + + + Home + + + 9:41βββ + + + + + DASHBOARD + + + Good morning, Alex π + + + + + + YOUR PROGRESS + + + 250 + + XP Β· Level 3 + + + + + Level 3Level 4 + + + + + + + + + 5π₯ + STREAK + + + 12π§© + PUZZLES + + + 3β + WINS + + + + + + + + + π + Learn + Knight Β· Step 3 + + + + π§© + Puzzles + Rating 1287 + + + + + + + π Home + + + πLearn + + + βPlay + + + π€Profile + + + + + + + + + + D2 β Lessons + + + + Lessons Selection + + + 9:41βββ + + + CHESS ACADEMY + Your Learning Path + + 62% mastered + + + + + + + + + + β + Knight + + + + + + β + Pawn + + + + + + β + Bishop + + + + + + β + Rook + + + + + + + + β + + What is a Knight? + 3 min Β· +20 XP + + + + β + + How Knights Move + 5 min Β· +30 XP + + + + βΆ + + Knight Forks + 7 min Β· +40 XP + + In progress + + + π + + Advanced Knight Tactics + 8 min Β· +50 XP + + + + + + + Continue: Knight Forks + + + + + π Home + + + πLearn + + + βPlay + + + π€Profile + + + + + + + + + Lesson Detail + + + 9:41βββ + + + + β + + + STEP 2 OF 4 + + + Knight Forks + + + +40 XP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + β + + + + + + + + + β + + + + + + + + + + + + + β + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + β Knight + + Step 2 + + + + The Knight on d5 can fork both the Queen on c7 and the Rook on + e7 in a single move β winning material. + + + π‘ A fork attacks two pieces at once β your opponent can only + save one! + + + + + + + + + + + + + D3 β Puzzles + + + + Solving + + + 9:41βββ + + + PUZZLE + #4821 + + + Puzzles + + π₯ 5 + + + + + + + + + + + + + + + + + + + + + + + + + + β + + + + β + + + + + + + + + + + + + + + + + β + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Rating + 1287 + + + White to move. Find the Knight fork that wins material. + + + + + Fork + Knight + Intermediate + + + π‘ Show Hint (β5 pts) + + + + + Skip βΊ + + + + + π Home + + + πLearn + + + βPlay + + + π€Profile + + + + + + + + + Correct Result + + + 9:41βββ + + + PUZZLE + #4821 + + Puzzles + π₯ 6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + β + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + β + + + + + π + + Brilliant! + +10 bonus XP Β· Streak: 6 π₯ + + + + + + Rating + 1287 + + + White to move. Find the Knight fork that wins material. + + + + + Fork + Knight + Intermediate + + + + Next Puzzle βΊ + + + π + π + β + π€ + + + + + + + + + D4 β Play vs Computer + + + + Game Setup + + + 9:41βββ + + SINGLE PLAYER + Play vs Computer + + + + + Choose your side + + + + β + White + Moves first + + + β + Black + Moves second + + + β + Random + Surprise me! + + + + + + Difficulty + + + + π± + + Beginner + Learn the basics + + + + + + + + + β + + + + π + + Easy + Gentle challenge + + + + + + + + + + + + π€ + + Medium + Real thinking + + + + + + + + + + + + + Let's Play β + + + + π + π + β + π€ + + + + + + + + Active Game + + + + + β + + + vs Computer + + + π± Beginner Β· Playing White + + + + +0.4 + + + + + + + π€ + + + Computer + ββ captured + + 3:42 + + + + + + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + β + + + + β + + + β + + + β + + + + + + + β + + + + + + + + + + + β + + + + + + + + + β + + + + + + + + + + + + + + β + + + β + + + β + + + β + + + + β + + + β + + + β + + + β + + + β + + + + β + + + β + + + + β + + + β + + + + + + + + A + + You (White) + Move 5 + + 4:15 + + + + + + π‘ + Tutor Tip + +0.4 + + + Italian Game + + Good bishop development! Consider castling soon to protect + your king. + + + + + + + + + + + + + + Profile + + + Profile + + + 9:41βββ + + + + A + + Alex Chen + + π Student + + 250 XP + Β· + Level 3 + + + + + + + 5 + π Lessons + + + 12 + π§© Puzzles + + + 3 + β Wins + + + 5 + π₯ Streak + + + + + + + Badges + + 5/9 earned + + + + π±First Lesson + + + π§©Puzzle Solver + + + βFirst Win + + + π₯5-Day Streak + + + πQuick Learner + + + πChampion + + + + + + + βοΈ + + Edit Profile + Name, avatar, bio + + βΊ + + + π + + Notifications + Alerts & reminders + + βΊ + + + β + + Help & Support + FAQs & contact us + + βΊ + + + + + Sign out + + + + π + π + β + + π€ + + + + + + + + + + + diff --git a/test/navigation.test.tsx b/test/navigation.test.tsx index 52b197d..6b9bd83 100644 --- a/test/navigation.test.tsx +++ b/test/navigation.test.tsx @@ -13,7 +13,7 @@ async function renderAnonymous(initialUrl = '/') { }); await waitFor(() => { - expect(screen.getByText('Sign in')).toBeOnTheScreen(); + expect(screen.getByText('Welcome back')).toBeOnTheScreen(); }); return renderedRouter; @@ -24,7 +24,7 @@ describe('session-aware navigation', () => { const renderedRouter = await renderAnonymous('/mentor'); expect(renderedRouter.getPathname()).toBe('/sign-in'); - expect(screen.getByText('Sign in')).toBeOnTheScreen(); + expect(screen.getByText('Welcome back')).toBeOnTheScreen(); }); it('allows a student into the app but denies the mentor route', async () => { @@ -38,9 +38,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); expect( @@ -55,9 +53,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); }); @@ -72,9 +68,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Mentor (mentor).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); fireEvent.press( @@ -102,9 +96,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); act(() => { @@ -113,9 +105,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); }); @@ -130,9 +120,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Quick Actions')).toBeOnTheScreen(); }); act(() => { @@ -141,9 +129,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/profile'); - expect( - screen.getByText('Signed in as Mock Student (student).'), - ).toBeOnTheScreen(); + expect(screen.getByText('Mock Student')).toBeOnTheScreen(); }); fireEvent.press( @@ -154,7 +140,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/sign-in'); - expect(screen.getByText('Sign in')).toBeOnTheScreen(); + expect(screen.getByText('Welcome back')).toBeOnTheScreen(); }); if (router.canGoBack()) { @@ -164,7 +150,7 @@ describe('session-aware navigation', () => { await waitFor(() => { expect(renderedRouter.getPathname()).toBe('/sign-in'); - expect(screen.getByText('Sign in')).toBeOnTheScreen(); + expect(screen.getByText('Welcome back')).toBeOnTheScreen(); }); } });
Mobile App β All Screens
and Chess Β· Sign in to continue