Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fa6c5e4
feat(website): replace the demo with links into the web app
Teksiak Oct 4, 2026
6686cda
feat(mobile): add a web guest mode on an anonymous Supabase account
Teksiak Oct 4, 2026
2c1b10c
feat(mobile): give the UI kit web hover, focus and dialog behaviour
Teksiak Oct 4, 2026
3de50d3
feat(mobile): lay out the calendar and plan history for desktop web
Teksiak Oct 4, 2026
0034e8f
feat(mobile): make the coach chat an embeddable panel for the web
Teksiak Oct 4, 2026
d6b9b41
feat(mobile): lay out You, profile pages and settings for desktop web
Teksiak Oct 4, 2026
bd2d35d
feat(mobile): add the desktop web shell with sidebar and coach dock
Teksiak Oct 4, 2026
739c1d3
feat(mobile): turn onboarding into a desktop wizard on the web
Teksiak Oct 4, 2026
cc5df9c
fix(mobile): open See week from the coach dock without stacking tabs
Teksiak Oct 4, 2026
7c16226
fix(mobile): keep the page styles and icon in the single-page web export
Teksiak Oct 4, 2026
71df3be
feat(mobile): lay out sessions, logging and the gym runner for deskto…
Teksiak Oct 4, 2026
5027c02
feat(mobile): turn Today into a desktop dashboard on the web
Teksiak Oct 4, 2026
564c900
ci: build and deploy the landing page and the web app to GitHub Pages
Teksiak Oct 4, 2026
05f947c
docs: describe the web app, guest mode and the removed website demo
Teksiak Oct 4, 2026
c6d12b6
chore: add launch configurations for the web app in mock mode
Teksiak Oct 4, 2026
ab9af5c
build: preview the GitHub Pages site locally with deep links
Teksiak Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
63 changes: 57 additions & 6 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,91 @@
{
"name": "design",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "4800", "--directory", "design"],
"runtimeArgs": [
"-m",
"http.server",
"4800",
"--directory",
"design"
],
"port": 4800
},
{
"name": "design-any-port",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "exec python3 -m http.server \"$PORT\" --directory design"],
"runtimeArgs": [
"-c",
"exec python3 -m http.server \"$PORT\" --directory design"
],
"port": 4801,
"autoPort": true
},
{
"name": "website",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev:website"],
"runtimeArgs": [
"run",
"dev:website"
],
"port": 4810
},
{
"name": "mobile-web",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "cd apps/mobile && exec npx expo start --web --port 8081"],
"runtimeArgs": [
"-c",
"cd apps/mobile && exec npx expo start --web --port 8081"
],
"port": 8081
},
{
"name": "mobile-web-any-port",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "cd apps/mobile && exec npx expo start --web --port \"$PORT\""],
"runtimeArgs": [
"-c",
"cd apps/mobile && exec npx expo start --web --port \"$PORT\""
],
"port": 8082,
"autoPort": true
},
{
"name": "mobile-ios",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "cd apps/mobile && exec npx expo start --ios --port 8081"],
"runtimeArgs": [
"-c",
"cd apps/mobile && exec npx expo start --ios --port 8081"
],
"port": 8081
},
{
"name": "mobile-web-mock",
"runtimeExecutable": "sh",
"runtimeArgs": [
"-c",
"cd apps/mobile && EXPO_PUBLIC_API_MODE=mock exec npx expo start --web --port \"$PORT\""
],
"port": 8090,
"autoPort": true
},
{
"name": "mobile-web-mock-watch",
"runtimeExecutable": "sh",
"runtimeArgs": [
"-c",
"cd apps/mobile && EXPO_PUBLIC_API_MODE=mock exec npx expo start --web --port \"$PORT\""
],
"port": 8093,
"autoPort": true
},
{
"name": "pages-preview",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"preview:pages"
],
"port": 4830,
"autoPort": true
}
]
}
63 changes: 45 additions & 18 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,38 +1,65 @@
name: Deploy website to GitHub Pages
name: GitHub Pages

# One site: the landing page (apps/website) at the root and the Expo web app (apps/mobile)
# under /app/. See docs/deployment.md. Pull requests typecheck and build it without deploying.
on:
push:
branches: [main, develop]
paths: [apps/website/**, package-lock.json, .github/workflows/pages.yml]
paths: &site-paths
- apps/website/**
- apps/mobile/**
- packages/**
- package.json
- package-lock.json
- scripts/build-pages.mjs
- .github/workflows/pages.yml
pull_request:
paths: *site-paths
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
# One deployment at a time (the newest push wins); each pull request builds on its own.
group: ${{ github.event_name == 'pull_request' && format('pages-pr-{0}', github.event.number) || 'pages' }}
cancel-in-progress: true

jobs:
deploy:
build:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci --workspace @hackyeah/website
- run: npm run typecheck --workspace @hackyeah/website
- run: npm run build --workspace @hackyeah/website
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
- run: npm ci
- run: npm run typecheck --workspace=@hackyeah/website
- run: npm run typecheck --workspace=@hackyeah/mobile
- run: npm run build:pages
env:
# Publishable values, not secrets: Expo bundles every EXPO_PUBLIC_ value into the
# client and row-level security guards the data. Repository variables of the same
# name override these defaults.
EXPO_PUBLIC_SUPABASE_URL: ${{ vars.EXPO_PUBLIC_SUPABASE_URL || 'https://xmzjxcxtsytwscktojzr.supabase.co' }}
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ vars.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_E6t3h9-f_UyR_01fraY8YA_408Y_537' }}
- if: github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v5
with:
path: apps/website/dist
path: dist/pages
include-hidden-files: true # .nojekyll

deploy:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
19 changes: 13 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Build a beginner sport app for inactive adults: preferences, sport suggestions,
AI-generated multi-day plans, completion, and feedback. Read the relevant docs
below before changing a feature.

- Monorepo: `apps/backend` (NestJS), `apps/web` (React), `apps/mobile` (Expo),
`apps/website` (React marketing site and demo, deployed to GitHub Pages).
- Monorepo: `apps/backend` (NestJS), `apps/mobile` (Expo: iOS, Android and the web
app, deployed to GitHub Pages at `/app/`), `apps/website` (React landing page at the
GitHub Pages root).
- Use TypeScript and shared lint/format rules. Current setup uses npm workspaces;
pnpm is the agreed target. Follow the migration plan in `docs/development.md`.
- Follow the [coding principles](docs/code-principles.md): one home per fact, one
Expand All @@ -18,8 +19,12 @@ below before changing a feature.
- Validate requests and AI output; enforce authenticated user ownership.
- Validated chat revisions replace the active plan immediately. Preserve prior
versions and completed activities.
- Guest entry (web only; mobile has none) creates an isolated anonymous identity
with seeded demo data.
- Guest entry (web only; the phone apps have none) signs in an anonymous Supabase
user named Guest on the real API. It starts at onboarding and can be saved as an
email account.
- The web app is the Expo web build. Desktop layouts (sidebar, docked coach, columns)
are gated by `useLayout()`; native and web below 768 px keep the phone layout.
See [Web app](docs/features/web-app.md).
- Branch from `develop`; target feature PRs to `develop` and release PRs to `main`.
Include acceptance criteria, verification evidence, and one teammate approval.
- Keep secrets out of clients and Git. Keep mobile compatible with API changes.
Expand All @@ -45,7 +50,9 @@ below before changing a feature.
- [Mobile review decisions, 3 October](docs/mobile-review-2026-10-03.md) — newer where they differ
- [Design reference: screens, tokens, preview](design/README.md) — read before building UI
- [Mobile app on Supabase](docs/features/mobile-supabase-integration.md) — the default backend: Supabase Auth, the remote adapter, configuration and what is deferred
- [Web app and guest mode](docs/features/web-app.md) — the Expo web build as a responsive panel, guest entry, GitHub Pages deployment
- [Mobile app on a mocked API](docs/features/mobile-mock-app.md) — the opt-in offline mock (`EXPO_PUBLIC_API_MODE=mock`), the demo account and Demo controls

An Expo starter and wearable contracts/extraction packages exist. Backend, dashboard and CI are planned;
do not claim planned commands or checks are already available.
The Expo app (phones and web), the Supabase product API and wearable packages exist. CI is
the GitHub Pages workflow (build on pull requests, deploy from `main`/`develop`); the NestJS
backend is planned. Do not claim planned commands or checks are already available.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@

> Built during **HackYeah 2026**.

Movo is a **mobile app** currently in active development. The mobile app is not available for download yet, but a **web app preview** is available and working.

In the future, Movo will also be available as a full **web app**, with a look similar to the current preview.
Movo is a sport app for beginners, built as one Expo app for phones and the web. The mobile app is not available for download yet; the **web app** runs in the browser.

---

## Project status

| Component | Status |
| ---------------- | ---------------------------- |
| Web app preview | Done |
| Mobile app | Not available yet |
| Web app | Not available yet |
| Component | Status |
| ------------ | ------------------------------------------------------- |
| Landing page | Live at https://taqix.github.io/hackyeah-2026/ |
| Web app | Live at https://taqix.github.io/hackyeah-2026/app/ |
| Mobile app | Not available yet |

The project idea, technology stack, and full setup instructions will be expanded as development progresses.

---

## Requirements

To access the web app preview, all you need is the preview link:
To use Movo, open the web app in a browser, then sign in or continue as a guest:

> https://taqix.github.io/hackyeah-2026/app/

> https://taqix.github.io/hackyeah-2026/
How the site is built and deployed: [GitHub Pages](docs/deployment.md#github-pages-landing-page-and-web-app).

---

Expand Down
20 changes: 20 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ConfigContext, ExpoConfig } from 'expo/config';

/**
* app.json, plus a sub-path web export when EXPO_WEB_BASE_URL is set (the GitHub
* Pages build sets /hackyeah-2026/app). That export is a single-page app: a static
* host can't serve the static renderer's per-route pages for dynamic routes such as
* /session/<id>, so every route loads one index.html and the router reads the URL.
* `expo start` and native builds leave it unset and get app.json unchanged.
*/
export default ({ config }: ConfigContext): ExpoConfig => {
const expo = config as ExpoConfig;
const path = process.env.EXPO_WEB_BASE_URL?.trim().replace(/^\/+|\/+$/g, '');
if (!path) return expo;

return {
...expo,
web: { ...expo.web, output: 'single' },
experiments: { ...expo.experiments, baseUrl: `/${path}` },
};
};
17 changes: 17 additions & 0 deletions apps/mobile/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ export interface ApiClient {
*/
signUpWithEmail(email: string, password: string, name: string): Promise<AuthSession>;
signInWithGoogle(): Promise<AuthSession>;
/**
* Continue as guest (web only; the mobile app has no guest entry): signs
* in a new anonymous account named "Guest" with no history, which uses the
* API like any account and goes through onboarding. Resolves with the
* session already signed in, if there is one, instead of making another.
* Rejects with GuestModeUnavailable (`remote/auth`) when the server has
* guests switched off.
*/
signInAsGuest(): Promise<AuthSession>;
/**
* Save your progress: adds an email and password to the signed-in guest,
* which keeps its plan and history and becomes an email account. Rejects
* with confirmation_required when the email has to be confirmed first (the
* account stays a guest until then), email_taken, weak_password under 8
* characters, validation for a bad email or when the account isn't a guest.
*/
upgradeGuest(email: string, password: string): Promise<AuthSession>;
sendPasswordReset(email: string): Promise<void>;
/**
* Sets a new password for the signed-in account (the password reset link
Expand Down
37 changes: 36 additions & 1 deletion apps/mobile/src/api/hooks/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient, type QueryClient } from '@tansta
import { api } from '@/api';
import { queryKeys } from '@/api/query-keys';
import { isSignInCancelled } from '@/api/remote/auth';
import type { AuthSession } from '@/api/types';
import type { Account, AuthSession } from '@/api/types';

/** Everything cached except the session and the providers belongs to one user. */
const isUserData = (key: readonly unknown[]) => key[0] !== 'auth';
Expand Down Expand Up @@ -77,6 +77,41 @@ export function useSignInWithGoogle() {
});
}

