diff --git a/apps/mobile/app/(app)/(tabs)/_layout.tsx b/apps/mobile/app/(app)/(tabs)/_layout.tsx new file mode 100644 index 0000000..c83e927 --- /dev/null +++ b/apps/mobile/app/(app)/(tabs)/_layout.tsx @@ -0,0 +1,16 @@ +import { Tabs } from 'expo-router'; + +export default function TabsLayout() { + return ( + + + + + + ); +} diff --git a/apps/mobile/app/(app)/(tabs)/index.tsx b/apps/mobile/app/(app)/(tabs)/index.tsx new file mode 100644 index 0000000..5bee0d9 --- /dev/null +++ b/apps/mobile/app/(app)/(tabs)/index.tsx @@ -0,0 +1,55 @@ +import { Link } from 'expo-router'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function HomeScreen() { + return ( + + Mobile Foundation + Signed-in app shell. + + Tabs, routing, environment-aware API helpers, and session context are in place for + report and auth flows. + + + Open reports tab + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + backgroundColor: '#fffaf2', + }, + eyebrow: { + textTransform: 'uppercase', + letterSpacing: 2, + color: '#2f5d50', + marginBottom: 12, + fontWeight: '700', + }, + title: { + fontSize: 34, + fontWeight: '700', + color: '#112219', + }, + copy: { + marginTop: 16, + color: '#405149', + lineHeight: 22, + }, + secondaryButton: { + marginTop: 24, + borderColor: '#d9d0bf', + borderWidth: 1, + color: '#112219', + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 999, + overflow: 'hidden', + fontWeight: '700', + }, +}); diff --git a/apps/mobile/app/(app)/(tabs)/reports.tsx b/apps/mobile/app/(app)/(tabs)/reports.tsx new file mode 100644 index 0000000..8e7f013 --- /dev/null +++ b/apps/mobile/app/(app)/(tabs)/reports.tsx @@ -0,0 +1,32 @@ +import { StyleSheet, Text, View } from 'react-native'; + +export default function ReportsPlaceholderScreen() { + return ( + + Reports tab placeholder + + This tab is reserved for mobile report submission and history screens in the next + issue batches. + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + backgroundColor: '#fff', + }, + title: { + fontSize: 28, + fontWeight: '700', + color: '#112219', + }, + copy: { + marginTop: 12, + color: '#51615a', + lineHeight: 22, + }, +}); diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx new file mode 100644 index 0000000..f116f1f --- /dev/null +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -0,0 +1,38 @@ +import { Link } from 'expo-router'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function SettingsPlaceholderScreen() { + return ( + + Settings placeholder + Session controls and profile settings will land here. + + Return to auth shell + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + backgroundColor: '#fff', + }, + title: { + fontSize: 28, + fontWeight: '700', + color: '#112219', + }, + copy: { + marginTop: 12, + color: '#51615a', + lineHeight: 22, + }, + link: { + marginTop: 20, + color: '#1f4d3f', + fontWeight: '700', + }, +}); diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx new file mode 100644 index 0000000..9ebb11e --- /dev/null +++ b/apps/mobile/app/(app)/_layout.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; + +export default function AppLayout() { + return ( + + ); +} diff --git a/apps/mobile/app/(auth)/_layout.tsx b/apps/mobile/app/(auth)/_layout.tsx new file mode 100644 index 0000000..149c586 --- /dev/null +++ b/apps/mobile/app/(auth)/_layout.tsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; + +export default function AuthLayout() { + return ( + + ); +} diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx new file mode 100644 index 0000000..8fc7753 --- /dev/null +++ b/apps/mobile/app/(auth)/login.tsx @@ -0,0 +1,53 @@ +import { Link } from 'expo-router'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function LoginScreen() { + return ( + + Sidewalk Mobile + Auth shell ready. + + This route is the foundation for OTP login and session restore in the next issue batch. + + + Continue to app shell + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + padding: 24, + backgroundColor: '#f4efe6', + }, + eyebrow: { + textTransform: 'uppercase', + letterSpacing: 2, + color: '#2f5d50', + marginBottom: 12, + fontWeight: '700', + }, + title: { + fontSize: 36, + fontWeight: '700', + color: '#112219', + }, + copy: { + marginTop: 16, + color: '#405149', + lineHeight: 22, + }, + primaryButton: { + marginTop: 28, + backgroundColor: '#1f4d3f', + color: '#f8fff8', + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 999, + overflow: 'hidden', + fontWeight: '700', + }, +}); diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index d2a8b0b..6bd59dd 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -1,5 +1,14 @@ -import { Stack } from "expo-router"; +import { Stack } from 'expo-router'; +import { SessionProvider } from './providers/session-provider'; export default function RootLayout() { - return ; + return ( + + + + ); } diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index 79dcfdc..73d43ab 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -42,22 +42,3 @@ export default function App() { ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 20, - }, - status: { - fontSize: 16, - textAlign: 'center', - color: '#333', - }, -}); diff --git a/apps/mobile/app/lib/api.ts b/apps/mobile/app/lib/api.ts new file mode 100644 index 0000000..24f5158 --- /dev/null +++ b/apps/mobile/app/lib/api.ts @@ -0,0 +1,19 @@ +import Constants from 'expo-constants'; + +const configuredUrl = + Constants.expoConfig?.extra?.apiBaseUrl ?? + process.env.EXPO_PUBLIC_API_BASE_URL ?? + 'http://localhost:5001'; + +export const apiBaseUrl = configuredUrl.replace(/\/+$/, ''); + +export const apiFetch = async (path: string, init?: RequestInit): Promise => { + const response = await fetch(`${apiBaseUrl}${path}`, init); + const payload = (await response.json()) as T & { error?: { message?: string } }; + + if (!response.ok) { + throw new Error(payload.error?.message ?? 'Request failed'); + } + + return payload; +}; diff --git a/apps/mobile/app/providers/session-provider.tsx b/apps/mobile/app/providers/session-provider.tsx new file mode 100644 index 0000000..a1c417f --- /dev/null +++ b/apps/mobile/app/providers/session-provider.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { createContext, ReactNode, useContext, useMemo, useState } from 'react'; + +type SessionState = { + accessToken: string | null; + setAccessToken: (token: string | null) => void; +}; + +const SessionContext = createContext(null); + +export function SessionProvider({ children }: Readonly<{ children: ReactNode }>) { + const [accessToken, setAccessToken] = useState(null); + const value = useMemo( + () => ({ + accessToken, + setAccessToken, + }), + [accessToken], + ); + + return {children}; +} + +export const useSession = () => { + const context = useContext(SessionContext); + if (!context) { + throw new Error('useSession must be used within SessionProvider'); + } + + return context; +}; diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..8433aee --- /dev/null +++ b/apps/web/app/dashboard/admin/page.tsx @@ -0,0 +1,20 @@ +import { AdminQueue } from './queue'; + +export default function AdminDashboardPage() { + return ( +
+
+

Admin Queue

+

Triage incoming reports.

+

+ Filter recent reports, review integrity state, and anchor a status update from a + single moderation screen. +

+
+ +
+ +
+
+ ); +} diff --git a/apps/web/app/dashboard/admin/queue.tsx b/apps/web/app/dashboard/admin/queue.tsx new file mode 100644 index 0000000..ab2d76a --- /dev/null +++ b/apps/web/app/dashboard/admin/queue.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { FormEvent, useEffect, useState } from 'react'; +import { authenticatedJsonFetch } from '../../lib/auth-fetch'; + +type QueueReport = { + id: string; + title: string; + category: string; + status: string; + anchor_status: string; + integrity_flag: string; +}; + +type QueueResponse = { + data: QueueReport[]; +}; + +const nextStatuses = ['ACKNOWLEDGED', 'RESOLVED', 'REJECTED', 'ESCALATED']; + +export function AdminQueue() { + const [reports, setReports] = useState([]); + const [selectedReportId, setSelectedReportId] = useState(''); + const [status, setStatus] = useState('ACKNOWLEDGED'); + const [stellarTxHash, setStellarTxHash] = useState(''); + const [evidence, setEvidence] = useState(''); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + const loadQueue = async () => { + try { + const payload = await authenticatedJsonFetch('/api/reports?page=1&pageSize=10'); + if (!cancelled) { + setReports(payload.data); + setSelectedReportId(payload.data[0]?.id ?? ''); + } + } catch (loadError) { + if (!cancelled) { + setError(loadError instanceof Error ? loadError.message : 'Unable to load moderation queue'); + } + } + }; + + void loadQueue(); + + return () => { + cancelled = true; + }; + }, []); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setError(null); + setMessage(null); + + try { + await authenticatedJsonFetch('/api/reports/status', { + method: 'POST', + body: JSON.stringify({ + originalTxHash: stellarTxHash, + status, + evidence: evidence || undefined, + }), + }); + setMessage('Status update anchored successfully.'); + } catch (submitError) { + setError(submitError instanceof Error ? submitError.message : 'Unable to submit status update'); + } + }; + + return ( + <> +
+ {reports.map((report) => ( +
+

{report.category}

+

{report.title}

+

+ {report.status} ยท {report.anchor_status} +

+ {report.integrity_flag !== 'NORMAL' ? ( +

Integrity flag: {report.integrity_flag}

+ ) : null} + +
+ ))} +
+ +
+
+ + + +