├── .husky ├── pre-push └── pre-commit ├── utils ├── types.ts ├── errors │ └── ValidationError.ts ├── usernameValidator.ts └── supabase │ └── server.ts ├── supabase ├── .gitignore ├── functions │ ├── email-signup-link │ │ ├── deno.json │ │ └── .npmrc │ ├── env.example │ └── deno.json ├── migrations │ ├── 20250825164024_alpha_token_email.sql │ ├── 20250518145124_new_profile_trigger.sql │ ├── 20250812161712_project_defaults.sql │ ├── 20250803000000_add_project_updates_table.sql │ ├── 20250905144832_usage_on_policy_docs.sql │ ├── 20250823004425_alpha_token_table.sql │ ├── 20250831000001_fix_project_images_policy.sql │ ├── 20251031000000_change_images_to_urls.sql │ ├── 20250831000000_add_project_image_refs.sql │ ├── 20250830223716_private_profile.sql │ ├── 20250807230208_insert_profile_update.sql │ ├── 20250830500000_add_images_table.sql │ ├── 20250907141731_policy_doc_triggers.sql │ ├── 20251021000000_create_images_bucket.sql │ └── 20250801104606_create_project_updates.sql ├── seed.sql └── __tests__ │ ├── testClients.ts │ └── testUser.ts ├── docs ├── oathapps.png ├── appregistered.png ├── registerapp.png └── pull_request_template.md ├── public ├── 404 │ ├── funny1.png │ ├── funny10.png │ ├── funny2.png │ ├── funny3.png │ ├── funny4.png │ ├── funny5.png │ ├── funny6.png │ ├── funny7.png │ ├── funny8.png │ └── funny9.png ├── door.jpg ├── logo3.png ├── og-image.jpg ├── BiP_Banner.png ├── BuiltInPublic.png ├── terminal-logo.png └── example-cover-img.jpg ├── src ├── components │ ├── Navbar │ │ ├── index.ts │ │ └── Navbar.tsx │ ├── Projects │ │ ├── CreateProject │ │ │ ├── createProject.schema.ts │ │ │ └── actions.ts │ │ ├── ProjectVisibilityBadge.tsx │ │ ├── ProjectPanel │ │ │ ├── ProjectDeleteButton.tsx │ │ │ ├── ProjectPanel.tsx │ │ │ ├── ProjectDisplayPanel.tsx │ │ │ ├── ProjectVisibilityDropdown.tsx │ │ │ ├── ProjectStatusDropdown.tsx │ │ │ └── ProjectEditPanel.tsx │ │ ├── ProjectUpdateCard.tsx │ │ ├── ProjectStatusBadge.tsx │ │ └── ProjectCard.tsx │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── avatar.tsx │ │ ├── confirmation-dialog.tsx │ │ ├── button.tsx │ │ └── card.tsx │ ├── Buttons │ │ ├── EditButton.tsx │ │ ├── SignOutBtn.tsx │ │ └── BackButton.tsx │ ├── Providers │ │ ├── ThemeProvider.tsx │ │ ├── QueryProvider.tsx │ │ ├── ProfileProvider.tsx │ │ └── ProjectProvider.tsx │ ├── ProfileIcon.tsx │ ├── Footer.tsx │ ├── Profile │ │ └── Avatar.tsx │ └── Policy │ │ └── DisplayDocumentDialog.tsx ├── app │ ├── favicon.ico │ ├── (main) │ │ ├── [username] │ │ │ ├── profile.css │ │ │ ├── project │ │ │ │ ├── page.tsx │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── components │ │ │ │ ├── FeedSection.tsx │ │ │ │ ├── StreakSection.tsx │ │ │ │ ├── UserInfo.tsx │ │ │ │ └── GradientBlobs.tsx │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── friends-projects │ │ │ │ └── FriendsProjects.tsx │ │ │ ├── projects │ │ │ │ └── ProjectList.tsx │ │ │ ├── groups │ │ │ │ └── Groups.tsx │ │ │ ├── profile │ │ │ │ └── Profile.tsx │ │ │ ├── feed │ │ │ │ ├── Likes.tsx │ │ │ │ └── Feed.tsx │ │ │ ├── page.tsx │ │ │ └── stats │ │ │ │ └── Stats.tsx │ │ ├── layout.tsx │ │ └── onboarding │ │ │ ├── onboarding-form │ │ │ ├── onboarding-form.schema.ts │ │ │ └── actions.ts │ │ │ └── page.tsx │ ├── staging-auth │ │ ├── page.tsx │ │ ├── stagingAuth.schema.ts │ │ ├── actions.ts │ │ └── StagingAuth.tsx │ ├── thanks │ │ └── page.tsx │ ├── auth │ │ ├── actions.ts │ │ ├── oauth │ │ │ └── route.ts │ │ ├── DevSignIn.tsx │ │ └── page.tsx │ ├── project │ │ └── [id] │ │ │ └── page.tsx │ ├── not-found.tsx │ ├── about │ │ └── page.tsx │ └── layout.tsx ├── setupTests.ts ├── hooks │ ├── useProject │ │ ├── updateProject.schema.ts │ │ └── editProject.schema.ts │ ├── useUser │ │ ├── useUser.tsx │ │ └── actions.ts │ ├── useProfile │ │ ├── profile.schema.ts │ │ ├── useProfile.tsx │ │ └── actions.ts │ ├── usePolicy │ │ ├── usePolicyDocument.ts │ │ └── actions.ts │ └── useImage │ │ └── image.schema.ts ├── use-cases │ ├── BaseFetchUseCase.ts │ ├── projects │ │ ├── DeleteProject.ts │ │ ├── GetProject.ts │ │ ├── CreateNewProject.ts │ │ ├── __tests__ │ │ │ ├── DeleteProject.test.ts │ │ │ └── CreateNewProject.test.ts │ │ └── EditProject.ts │ ├── BaseMutationUseCase.ts │ ├── userConsent │ │ ├── UserConsent.ts │ │ └── __tests__ │ │ │ └── UserConsent.test.ts │ └── updateUserProfile │ │ └── UpdateUserProfile.ts ├── repositories │ ├── policyRepository │ │ └── policy.types.ts │ ├── profileRepository │ │ └── profile.types.ts │ └── projectRepository │ │ └── project.types.ts ├── middleware.ts ├── lib │ └── utils.ts └── services │ └── UINotification.service.ts ├── postcss.config.mjs ├── config └── private │ └── profanity-list.ts ├── .gitguardian.toml ├── .prettierrc.yml ├── instrumentation-client.ts ├── .prettierignore ├── .env.example ├── components.json ├── scripts ├── seeds │ ├── skills.ts │ ├── policy-doc.ts │ ├── profile-skills.ts │ ├── posts.ts │ ├── projects.ts │ └── auth-users.ts ├── generateSupabaseTypes.ts └── seed.ts ├── .github ├── workflows │ ├── prettier.yml │ ├── push-migrations-staging.yml │ ├── semgrep.yml │ ├── push-migrations-prod.yml │ ├── syft.yml │ ├── renovate-lockfile-gate.yml │ ├── renovate-lockfile-pr.yml │ ├── codeql.yml │ ├── gitleaks.yml │ └── unit-tests.yml └── ISSUE_TEMPLATE │ └── new-feature-request.md ├── vitest.config.mts ├── tsconfig.json ├── .gitleaks.toml ├── knip.config.json ├── .gitignore ├── renovate.json ├── eslint.config.mjs ├── SECURITY.md └── package.json /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npm run precheck -------------------------------------------------------------------------------- /utils/types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | null | undefined; 2 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /supabase/functions/email-signup-link/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": {} 3 | } 4 | -------------------------------------------------------------------------------- /supabase/functions/env.example: -------------------------------------------------------------------------------- 1 | RESEND_API_KEY= 2 | WEBHOOK_SECRET= 3 | PLATFORM_HOST= 4 | -------------------------------------------------------------------------------- /docs/oathapps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/docs/oathapps.png -------------------------------------------------------------------------------- /public/door.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/door.jpg -------------------------------------------------------------------------------- /public/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/logo3.png -------------------------------------------------------------------------------- /src/components/Navbar/index.ts: -------------------------------------------------------------------------------- 1 | import NavBar from './Navbar'; 2 | 3 | export default NavBar; 4 | -------------------------------------------------------------------------------- /public/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/og-image.jpg -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /docs/appregistered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/docs/appregistered.png -------------------------------------------------------------------------------- /docs/registerapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/docs/registerapp.png -------------------------------------------------------------------------------- /public/404/funny1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny1.png -------------------------------------------------------------------------------- /public/404/funny10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny10.png -------------------------------------------------------------------------------- /public/404/funny2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny2.png -------------------------------------------------------------------------------- /public/404/funny3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny3.png -------------------------------------------------------------------------------- /public/404/funny4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny4.png -------------------------------------------------------------------------------- /public/404/funny5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny5.png -------------------------------------------------------------------------------- /public/404/funny6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny6.png -------------------------------------------------------------------------------- /public/404/funny7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny7.png -------------------------------------------------------------------------------- /public/404/funny8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny8.png -------------------------------------------------------------------------------- /public/404/funny9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/404/funny9.png -------------------------------------------------------------------------------- /public/BiP_Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/BiP_Banner.png -------------------------------------------------------------------------------- /public/BuiltInPublic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/BuiltInPublic.png -------------------------------------------------------------------------------- /public/terminal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/terminal-logo.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/example-cover-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Christin-paige/BuiltInPublic/HEAD/public/example-cover-img.jpg -------------------------------------------------------------------------------- /config/private/profanity-list.ts: -------------------------------------------------------------------------------- 1 | const profanityList = ['ass', 'shit', 'damn', 'hell', 'fuck']; 2 | 3 | export default profanityList; 4 | -------------------------------------------------------------------------------- /.gitguardian.toml: -------------------------------------------------------------------------------- 1 | title = "Gitleaks Configuration" 2 | 3 | [allowlist] 4 | description = "Allowlisted files" 5 | files = [ 6 | ".env.example" 7 | ] 8 | -------------------------------------------------------------------------------- /supabase/migrations/20250825164024_alpha_token_email.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."alpha_tokens" add column send_email varchar(255) not null default 'builtinpublic@gmail.com'; 2 | -------------------------------------------------------------------------------- /supabase/migrations/20250518145124_new_profile_trigger.sql: -------------------------------------------------------------------------------- 1 | CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.insert_profile_for_new_user (); 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { afterEach } from 'vitest'; 2 | import { cleanup } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | 5 | afterEach(() => { 6 | cleanup(); 7 | }); 8 | -------------------------------------------------------------------------------- /supabase/migrations/20250812161712_project_defaults.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."projects" alter column "visibility" set default 'private'; 2 | alter table "public"."projects" rename column "repo_url" to "external_url"; 3 | -------------------------------------------------------------------------------- /supabase/migrations/20250803000000_add_project_updates_table.sql: -------------------------------------------------------------------------------- 1 | -- do not add anything to this file, it was committed empty and is being left in to maintain migration history 2 | -- changes added here will not be reflected in remote environments -------------------------------------------------------------------------------- /src/hooks/useProject/updateProject.schema.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const updateProjectSchema = z.object({ 4 | update: z.string().min(1).max(255), 5 | }); 6 | 7 | export type UpdateProjectSchema = z.infer; 8 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: true 2 | singleQuote: true 3 | printWidth: 80 4 | quoteProps: 'as-needed' 5 | tabWidth: 2 6 | useTabs: false 7 | trailingComma: es5 8 | bracketSpacing: true 9 | arrowParens: always 10 | endOfLine: lf 11 | jsxSingleQuote: true 12 | -------------------------------------------------------------------------------- /src/app/(main)/[username]/profile.css: -------------------------------------------------------------------------------- 1 | .cyan-glow { 2 | box-shadow: #00c7ff 0px 0px 10px 0px; 3 | } 4 | 5 | .purple-glow { 6 | box-shadow: #86059f 0px 0px 10px 0px; 7 | } 8 | 9 | .magenta-glow { 10 | box-shadow: #ff00ea 0px 0px 10px 0px; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Projects/CreateProject/createProject.schema.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const createProjectSchema = z.object({ 4 | name: z.string().min(2).max(100), 5 | }); 6 | 7 | export type CreateProjectSchema = z.infer; 8 | -------------------------------------------------------------------------------- /supabase/functions/email-signup-link/.npmrc: -------------------------------------------------------------------------------- 1 | # Configuration for private npm package dependencies 2 | # For more information on using private registries with Edge Functions, see: 3 | # https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries 4 | -------------------------------------------------------------------------------- /instrumentation-client.ts: -------------------------------------------------------------------------------- 1 | import posthog from 'posthog-js'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 5 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 6 | defaults: '2025-05-24', 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /utils/errors/ValidationError.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor( 3 | message: string, 4 | public validationErrors: Record 5 | ) { 6 | super(message); 7 | this.name = 'ValidationError'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/staging-auth/page.tsx: -------------------------------------------------------------------------------- 1 | import StagingAuth from './StagingAuth'; 2 | 3 | export default async function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/staging-auth/stagingAuth.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const StagingAuthSchema = z.object({ 4 | password: z.string().min(2).max(32), 5 | }); 6 | 7 | export type StagingAuthSchemaType = z.infer; 8 | 9 | export default StagingAuthSchema; 10 | -------------------------------------------------------------------------------- /src/app/(main)/[username]/project/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { redirect, useParams } from 'next/navigation'; 4 | 5 | export default function Page() { 6 | const params = useParams(); 7 | const username = params.username as string; 8 | 9 | redirect(`/${username}`); 10 | } 11 | -------------------------------------------------------------------------------- /supabase/migrations/20250905144832_usage_on_policy_docs.sql: -------------------------------------------------------------------------------- 1 | grant usage on schema "policy" to authenticated, anon; 2 | grant usage on type "policy"."policy_doc_types" to authenticated, anon; 3 | grant usage on type "policy"."revocation_reasons" to authenticated, anon; 4 | grant usage on type "policy"."consent_methods" to authenticated, anon; 5 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /supabase/migrations/20250823004425_alpha_token_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists "public"."alpha_tokens" ( 2 | "id" uuid not null primary key default gen_random_uuid(), 3 | "user_id" uuid references "public"."profiles" (id) on delete cascade, 4 | "created_at" timestamptz not null default now() 5 | ); 6 | 7 | alter table "public"."alpha_tokens" enable row level security; 8 | -------------------------------------------------------------------------------- /src/use-cases/BaseFetchUseCase.ts: -------------------------------------------------------------------------------- 1 | import { AnySupabaseClient } from 'utils/supabase/server'; 2 | 3 | export abstract class BaseFetchUseCase { 4 | protected supabase: AnySupabaseClient; 5 | 6 | constructor(supabase: AnySupabaseClient) { 7 | this.supabase = supabase; 8 | } 9 | 10 | abstract execute(params: TParams): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /supabase/migrations/20250831000001_fix_project_images_policy.sql: -------------------------------------------------------------------------------- 1 | -- Drop the existing policy first 2 | DROP POLICY IF EXISTS "View project images" ON projects; 3 | 4 | -- Recreate the policy with correct enum casting 5 | CREATE POLICY "View project images" 6 | ON projects 7 | FOR SELECT 8 | USING ( 9 | visibility = 'public'::project_visibility 10 | OR owner_id = auth.uid() 11 | ); -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore generated Supabase types 2 | supabase/**/*.types.ts 3 | 4 | # Ignore dependencies and build output 5 | node_modules/ 6 | dist/ 7 | build/ 8 | coverage/ 9 | .next/ 10 | out/ 11 | 12 | # Ignore configuration files 13 | .vscode/ 14 | knip.config.json 15 | eslint.config.js 16 | postcss.config.js 17 | next.config.js 18 | tailwind.config.js 19 | vitest.config.js 20 | jest.config.js -------------------------------------------------------------------------------- /supabase/migrations/20251031000000_change_images_to_urls.sql: -------------------------------------------------------------------------------- 1 | -- Change primary_image and gallery_images back to TEXT to store URLs instead of UUIDs 2 | ALTER TABLE projects 3 | DROP CONSTRAINT IF EXISTS projects_primary_image_fkey; 4 | 5 | ALTER TABLE projects 6 | ALTER COLUMN primary_image TYPE TEXT USING primary_image::TEXT; 7 | 8 | ALTER TABLE projects 9 | ALTER COLUMN gallery_images TYPE TEXT[] USING gallery_images::TEXT[]; 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY=run-supabase-status-to-get-this 3 | SUPABASE_SERVICE_ROLE_KEY=run-supabase-status-to-get-this 4 | 5 | GITHUB_AUTH_EXTERNAL_CLIENT_ID=123456 #Your Client ID will go here 6 | GITHUB_AUTH_EXTERNAL_SECRET=123456 #Your github secret goes here 7 | GOOGLE_AUTH_EXTERNAL_CLIENT_ID=123456 #Your Client ID will go here 8 | GOOGLE_AUTH_EXTERNAL_SECRET=123456 #Your google secret goes here -------------------------------------------------------------------------------- /src/app/(main)/dashboard/friends-projects/FriendsProjects.tsx: -------------------------------------------------------------------------------- 1 | const FriendsProjects = () => { 2 | return ( 3 |
4 |