/**
* Continue as guest (web only): signs in a new anonymous account named Guest,
* which the gate then sends to onboarding. Rejects with GuestModeUnavailable
* when the server has guests switched off.
*/
export function useSignInAsGuest() {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['auth', 'sign-in-guest'],
mutationFn: () => api.auth.signInAsGuest(),
onSuccess: (session) => signedIn(queryClient, session),
});
}

/**
* Save your progress: adds an email and password to the signed-in guest. The
* person, plan and history stay the same, so only the session and the account
* are read again. Rejects with confirmation_required (check the inbox),
* email_taken, weak_password or validation.
*/
export function useUpgradeGuest() {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['auth', 'upgrade-guest'],
mutationFn: ({ email, password }: { email: string; password: string }) => api.auth.upgradeGuest(email, password),
onSuccess: (session) => {
queryClient.setQueryData(queryKeys.session, session);
queryClient.setQueryData<Account>(queryKeys.account, (account) =>
account ? { ...account, user: session.user } : account,
);
void queryClient.invalidateQueries({ queryKey: queryKeys.account });
},
});
}

/** Forgot password? Sends the reset email. */
export function useSendPasswordReset() {
return useMutation({
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/api/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export {
useLookupEmail,
useSendPasswordReset,
useSession,
useSignInAsGuest,
useSignInWithEmail,
useSignInWithGoogle,
useSignOut,
useSignUpWithEmail,
useUpdatePassword,
useUpgradeGuest,
} from './auth';
export { useSports, useSport } from './catalog';
export { useChatMessages, useSendChatMessage, useUndoChatMessage, type SendChatMessageInput } from './chat';
Expand Down
Loading
Loading