Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/client/app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"expo": {
"name": "client",
"name": "CMUnify",
"slug": "client",
"version": "1.0.0",
"orientation": "portrait",
Expand Down Expand Up @@ -33,9 +33,9 @@
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"backgroundColor": "#FAFAF9",
"dark": {
"backgroundColor": "#000000"
"backgroundColor": "#0C0E12"
}
}
]
Expand Down
84 changes: 84 additions & 0 deletions apps/client/app/+html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ScrollViewStyleReset } from 'expo-router/html';
import { type PropsWithChildren } from 'react';

/**
* Custom HTML shell for the static web export (expo-router +html).
*
* Carries everything the PWA needs: manifest, icons, iOS standalone tags,
* theme-color for both color schemes, safe-area-aware viewport, mobile
* polish CSS (no tap highlight, no overscroll bounce, no iOS text inflation),
* and service-worker registration.
*/
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<title>CMUnify</title>
<meta
name="description"
content="Every CMU event from Slack, Discord, and campus feeds in one calendar."
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"
/>

{/* PWA */}
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#FAFAF9" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0C0E12" />
<meta name="application-name" content="CMUnify" />

{/* iOS installed-app behavior */}
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="CMUnify" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

<ScrollViewStyleReset />

{/* Mobile polish: paint the root in the right scheme, kill tap
flashes and rubber-band overscroll, respect notches */}
<style
dangerouslySetInnerHTML={{
__html: `
html, body { height: 100%; }
body {
overflow: hidden;
background-color: #FAFAF9;
overscroll-behavior: none;
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
touch-action: manipulation;
padding: env(safe-area-inset-top) 0 0 0;
}
@media (prefers-color-scheme: dark) {
body { background-color: #0C0E12; }
}
`,
}}
/>

{/* Service worker: offline shell + cached assets */}
<script
dangerouslySetInnerHTML={{
__html: `
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js').catch(function () {});
});
}
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
35 changes: 20 additions & 15 deletions apps/client/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import 'react-native-reanimated';

import { AuthProvider } from '@/contexts/AuthContext';
Expand Down Expand Up @@ -37,20 +38,24 @@ function ThemedStack() {

export default function RootLayout() {
return (
<DevModeProvider>
<GoogleAuthProvider>
<AuthProvider>
<GoogleCalendarProvider>
<SettingsProvider>
<EventsProvider>
<SlackProvider>
<ThemedStack />
</SlackProvider>
</EventsProvider>
</SettingsProvider>
</GoogleCalendarProvider>
</AuthProvider>
</GoogleAuthProvider>
</DevModeProvider>
// SafeAreaProvider lets the tab bar and headers respect notches and the
// home indicator when the PWA is installed to a phone's home screen
<SafeAreaProvider>
<DevModeProvider>
<GoogleAuthProvider>
<AuthProvider>
<GoogleCalendarProvider>
<SettingsProvider>
<EventsProvider>
<SlackProvider>
<ThemedStack />
</SlackProvider>
</EventsProvider>
</SettingsProvider>
</GoogleCalendarProvider>
</AuthProvider>
</GoogleAuthProvider>
</DevModeProvider>
</SafeAreaProvider>
);
}
Binary file modified apps/client/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/client/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/client/public/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/client/public/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/client/public/icons/icon-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions apps/client/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "CMUnify — Every campus event, one place",
"short_name": "CMUnify",
"description": "Every CMU event from Slack, Discord, and campus feeds in one calendar, with recommendations that learn what you actually go to.",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#FAFAF9",
"theme_color": "#FAFAF9",
"categories": ["education", "social", "productivity"],
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icons/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Calendar",
"url": "/calendar",
"icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }]
},
{
"name": "Find events",
"url": "/find",
"icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }]
},
{
"name": "Freshman Guide",
"url": "/resources",
"icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }]
}
]
}
101 changes: 101 additions & 0 deletions apps/client/public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* CMUnify service worker.
*
* Strategy:
* - Hashed build assets (/_expo/static/...) are immutable -> cache-first
* - Navigations -> network-first, falling back to the cached app shell so
* the app opens offline after the first visit
* - Other same-origin GETs (icons, manifest) -> stale-while-revalidate
*
* Bump CACHE_VERSION to invalidate old caches on deploy.
*/
const CACHE_VERSION = 'cmunify-v1';
const SHELL_URLS = ['/', '/manifest.json', '/icons/icon-192.png', '/icons/icon-512.png'];

self.addEventListener('install', (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_VERSION);
await cache.addAll(SHELL_URLS);
// The page's first-load requests happen before this worker controls
// it, so the hashed bundles never hit the fetch handler. Parse the
// shell HTML here and precache every /_expo asset it references so
// the app works offline after a single visit.
try {
const response = await fetch('/');
const html = await response.text();
const assets = [...html.matchAll(/(?:src|href)="(\/_expo\/[^"]+)"/g)].map((m) => m[1]);
if (assets.length) await cache.addAll([...new Set(assets)]);
} catch {
// Offline install — runtime caching will fill in later
}
await self.skipWaiting();
})()
);
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(keys.filter((key) => key !== CACHE_VERSION).map((key) => caches.delete(key)))
)
.then(() => self.clients.claim())
);
});

self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;

const url = new URL(request.url);
if (url.origin !== self.location.origin) return; // never touch cross-origin (Supabase, Google)

// Immutable hashed bundles: cache-first
if (url.pathname.startsWith('/_expo/static/')) {
event.respondWith(
caches.open(CACHE_VERSION).then(async (cache) => {
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) cache.put(request, response.clone());
return response;
})
);
return;
}

// Navigations: network-first with shell fallback for offline
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const copy = response.clone();
caches.open(CACHE_VERSION).then((cache) => cache.put(request, copy));
}
return response;
})
.catch(async () => {
const cache = await caches.open(CACHE_VERSION);
return (await cache.match(request)) || (await cache.match('/'));
})
);
return;
}

// Everything else same-origin: stale-while-revalidate
event.respondWith(
caches.open(CACHE_VERSION).then(async (cache) => {
const cached = await cache.match(request);
const network = fetch(request)
.then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
})
.catch(() => cached);
return cached || network;
})
);
});
Loading