{`Friends' Projects`}

5 | 6 |
7 |

No projects yet. Start building!

8 |
9 |
10 | ); 11 | }; 12 | 13 | export default FriendsProjects; 14 | -------------------------------------------------------------------------------- /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -- Seed data for Supabase database 2 | -- This file is automatically run when doing a database reset 3 | 4 | -- Insert test policy document into policy.policy_documents table 5 | INSERT INTO policy.policy_documents (document_type, version, effective_from, content) 6 | VALUES ( 7 | 'T&C', 8 | '0.0.1', 9 | now(), 10 | 'I agree to all of the things, 11 | even the things that contradict the other things, 12 | regardless of consequence, forever and always.' 13 | ) 14 | ON CONFLICT DO NOTHING; 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Buttons/EditButton.tsx: -------------------------------------------------------------------------------- 1 | import { Pencil } from 'lucide-react'; 2 | 3 | export default function EditButton({ 4 | onClick, 5 | label, 6 | }: { 7 | onClick: () => void; 8 | label: string; 9 | }) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/(main)/dashboard/projects/ProjectList.tsx: -------------------------------------------------------------------------------- 1 | const ProjectList = () => { 2 | return ( 3 |
4 |

Projects

5 | 6 |

Project

7 |

Project

8 |

Project

9 |
10 | ); 11 | }; 12 | 13 | export default ProjectList; 14 | -------------------------------------------------------------------------------- /src/app/(main)/dashboard/groups/Groups.tsx: -------------------------------------------------------------------------------- 1 | const Groups = () => { 2 | return ( 3 |
4 |

My Groups

5 |
6 |

Group

7 |

Group

8 |

Group

9 |
10 |
11 | ); 12 | }; 13 | 14 | export default Groups; 15 | -------------------------------------------------------------------------------- /src/hooks/useUser/useUser.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useQuery } from '@tanstack/react-query'; 4 | import { getCurrentUser, signOutUser } from './actions'; 5 | import UINotification from '@/services/UINotification.service'; 6 | 7 | export default function useUser() { 8 | const { data, isLoading, error } = useQuery({ 9 | queryKey: ['user'], 10 | queryFn: getCurrentUser, 11 | }); 12 | 13 | if (error) { 14 | UINotification.error('Could not retrieve user'); 15 | } 16 | // TODO: check for error in useQuery response and notify user 17 | return { data, isLoading, error, signOutUser }; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/(main)/[username]/components/FeedSection.tsx: -------------------------------------------------------------------------------- 1 | import { CreateProjectButton } from '@/components/Projects/CreateProject/CreateProjectButton'; 2 | import { useProfileContext } from '@/components/Providers/ProfileProvider'; 3 | import { ProjectsList } from '@/components/Projects/ProjectsList'; 4 | 5 | const FeedSection = () => { 6 | const { canEdit } = useProfileContext(); 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default FeedSection; 16 | -------------------------------------------------------------------------------- /src/app/thanks/page.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 | 8 | {'See you soon!'} 9 | 10 | 11 | {`Thanks for your interest in Built In Public, we're currently in a closed alpha stage. We'll get back to you soon!`} 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | ThemeProvider as NextThemesProvider, 6 | ThemeProviderProps, 7 | } from 'next-themes'; 8 | 9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | const [mounted, setMounted] = React.useState(false); 11 | 12 | // Only render children after mounting to avoid hydration mismatch 13 | React.useEffect(() => { 14 | setMounted(true); 15 | }, []); 16 | 17 | if (!mounted) { 18 | return null; 19 | } 20 | 21 | return {children}; 22 | } 23 | -------------------------------------------------------------------------------- /src/repositories/policyRepository/policy.types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'supabase/supabase.types'; 2 | 3 | export type PolicyDocumentType = 4 | Database['policy']['Enums']['policy_doc_types']; 5 | 6 | export interface PolicyDocumentDTO { 7 | id: string; 8 | version: string; 9 | document_type: PolicyDocumentType; 10 | effective_from: string; 11 | superseded_at: string | null; 12 | content: string; 13 | } 14 | 15 | export interface PolicyDocument { 16 | id: string; 17 | version: string; 18 | documentType: PolicyDocumentType; 19 | effectiveFrom: string; 20 | supersededAt: string | null; 21 | content: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ProfileIcon.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Image from 'next/image'; 3 | 4 | interface ProfileIconProps { 5 | url?: string | null; 6 | className?: string; 7 | } 8 | 9 | export default function ProfileIcon({ url, className = '' }: ProfileIconProps) { 10 | return ( 11 |
12 | User profile 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useProject/editProject.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const editProjectSchema = z.object({ 4 | name: z.string().min(2).max(100).optional(), 5 | description: z.string().min(2).max(500).optional(), 6 | status: z 7 | .enum(['planning', 'in-progress', 'on-hold', 'completed', 'launched']) 8 | .optional(), 9 | visibility: z.enum(['public', 'private']).optional(), 10 | externalUrl: z.url().optional(), 11 | primaryImage: z.string().uuid().or(z.string().url()).optional(), 12 | galleryImages: z.array(z.string().uuid().or(z.string().url())).optional(), 13 | }); 14 | 15 | export type EditProjectSchema = z.infer; 16 | -------------------------------------------------------------------------------- /src/repositories/profileRepository/profile.types.ts: -------------------------------------------------------------------------------- 1 | import { Database } from 'supabase/supabase.types'; 2 | 3 | export interface UserConsents { 4 | consentedAt: string; 5 | documentId: string; 6 | documentType: Database['policy']['Enums']['policy_doc_types']; 7 | } 8 | 9 | export interface ProfileDTO { 10 | id: string; 11 | username: string | null; 12 | avatar_url: string | null; 13 | bio: string | null; 14 | display_name: string | null; 15 | } 16 | 17 | export interface Profile { 18 | id: string; 19 | username: string | null; 20 | avatarUrl: string | null; 21 | bio?: string; 22 | displayName?: string | null; 23 | consents?: UserConsents | null; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/useProfile/profile.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // Schema function for user profile display name 4 | export const displayNameSchema = z.object({ 5 | displayName: z 6 | .string() 7 | .min(2, 'Display name must be at least 2 characters') 8 | .max(50, 'Display name must be no more than 50 characters'), 9 | }); 10 | 11 | // Schema function for user profile bio 12 | export const bioSchema = z.object({ 13 | bio: z 14 | .string() 15 | .max(256, 'Bio must be no more than 256 characters') 16 | .nullable() 17 | .optional(), 18 | }); 19 | 20 | export type DisplayNameSchema = z.infer; 21 | export type BioSchema = z.infer; 22 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /src/app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../../app/globals.css'; 2 | import { ThemeProvider } from '@/components/Providers/ThemeProvider'; 3 | import QueryProvider from '@/components/Providers/QueryProvider'; 4 | 5 | // This layout doesn't use a or tag because it's nested 6 | // inside the root layout which already includes those tags. 7 | export default async function MainLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | return ( 13 | 14 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Buttons/SignOutBtn.tsx: -------------------------------------------------------------------------------- 1 | import useUser from '@/hooks/useUser/useUser'; 2 | import { Button } from '@/components/ui/button'; 3 | import { Skeleton } from '@/components/ui/skeleton'; 4 | import { LogOut } from 'lucide-react'; 5 | 6 | export default function SignOutBtn() { 7 | const { signOutUser, data, isLoading } = useUser(); 8 | 9 | if (isLoading) { 10 | return ; 11 | } 12 | 13 | if (!data?.id) { 14 | return null; 15 | } 16 | 17 | return ( 18 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { updateSession } from '../utils/supabase/middleware'; 2 | import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'; 3 | import { NextRequest, NextResponse } from 'next/server'; 4 | 5 | export async function middleware(req: NextRequest) { 6 | return await updateSession(req); 7 | } 8 | 9 | export const config = { 10 | matcher: [ 11 | /* 12 | * Match all request paths except for the ones starting with: 13 | * - _next/static (static files) 14 | * - _next/image (image optimization files) 15 | * - favicon.ico (favicon file) 16 | * Feel free to modify this pattern to include more paths. 17 | */ 18 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/seeds/skills.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../seed'; 2 | 3 | export async function seedSkills() { 4 | // List of the skills to seed in the database: with name 5 | const skills = [ 6 | { name: 'JavaScript' }, 7 | { name: 'TypeScript' }, 8 | { name: 'React' }, 9 | { name: 'Node.js' }, 10 | { name: 'Express' }, 11 | { name: 'MongoDB' }, 12 | { name: 'SQL' }, 13 | { name: 'GraphQL' }, 14 | { name: 'REST' }, 15 | { name: 'HTML' }, 16 | { name: 'CSS' }, 17 | { name: 'TailwindCSS' }, 18 | ]; 19 | 20 | // Insert into skills 21 | const { error } = await supabase.from('skills').upsert(skills); 22 | if (error) throw new Error(`Failed to seed skills: ${error.message}`); 23 | console.info(`Seeded ${skills.length} skills`); 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yml: -------------------------------------------------------------------------------- 1 | name: Prettier Formatting Check 2 | 3 | on: 4 | pull_request: 5 | branches: [main, development] 6 | 7 | jobs: 8 | prettier: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v5 14 | 15 | - name: Setup pnpm 16 | uses: pnpm/action-setup@v4 17 | with: 18 | version: 10.0.0 19 | 20 | - name: Set up Node 21 | uses: actions/setup-node@v5 22 | with: 23 | node-version: '22' 24 | cache: 'pnpm' 25 | 26 | - name: Install dependencies 27 | run: pnpm install --frozen-lockfile 28 | 29 | - name: Run Prettier 30 | run: pnpx prettier --config .prettierrc.yml --check . --ignore-path .prettierignore 31 | -------------------------------------------------------------------------------- /supabase/functions/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "lib": ["deno.window"], 5 | "strict": true 6 | }, 7 | "lint": { 8 | "rules": { 9 | "tags": ["recommended"] 10 | } 11 | }, 12 | "fmt": { 13 | "useTabs": false, 14 | "lineWidth": 80, 15 | "indentWidth": 2, 16 | "semiColons": true, 17 | "singleQuote": true, 18 | "proseWrap": "preserve" 19 | }, 20 | "imports": { 21 | "supabase": "jsr:@supabase/supabase-js@2", 22 | "@supabase/functions-js": "jsr:@supabase/functions-js@2", 23 | "@supabase/functions-js/": "jsr:@supabase/functions-js@2/", 24 | "cors": "https://deno.land/x/cors@v1.2.2/mod.ts" 25 | }, 26 | "tasks": { 27 | "start": "deno run --allow-net --allow-read --allow-env index.ts" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/staging-auth/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { cookies } from 'next/headers'; 4 | import { redirect } from 'next/navigation'; 5 | import { StagingAuthSchemaType } from './stagingAuth.schema'; 6 | import { timingSafeEqual } from 'crypto'; 7 | 8 | export async function authenticateStaging(formData: StagingAuthSchemaType) { 9 | const { password } = formData; 10 | 11 | if ( 12 | timingSafeEqual( 13 | Buffer.from(password), 14 | Buffer.from(process.env.NEXT_PUBLIC_STAGING_PASS!) 15 | ) 16 | ) { 17 | (await cookies()).set('staging-auth', 'true', { 18 | maxAge: 60 * 60 * 2, 19 | httpOnly: true, 20 | secure: true, 21 | sameSite: 'lax', 22 | path: '/', 23 | }); 24 | 25 | redirect('/'); 26 | } 27 | 28 | return { success: false }; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(main)/dashboard/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Avatar } from '@/components/Profile/Avatar'; 4 | import useUser from '@/hooks/useUser/useUser'; 5 | 6 | export default function Profile() { 7 | const { isLoading, data } = useUser(); 8 | 9 | if (isLoading) { 10 | return <>; 11 | } 12 | return ( 13 |
14 | {!data?.id ? ( 15 |

profile

16 | ) : ( 17 | 22 | )} 23 | 24 |

25 | {data?.username} 26 |

27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths(), react()], 7 | test: { 8 | environment: 'jsdom', 9 | // Only include app unit tests 10 | include: ['src/**/*.test.{ts,tsx}', 'src/**/__tests__/**/*.{ts,tsx}'], 11 | // Explicitly exclude any Supabase policy tests 12 | exclude: ['node_modules', 'dist', 'coverage', 'supabase/**'], 13 | coverage: { 14 | provider: 'v8', 15 | reporter: ['text', 'lcov', 'html'], 16 | reportsDirectory: './coverage', 17 | // optional: enforce a basic gate 18 | // thresholds: { lines: 70, functions: 70, branches: 60, statements: 70 }, 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/(main)/[username]/project/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import BackButton from '@/components/Buttons/BackButton'; 4 | import { ProjectPanel } from '@/components/Projects/ProjectPanel/ProjectPanel'; 5 | 6 | interface ProjectProps { 7 | params: Promise<{ 8 | id: string; 9 | }>; 10 | } 11 | 12 | export default async function Project({ params }: ProjectProps) { 13 | const { id } = await params; 14 | 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /supabase/migrations/20250831000000_add_project_image_refs.sql: -------------------------------------------------------------------------------- 1 | -- Add image fields to projects table 2 | ALTER TABLE projects 3 | ADD COLUMN IF NOT EXISTS primary_image UUID references public.images (id), 4 | ADD COLUMN IF NOT EXISTS gallery_images UUID[] default array[]::UUID[]; 5 | 6 | -- Add policies for image fields 7 | ALTER TABLE projects ENABLE ROW LEVEL SECURITY; 8 | 9 | -- Policy for viewing project images (anyone can view public projects) 10 | CREATE POLICY "View project images" 11 | ON projects 12 | FOR SELECT 13 | USING ( 14 | visibility = 'public'::project_visibility 15 | OR owner_id = auth.uid() 16 | ); 17 | 18 | -- Policy for updating project images (only owner can update) 19 | CREATE POLICY "Update project images" 20 | ON projects 21 | FOR UPDATE 22 | USING (owner_id = auth.uid()) 23 | WITH CHECK (owner_id = auth.uid()); -------------------------------------------------------------------------------- /src/app/(main)/[username]/components/StreakSection.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import GitHubCalendar, { Activity } from 'react-github-calendar'; 3 | 4 | type Contribution = { 5 | date: string; 6 | count: number; 7 | level: number; 8 | }; 9 | 10 | const StreakSection = () => { 11 | const [view, setView] = useState<'calendar' | 'github'>('calendar'); 12 | 13 | const selectLastHalfYear = (contributions: Contribution[]): Activity[] => { 14 | const now = new Date(); 15 | const sixMonthsAgo = new Date(now.setMonth(now.getMonth() - 6)); 16 | 17 | return contributions.filter((activity) => { 18 | const date = new Date(activity.date); 19 | return date >= sixMonthsAgo; 20 | }) as Activity[]; 21 | }; 22 | 23 | return
; 24 | }; 25 | 26 | export default StreakSection; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "checkJs": false, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "module": "esnext", 12 | "esModuleInterop": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["src/*"] 25 | } 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | ".next/types/**/*.ts", 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "utils/**/*.ts" 33 | ], 34 | "exclude": ["node_modules", "**/*.js", "**/*.jsx"] 35 | } 36 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | title = "BuiltInPublic Secret Scan Config" 2 | enableDefaultRules = true 3 | 4 | [allowlist] 5 | description = "Allowlist for known safe secrets and files" 6 | 7 | files = [ 8 | "^\\.env\\.example$", 9 | "^scripts/seeds/.*\\.ts$", 10 | "^gitleaks-report\\.json$", 11 | "^seeds\\.sql$" 12 | ] 13 | 14 | paths = [ 15 | "scripts/seeds", 16 | "supabase/fixtures" 17 | ] 18 | 19 | commitAuthors = ["dependabot"] 20 | 21 | [[rules]] 22 | id = "aws-access-key" 23 | description = "AWS Access Key" 24 | regex = '''AKIA[0-9A-Z]{16}''' 25 | tags = ["key", "AWS"] 26 | 27 | [[rules]] 28 | id = "stripe-api-key" 29 | description = "Stripe API Key" 30 | regex = '''sk_live_[0-9a-zA-Z]{24}''' 31 | tags = ["key", "Stripe"] 32 | 33 | [[rules]] 34 | id = "github-pat" 35 | description = "GitHub Personal Access Token" 36 | regex = '''ghp_[A-Za-z0-9_]{36,255}''' 37 | tags = ["key", "GitHub"] 38 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |