merge
This commit is contained in:
61
.github/workflows/deploy.yml
vendored
Normal file
61
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production-main
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Set up Supabase CLI
|
||||||
|
uses: supabase/setup-cli@v1
|
||||||
|
with:
|
||||||
|
version: 2.101.0
|
||||||
|
|
||||||
|
- name: Link Supabase project
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||||
|
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
|
||||||
|
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
|
||||||
|
run: supabase link --project-ref "$SUPABASE_PROJECT_REF" --password "$SUPABASE_DB_PASSWORD"
|
||||||
|
|
||||||
|
- name: Apply pending migrations and seed baseline data
|
||||||
|
env:
|
||||||
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||||
|
run: supabase db push --linked --include-seed
|
||||||
|
|
||||||
|
- name: Set up Vercel CLI
|
||||||
|
run: npm install --global vercel@54.4.1
|
||||||
|
|
||||||
|
- name: Deploy to Vercel production
|
||||||
|
env:
|
||||||
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||||
|
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||||
|
run: vercel deploy --prod --yes --token "$VERCEL_TOKEN"
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -16,6 +16,7 @@
|
|||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@@ -30,12 +31,17 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files
|
||||||
.env*.local
|
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env*.example
|
||||||
|
!.env*.sample
|
||||||
|
!.env*.template
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
supabase/.temp/
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -18,7 +18,7 @@ This repo currently stops at:
|
|||||||
- environment variable template
|
- environment variable template
|
||||||
- deployment-ready project structure
|
- deployment-ready project structure
|
||||||
|
|
||||||
This repo does not yet include MVP feature pages, SQL schema, avatar logic, habits, streaks, or shop logic.
|
This repo does not yet include MVP feature pages, avatar logic, habits, streaks, or shop logic.
|
||||||
|
|
||||||
## Local setup
|
## Local setup
|
||||||
|
|
||||||
@@ -59,11 +59,37 @@ Create a Supabase project, then copy the project URL and anon key from **Project
|
|||||||
|
|
||||||
The starter also supports Supabase's publishable key name, but this repo documents the anon key convention from the project plan.
|
The starter also supports Supabase's publishable key name, but this repo documents the anon key convention from the project plan.
|
||||||
|
|
||||||
|
### Database workflow
|
||||||
|
|
||||||
|
- Schema migrations live under `supabase/migrations/`.
|
||||||
|
- Baseline catalog data lives in `supabase/seed.sql` and is safe to rerun during deploys because it uses idempotent upserts.
|
||||||
|
- Local CLI config lives in `supabase/config.toml` for `supabase start`, `supabase db reset`, and related commands.
|
||||||
|
- Production database sync runs from GitHub Actions on `main` using `supabase db push --linked --include-seed` before the app deploy relies on those changes.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
1. Push the repo to GitHub.
|
This repo includes `.github/workflows/deploy.yml`, which runs on pushes to `main`.
|
||||||
2. Import the repo into Vercel.
|
|
||||||
3. Add the same Supabase environment variables in Vercel.
|
The workflow:
|
||||||
4. Deploy.
|
|
||||||
|
- checks out the repo
|
||||||
|
- runs `npm ci`, `npm run lint`, and `npm run build`
|
||||||
|
- links the Supabase CLI to the production project
|
||||||
|
- applies pending database migrations and reruns `supabase/seed.sql` via `supabase db push --linked --include-seed`
|
||||||
|
- deploys the app to Vercel production only after the database sync succeeds
|
||||||
|
|
||||||
|
### Required GitHub secrets
|
||||||
|
|
||||||
|
- `SUPABASE_ACCESS_TOKEN`
|
||||||
|
- `SUPABASE_PROJECT_REF`
|
||||||
|
- `SUPABASE_DB_PASSWORD`
|
||||||
|
- `VERCEL_TOKEN`
|
||||||
|
- `VERCEL_ORG_ID`
|
||||||
|
- `VERCEL_PROJECT_ID`
|
||||||
|
|
||||||
|
### Required Vercel environment variables
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_SUPABASE_URL`
|
||||||
|
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||||
|
|
||||||
Frontend and backend are both served from this Next.js app. Supabase provides auth and database services.
|
Frontend and backend are both served from this Next.js app. Supabase provides auth and database services.
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ export default function Page() {
|
|||||||
<CardTitle className="text-2xl">
|
<CardTitle className="text-2xl">
|
||||||
Thank you for signing up!
|
Thank you for signing up!
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Check your email to confirm</CardDescription>
|
<CardDescription>
|
||||||
|
{`You're ready to go`}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
You've successfully signed up. Please check your email to
|
Your account is ready. You can sign in and start using Habit Pet.
|
||||||
confirm your account before signing in.
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -42,14 +42,19 @@ export function SignUpForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase.auth.signUp({
|
const { data, error } = await supabase.auth.signUp({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
options: {
|
options: {
|
||||||
emailRedirectTo: `${window.location.origin}${routes.onboardingQuiz}`,
|
emailRedirectTo: `${window.location.origin}/auth/confirm?next=${encodeURIComponent(routes.onboardingQuiz)}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
if (data.session) {
|
||||||
|
router.push(routes.onboardingQuiz);
|
||||||
|
router.refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.push("/auth/sign-up-success");
|
router.push("/auth/sign-up-success");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setError(error instanceof Error ? error.message : "An error occurred");
|
setError(error instanceof Error ? error.message : "An error occurred");
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
{
|
{
|
||||||
ignores: [".next/**", "node_modules/**"],
|
ignores: [".next/**", "node_modules/**", ".worktrees/**"],
|
||||||
},
|
},
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
];
|
];
|
||||||
|
|||||||
3974
package-lock.json
generated
3974
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
|||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vercel": "^54.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
supabase/config.toml
Normal file
54
supabase/config.toml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
project_id = "habit-pet"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
enabled = true
|
||||||
|
port = 54321
|
||||||
|
schemas = ["public", "graphql_public"]
|
||||||
|
extra_search_path = ["public", "extensions"]
|
||||||
|
max_rows = 1000
|
||||||
|
|
||||||
|
[db]
|
||||||
|
port = 54322
|
||||||
|
shadow_port = 54320
|
||||||
|
major_version = 15
|
||||||
|
|
||||||
|
[db.seed]
|
||||||
|
enabled = true
|
||||||
|
sql_paths = ["./seed.sql"]
|
||||||
|
|
||||||
|
[studio]
|
||||||
|
enabled = true
|
||||||
|
port = 54323
|
||||||
|
|
||||||
|
[inbucket]
|
||||||
|
enabled = true
|
||||||
|
port = 54324
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
enabled = true
|
||||||
|
file_size_limit = "50MiB"
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = true
|
||||||
|
site_url = "https://cursor-hackathon-flame.vercel.app"
|
||||||
|
additional_redirect_urls = [
|
||||||
|
"http://127.0.0.1:3000/**",
|
||||||
|
"http://localhost:3000/**",
|
||||||
|
"https://cursor-hackathon-flame.vercel.app/**",
|
||||||
|
"https://cursor-hackathon-*-harkamal-randhawas-projects.vercel.app/**",
|
||||||
|
"https://cursor-hackathon-*-recentrunner-harkamal-randhawas-projects.vercel.app/**",
|
||||||
|
]
|
||||||
|
jwt_expiry = 3600
|
||||||
|
enable_refresh_token_rotation = true
|
||||||
|
refresh_token_reuse_interval = 10
|
||||||
|
|
||||||
|
[auth.email]
|
||||||
|
enable_signup = true
|
||||||
|
double_confirm_changes = true
|
||||||
|
enable_confirmations = false
|
||||||
|
|
||||||
|
[realtime]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analytics]
|
||||||
|
enabled = false
|
||||||
67
supabase/migrations/20260523220000_initial_schema.sql
Normal file
67
supabase/migrations/20260523220000_initial_schema.sql
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
create extension if not exists pgcrypto;
|
||||||
|
|
||||||
|
create table profiles (
|
||||||
|
id uuid primary key references auth.users(id) on delete cascade,
|
||||||
|
display_name text,
|
||||||
|
focus_topics text[] default '{}',
|
||||||
|
onboarding_complete boolean default false,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table habits (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
user_id uuid references auth.users(id) on delete cascade not null,
|
||||||
|
name text not null,
|
||||||
|
active boolean default true,
|
||||||
|
created_at timestamptz default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table habit_logs (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
habit_id uuid references habits(id) on delete cascade not null,
|
||||||
|
user_id uuid references auth.users(id) on delete cascade not null,
|
||||||
|
completed_on date not null default current_date,
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
unique (habit_id, completed_on)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table daily_entries (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
user_id uuid references auth.users(id) on delete cascade not null,
|
||||||
|
entry_date date not null default current_date,
|
||||||
|
mood int check (mood between 1 and 5),
|
||||||
|
stress int check (stress between 1 and 5),
|
||||||
|
energy int check (energy between 1 and 5),
|
||||||
|
sleep_hours numeric,
|
||||||
|
sleep_quality int check (sleep_quality between 1 and 5),
|
||||||
|
journal text,
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
unique (user_id, entry_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table avatar_state (
|
||||||
|
user_id uuid primary key references auth.users(id) on delete cascade,
|
||||||
|
avatar_name text default 'Pixel Me',
|
||||||
|
mood_state text default 'neutral',
|
||||||
|
health int default 50,
|
||||||
|
energy int default 50,
|
||||||
|
coins int default 0,
|
||||||
|
streak int default 0,
|
||||||
|
equipped_item text default 'none',
|
||||||
|
updated_at timestamptz default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table shop_items (
|
||||||
|
id text primary key,
|
||||||
|
name text not null,
|
||||||
|
type text not null,
|
||||||
|
price int not null,
|
||||||
|
image_path text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table user_items (
|
||||||
|
user_id uuid references auth.users(id) on delete cascade,
|
||||||
|
item_id text references shop_items(id) on delete cascade,
|
||||||
|
purchased_at timestamptz default now(),
|
||||||
|
primary key (user_id, item_id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
alter table profiles enable row level security;
|
||||||
|
alter table habits enable row level security;
|
||||||
|
alter table habit_logs enable row level security;
|
||||||
|
alter table daily_entries enable row level security;
|
||||||
|
alter table avatar_state enable row level security;
|
||||||
|
alter table shop_items enable row level security;
|
||||||
|
alter table user_items enable row level security;
|
||||||
|
|
||||||
|
alter table habits
|
||||||
|
add constraint habits_id_user_id_unique unique (id, user_id);
|
||||||
|
|
||||||
|
alter table habit_logs
|
||||||
|
add constraint habit_logs_habit_id_user_id_fkey
|
||||||
|
foreign key (habit_id, user_id)
|
||||||
|
references habits (id, user_id)
|
||||||
|
on delete cascade;
|
||||||
|
|
||||||
|
create policy "profiles_select_own"
|
||||||
|
on profiles
|
||||||
|
for select
|
||||||
|
to authenticated
|
||||||
|
using (id = auth.uid());
|
||||||
|
|
||||||
|
create policy "profiles_insert_own"
|
||||||
|
on profiles
|
||||||
|
for insert
|
||||||
|
to authenticated
|
||||||
|
with check (id = auth.uid());
|
||||||
|
|
||||||
|
create policy "profiles_update_own"
|
||||||
|
on profiles
|
||||||
|
for update
|
||||||
|
to authenticated
|
||||||
|
using (id = auth.uid())
|
||||||
|
with check (id = auth.uid());
|
||||||
|
|
||||||
|
create policy "habits_manage_own"
|
||||||
|
on habits
|
||||||
|
for all
|
||||||
|
to authenticated
|
||||||
|
using (user_id = auth.uid())
|
||||||
|
with check (user_id = auth.uid());
|
||||||
|
|
||||||
|
create policy "habit_logs_manage_own"
|
||||||
|
on habit_logs
|
||||||
|
for all
|
||||||
|
to authenticated
|
||||||
|
using (user_id = auth.uid())
|
||||||
|
with check (user_id = auth.uid());
|
||||||
|
|
||||||
|
create policy "daily_entries_manage_own"
|
||||||
|
on daily_entries
|
||||||
|
for all
|
||||||
|
to authenticated
|
||||||
|
using (user_id = auth.uid())
|
||||||
|
with check (user_id = auth.uid());
|
||||||
|
|
||||||
|
create policy "avatar_state_manage_own"
|
||||||
|
on avatar_state
|
||||||
|
for all
|
||||||
|
to authenticated
|
||||||
|
using (user_id = auth.uid())
|
||||||
|
with check (user_id = auth.uid());
|
||||||
|
|
||||||
|
create policy "shop_items_read_all"
|
||||||
|
on shop_items
|
||||||
|
for select
|
||||||
|
to anon, authenticated
|
||||||
|
using (true);
|
||||||
|
|
||||||
|
create policy "user_items_manage_own"
|
||||||
|
on user_items
|
||||||
|
for all
|
||||||
|
to authenticated
|
||||||
|
using (user_id = auth.uid())
|
||||||
|
with check (user_id = auth.uid());
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
create or replace function public.handle_new_user()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
insert into public.profiles (id, display_name)
|
||||||
|
values (
|
||||||
|
new.id,
|
||||||
|
coalesce(new.raw_user_meta_data ->> 'display_name', split_part(new.email, '@', 1))
|
||||||
|
)
|
||||||
|
on conflict (id) do nothing;
|
||||||
|
|
||||||
|
insert into public.avatar_state (user_id)
|
||||||
|
values (new.id)
|
||||||
|
on conflict (user_id) do nothing;
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop trigger if exists on_auth_user_created on auth.users;
|
||||||
|
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row
|
||||||
|
execute function public.handle_new_user();
|
||||||
11
supabase/seed.sql
Normal file
11
supabase/seed.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
insert into shop_items (id, name, type, price, image_path)
|
||||||
|
values
|
||||||
|
('hat_blue', 'Blue Hat', 'accessory', 20, '/items/hat-blue.png'),
|
||||||
|
('glasses_round', 'Round Glasses', 'accessory', 30, '/items/glasses-round.png'),
|
||||||
|
('room_sunset', 'Sunset Room', 'background', 40, '/backgrounds/room-sunset.png')
|
||||||
|
on conflict (id) do update
|
||||||
|
set
|
||||||
|
name = excluded.name,
|
||||||
|
type = excluded.type,
|
||||||
|
price = excluded.price,
|
||||||
|
image_path = excluded.image_path;
|
||||||
Reference in New Issue
Block a user