├── .idea ├── .gitignore ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── supabase-nextjs-template.iml └── vcs.xml ├── LICENSE ├── README.md ├── nextjs ├── .env.template ├── .gitignore ├── .idea │ ├── .gitignore │ ├── inspectionProfiles │ │ └── Project_Default.xml │ ├── modules.xml │ ├── nextjs.iml │ └── vcs.xml ├── README.md ├── components.json ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── terms │ │ ├── privacy-notice.md │ │ ├── refund-policy.md │ │ └── terms-of-service.md │ ├── vercel.svg │ └── window.svg ├── src │ ├── app │ │ ├── api │ │ │ └── auth │ │ │ │ └── callback │ │ │ │ └── route.ts │ │ ├── app │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ ├── storage │ │ │ │ └── page.tsx │ │ │ ├── table │ │ │ │ └── page.tsx │ │ │ └── user-settings │ │ │ │ └── page.tsx │ │ ├── auth │ │ │ ├── 2fa │ │ │ │ └── page.tsx │ │ │ ├── forgot-password │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── register │ │ │ │ └── page.tsx │ │ │ ├── reset-password │ │ │ │ └── page.tsx │ │ │ └── verify-email │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── legal │ │ │ ├── [document] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── components │ │ ├── AppLayout.tsx │ │ ├── AuthAwareButtons.tsx │ │ ├── Confetti.tsx │ │ ├── Cookies.tsx │ │ ├── HomePricing.tsx │ │ ├── LegalDocument.tsx │ │ ├── LegalDocuments.tsx │ │ ├── MFASetup.tsx │ │ ├── MFAVerification.tsx │ │ ├── SSOButtons.tsx │ │ └── ui │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input.tsx │ │ │ └── textarea.tsx │ ├── lib │ │ ├── context │ │ │ └── GlobalContext.tsx │ │ ├── pricing.ts │ │ ├── supabase │ │ │ ├── client.ts │ │ │ ├── middleware.ts │ │ │ ├── server.ts │ │ │ ├── serverAdminClient.ts │ │ │ └── unified.ts │ │ ├── types.ts │ │ └── utils.ts │ └── middleware.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock └── supabase ├── .gitignore ├── config.toml ├── migrations ├── 20250107210416_MFA.sql ├── 20250130165844_example_storage.sql ├── 20250130181110_storage_policies.sql └── 20250130181641_todo_list.sql └── migrations_for_old ├── 20250107210416_MFA.sql ├── 20250130165844_example_storage.sql ├── 20250130181110_storage_policies.sql ├── 20250130181641_todo_list.sql └── 20250525183944_auth_removal.sql /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/supabase-nextjs-template.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Supabase Next.js SaaS Template 2 | 3 | A production-ready SaaS template built with Next.js 15, Supabase, and Tailwind CSS. This template provides everything you need to quickly launch your SaaS product, including authentication, user management, file storage, and more. 4 | 5 | ## LIVE DEMO 6 | 7 | Demo is here - https://basicsass.razikus.com 8 | 9 | 10 | ## Deployment video 11 | 12 | Video is here - https://www.youtube.com/watch?v=kzbXavLndmE 13 | 14 | ## Migration from auth schema 15 | 16 | According to this - https://github.com/Razikus/supabase-nextjs-template/issues/4 17 | 18 | We are no longer able to modify auth schema. I modified original migrations to rename it to custom schema. If you need to migrate from older version - check supabase/migrations_for_old/20250525183944_auth_removal.sql 19 | 20 | ## SupaNuggets 21 | 22 | On top of this template I'm building a SupaNuggets series - 50 mini apps 23 | 24 | https://supanuggets.razikus.com - grab your copy for free :) (Pay As You Want model) 25 | 26 | ## 🚀 Features 27 | 28 | - **Authentication** 29 | - Email/Password authentication 30 | - Multi-factor authentication (MFA) support 31 | - OAuth/SSO integration ready 32 | - Password reset and email verification 33 | 34 | - **User Management** 35 | - User profiles and settings 36 | - Secure password management 37 | - Session handling 38 | 39 | - **File Management Demo (2FA ready)** 40 | - Secure file upload and storage 41 | - File sharing capabilities 42 | - Drag-and-drop interface 43 | - Progress tracking 44 | 45 | - **Task Management Demo (2FA ready)** 46 | - CRUD operations example 47 | - Real-time updates 48 | - Filtering and sorting 49 | - Row-level security 50 | 51 | - **Security** 52 | - Row Level Security (RLS) policies 53 | - Secure file storage policies 54 | - Protected API routes 55 | - MFA implementation 56 | 57 | - **UI/UX** 58 | - Modern, responsive design 59 | - Dark mode support 60 | - Loading states 61 | - Error handling 62 | - Toast notifications 63 | - Confetti animations 64 | 65 | - **Legal & Compliance** 66 | - Privacy Policy template 67 | - Terms of Service template 68 | - Refund Policy template 69 | - GDPR-ready cookie consent 70 | 71 | ## 🛠️ Tech Stack 72 | 73 | - **Frontend** 74 | - Next.js 15 (App Router) 75 | - React 19 76 | - Tailwind CSS 77 | - shadcn/ui components 78 | - Lucide icons 79 | 80 | - **Backend** 81 | - Supabase 82 | - PostgreSQL 83 | - Row Level Security 84 | - Storage Buckets 85 | 86 | - **Authentication** 87 | - Supabase Auth 88 | - MFA support 89 | - OAuth providers 90 | 91 | ## 📦 Getting Started - local dev 92 | 93 | 1. Fork or clone repository 94 | 2. Prepare Supabase Project URL (Project URL from `Project Settings` -> `API` -> `Project URL`) 95 | 3. Prepare Supabase Anon and Service Key (`Anon Key`, `Service Key` from `Project Settings` -> `API` -> `anon public` and `service_role`) 96 | 4. Prepare Supabase Database Password (You can reset it inside `Project Settings` -> `Database` -> `Database Password`) 97 | 5. If you already know your app url -> adjust supabase/config.toml `site_url` and `additional_redirect_urls`, you can do it later 98 | 6. Run following commands (inside root of forked / downloaded repository): 99 | 100 | ```bash 101 | # Login to supabase 102 | npx supabase login 103 | # Link project to supabase (require database password) - you will get selector prompt 104 | npx supabase link 105 | 106 | # Send config to the server - may require confirmation (y) 107 | npx supabase config push 108 | 109 | # Up migrations 110 | npx supabase migrations up --linked 111 | 112 | ``` 113 | 114 | 7. Go to next/js folder and run `yarn` 115 | 8. Copy .env.template to .env.local 116 | 9. Adjust .env.local 117 | ``` 118 | NEXT_PUBLIC_SUPABASE_URL=https://APIURL 119 | NEXT_PUBLIC_SUPABASE_ANON_KEY=ANONKEY 120 | PRIVATE_SUPABASE_SERVICE_KEY=SERVICEROLEKEY 121 | 122 | ``` 123 | 10. Run yarn dev 124 | 11. Go to http://localhost:3000 🎉 125 | 126 | ## 🚀 Getting Started - deploy to vercel 127 | 128 | 1. Fork or clone repository 129 | 2. Create project in Vercel - choose your repo 130 | 3. Paste content of .env.local into environment variables 131 | 4. Click deploy 132 | 5. Adjust in supabase/config.toml site_url and additional_redirect_urls (important in additional_redirect_urls is to have https://YOURURL/** - these 2 **) 133 | 6. Done! 134 | 135 | ## 📄 Legal Documents 136 | 137 | The template includes customizable legal documents - these are in markdown, so you can adjust them as you see fit: 138 | 139 | - Privacy Policy (`/public/terms/privacy-notice.md`) 140 | - Terms of Service (`/public/terms/terms-of-service.md`) 141 | - Refund Policy (`/public/terms/refund-policy.md`) 142 | 143 | ## 🎨 Theming 144 | 145 | The template includes several pre-built themes: 146 | - `theme-sass` (Default) 147 | - `theme-blue` 148 | - `theme-purple` 149 | - `theme-green` 150 | 151 | Change the theme by updating the `NEXT_PUBLIC_THEME` environment variable. 152 | 153 | ## 🤝 Contributing 154 | 155 | Contributions are welcome! Please feel free to submit a Pull Request. 156 | 157 | 158 | ## Need Multitenancy, Billing (Paddle) and Role Based Access Control? 159 | 160 | I have paid template as well available here: 161 | 162 | https://sasstemplate.razikus.com 163 | 164 | Basically it's the same template but with Paddle + organisations API keys + multiple organisations + Role Based Access Control 165 | 166 | For code GITHUB you can get -50% off 167 | 168 | https://razikus.gumroad.com/l/supatemplate/GITHUB 169 | 170 | ## 📝 License 171 | 172 | This project is licensed under the Apache License - see the LICENSE file for details. 173 | 174 | ## 💪 Support 175 | 176 | If you find this template helpful, please consider giving it a star ⭐️ 177 | 178 | Or buy me a coffee! 179 | 180 | - [BuyMeACoffee](https://buymeacoffee.com/razikus) 181 | 182 | My socials: 183 | 184 | - [Twitter](https://twitter.com/Razikus_) 185 | - [GitHub](https://github.com/Razikus) 186 | - [Website](https://www.razikus.com) 187 | 188 | 189 | ## 🙏 Acknowledgments 190 | 191 | - [Next.js](https://nextjs.org/) 192 | - [Supabase](https://supabase.com/) 193 | - [Tailwind CSS](https://tailwindcss.com/) 194 | - [shadcn/ui](https://ui.shadcn.com/) 195 | -------------------------------------------------------------------------------- /nextjs/.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUPABASE_URL=https://YOURSUPABASE.supabase.co 2 | NEXT_PUBLIC_SUPABASE_ANON_KEY=YYY 3 | PRIVATE_SUPABASE_SERVICE_KEY=XXX 4 | NEXT_PUBLIC_PRODUCTNAME=BasicSass 5 | NEXT_PUBLIC_SSO_PROVIDERS=github,google,facebook,apple 6 | 7 | 8 | NEXT_PUBLIC_GOOGLE_TAG=G-GOOGLETAG 9 | NEXT_PUBLIC_THEME=theme-sass 10 | 11 | 12 | NEXT_PUBLIC_TIERS_NAMES=Basic,Growth,Max 13 | NEXT_PUBLIC_TIERS_PRICES=99,199,299 14 | NEXT_PUBLIC_TIERS_DESCRIPTIONS=Perfect for getting started,Best for growing teams,For enterprise-grade needs 15 | NEXT_PUBLIC_TIERS_FEATURES=14 day free trial|30 PDF files,14 day free trial|1000 PDF files,14 day free trial|Unlimited PDF files 16 | NEXT_PUBLIC_POPULAR_TIER=Growth 17 | NEXT_PUBLIC_COMMON_FEATURES=SSL security,unlimited updates,premium support -------------------------------------------------------------------------------- /nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /nextjs/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /nextjs/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /nextjs/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nextjs/.idea/nextjs.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /nextjs/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /nextjs/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 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 | } -------------------------------------------------------------------------------- /nextjs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /nextjs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sasstemplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/third-parties": "^15.1.5", 13 | "@paddle/paddle-js": "^1.3.3", 14 | "@paddle/paddle-node-sdk": "^2.3.2", 15 | "@radix-ui/react-alert-dialog": "^1.1.5", 16 | "@radix-ui/react-dialog": "^1.1.5", 17 | "@radix-ui/react-slot": "^1.1.1", 18 | "@supabase/ssr": "^0.5.2", 19 | "@supabase/supabase-js": "^2.47.10", 20 | "@vercel/analytics": "^1.4.1", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.1", 23 | "cookies-next": "^5.0.2", 24 | "lucide-react": "^0.469.0", 25 | "next": "15.1.3", 26 | "react": "^19.0.0", 27 | "react-dom": "^19.0.0", 28 | "react-markdown": "^9.0.3", 29 | "recharts": "^2.15.0", 30 | "tailwind-merge": "^2.6.0", 31 | "tailwindcss-animate": "^1.0.7" 32 | }, 33 | "devDependencies": { 34 | "@eslint/eslintrc": "^3", 35 | "@types/node": "^20", 36 | "@types/react": "^19", 37 | "@types/react-dom": "^19", 38 | "eslint": "^9", 39 | "eslint-config-next": "15.1.3", 40 | "postcss": "^8", 41 | "tailwindcss": "^3.4.1", 42 | "typescript": "^5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /nextjs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /nextjs/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs/public/terms/privacy-notice.md: -------------------------------------------------------------------------------- 1 | # Privacy Notice 2 | 3 | Last Updated: December 14, 2024 4 | 5 | ## 1. Company Information 6 | 7 | This Privacy Notice applies to: 8 | Company Name: 9 | 10 | Tax number: 1234567890 11 | 12 | ## 2. Introduction 13 | 14 | This Privacy Notice explains how we collect, use, disclose, and safeguard your information when you use SupaSasS services. 15 | 16 | ## 3. Information We Collect 17 | 18 | ### 3.1 Personal Information 19 | - Email address 20 | - Account credentials 21 | - Payment information (processed by Paddle) 22 | - Usage data 23 | 24 | ### 3.2 Business Information 25 | - Shopify store data (when using analysis tools) 26 | - Domain search queries 27 | - Analytics data 28 | 29 | ### 3.3 Technical Information 30 | - IP address 31 | - Browser type 32 | - Device information 33 | - Cookies and similar technologies 34 | 35 | ## 4. How We Use Your Information 36 | 37 | We use collected information for: 38 | - Providing and maintaining our services 39 | - Processing payments 40 | - Improving our services 41 | - Communicating with you 42 | - Analyzing usage patterns 43 | - Preventing fraud 44 | 45 | ## 5. Data Storage and Security 46 | 47 | ### 5.1 Storage 48 | - Data is stored on secure servers 49 | - We use Supabase for database management 50 | - Payment data is handled by Paddle 51 | 52 | ### 5.2 Security Measures 53 | - Encryption in transit and at rest 54 | - Regular security audits 55 | - Access controls and monitoring 56 | 57 | ## 6. Data Sharing and Disclosure 58 | 59 | We may share information with: 60 | - Service providers (Paddle, Supabase) 61 | - Legal authorities when required 62 | - Business partners with your consent 63 | 64 | ## 7. Your Rights 65 | 66 | You have the right to: 67 | - Access your personal data 68 | - Correct inaccurate data 69 | - Request data deletion 70 | - Object to processing 71 | - Data portability 72 | 73 | ## 8. Cookies and Tracking 74 | 75 | We use cookies for: 76 | - Authentication 77 | - Preferences 78 | - Analytics 79 | - Marketing (with consent) 80 | 81 | ## 9. International Data Transfers 82 | 83 | - Data may be processed in various locations 84 | - We ensure appropriate safeguards 85 | - EU data protection standards applied 86 | 87 | ## 10. Children's Privacy 88 | 89 | - Services not intended for users under 18 90 | - We do not knowingly collect children's data 91 | - Parents may request data deletion 92 | 93 | ## 11. Changes to Privacy Notice 94 | 95 | - We may update this notice periodically 96 | - Changes effective upon posting 97 | - Material changes notified via email 98 | 99 | ## 12. Contact Information 100 | 101 | For privacy-related questions: 102 | Email: contact@supasass.com 103 | 104 | -------------------------------------------------------------------------------- /nextjs/public/terms/refund-policy.md: -------------------------------------------------------------------------------- 1 | # Refund Policy 2 | 3 | Last Updated: December 14, 2024 4 | 5 | ## 1. Company Information 6 | 7 | Company Name: 8 | 9 | Tax number: 1234567890 10 | 11 | ## 2. General Policy 12 | 13 | We strive to ensure customer satisfaction with all our digital services and products. This policy outlines our procedures for refunds and cancellations. 14 | 15 | ## 3. Subscription Services 16 | 17 | ### 3.1 Cancellation 18 | - You may cancel your subscription at any time 19 | - Access continues until the end of the billing period 20 | - No partial refunds for unused time 21 | 22 | ### 3.2 Automatic Renewals 23 | - Subscriptions renew automatically 24 | - Cancel at least 24 hours before renewal 25 | - No refunds for automatic renewals 26 | 27 | ## 4. Data Products 28 | 29 | ### 4.1 Digital Products 30 | - Data products are non-refundable once delivered 31 | - Sample data available before purchase 32 | - Technical support provided for access issues 33 | 34 | ### 4.2 Custom Data Solutions 35 | - Custom orders are non-refundable 36 | - Requirements confirmed before processing 37 | - Revision period available 38 | 39 | ## 5. Refund Eligibility 40 | 41 | Refunds may be considered for: 42 | - Technical issues preventing access 43 | - Service unavailability 44 | - Incorrect charges 45 | - Legal requirements 46 | 47 | ## 6. Refund Process 48 | 49 | To request a refund: 50 | 1. Contact support within 14 days 51 | 2. Provide order details 52 | 3. Explain refund reason 53 | 4. Allow 5-10 business days for processing 54 | 55 | ## 7. Payment Processing 56 | 57 | ### 7.1 Refund Method 58 | - Refunds processed through original payment method 59 | - Processed via Paddle 60 | - Currency same as original payment 61 | 62 | ### 7.2 Processing Time 63 | - 2-5 business days for approval 64 | - 5-10 business days for bank processing 65 | - May vary by payment method 66 | 67 | ## 8. Exceptions 68 | 69 | No refunds for: 70 | - Used or accessed digital products 71 | - Expired subscriptions 72 | - Violations of Terms of Service 73 | - Fraudulent activities 74 | 75 | ## 9. Customer Support 76 | 77 | For refund requests contact: 78 | Email: contact@komiru.com 79 | 80 | ## 10. Changes to Policy 81 | 82 | - We reserve the right to modify this policy 83 | - Changes effective upon posting 84 | - Current customers notified of material changes 85 | 86 | -------------------------------------------------------------------------------- /nextjs/public/terms/terms-of-service.md: -------------------------------------------------------------------------------- 1 | # Terms of Service 2 | 3 | Last Updated: December 14, 2024 4 | 5 | ## 1. Company Information 6 | 7 | Company Name: 8 | 9 | Tax number: 1234567890 10 | 11 | ## 2. Introduction 12 | 13 | Welcome to SupaSasS ("we," "our," or "us"). These Terms of Service ("Terms") govern your access to and use of our website, applications, and services (collectively, the "Services"), including AI-powered tools, data analysis features, and subscription services. 14 | 15 | ## 3. Acceptance of Terms 16 | 17 | By accessing or using our Services, you agree to be bound by these Terms. If you disagree with any part of these terms, you may not access the Services. 18 | 19 | ## 4. Services Description 20 | 21 | Our Services include: 22 | - AI-powered domain search tools 23 | - Shopify store analysis 24 | - Data marketplace access 25 | - Customer analytics tools 26 | - Multi-language support features 27 | 28 | ## 5. Account Registration and Security 29 | 30 | ### 5.1 Account Creation 31 | - You must register for an account to access most features 32 | - You must provide accurate, current, and complete information 33 | - You must be at least 18 years old to create an account 34 | 35 | ### 5.2 Account Security 36 | - You are responsible for maintaining the confidentiality of your account credentials 37 | - You must notify us immediately of any unauthorized access 38 | - We reserve the right to disable accounts that violate our terms 39 | 40 | ## 6. Subscription Terms 41 | 42 | ### 6.1 Billing 43 | - Subscription fees are billed in advance through Paddle 44 | - Prices are listed in EUR/USD and may be subject to local taxes 45 | - We reserve the right to change pricing with 30 days notice 46 | 47 | ### 6.2 Cancellation 48 | - You may cancel your subscription at any time through your account dashboard 49 | - Refunds are subject to our Refund Policy 50 | - Cancellation will take effect at the end of the current billing period 51 | 52 | ## 7. Data Usage and Privacy 53 | 54 | ### 7.1 Data Collection 55 | - We collect and process data as described in our Privacy Policy 56 | - You retain ownership of your content and data 57 | - We use data to improve and maintain our services 58 | 59 | ### 7.2 AI Analysis 60 | - Our AI tools analyze data you provide 61 | - Analysis results are for informational purposes only 62 | - We do not guarantee accuracy of AI-generated recommendations 63 | 64 | ## 8. Intellectual Property 65 | 66 | ### 8.1 Our Rights 67 | - All content, features, and functionality are owned by Adam Raźniewski oprogramowanie 68 | - Our trademarks and trade dress may not be used without written permission 69 | 70 | ### 8.2 Your Rights 71 | - You retain all rights to data you upload 72 | - You grant us license to use your data to provide services 73 | 74 | ## 9. Limitations of Liability 75 | 76 | - Services are provided "as is" without warranties 77 | - We are not liable for any indirect, incidental, or consequential damages 78 | - Our liability is limited to the amount paid for services in the past 12 months 79 | 80 | ## 10. Termination 81 | 82 | We reserve the right to terminate or suspend access to our Services: 83 | - For violations of these Terms 84 | - For fraudulent or illegal activities 85 | - At our sole discretion with reasonable notice 86 | 87 | ## 11. Changes to Terms 88 | 89 | We may modify these Terms at any time: 90 | - Changes will be effective upon posting 91 | - Continued use constitutes acceptance of changes 92 | - Material changes will be notified via email 93 | 94 | ## 12. Governing Law 95 | 96 | These Terms shall be governed by and construed in accordance with the laws of Poland, without regard to its conflict of law provisions. 97 | 98 | ## 13. Contact Information 99 | 100 | For any questions about these Terms, please contact us at: 101 | Email: contact@supasass.com 102 | 103 | -------------------------------------------------------------------------------- /nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nextjs/src/app/api/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | // src/app/api/auth/callback/route.ts 2 | import { NextResponse } from 'next/server' 3 | import { createSSRSassClient } from "@/lib/supabase/server"; 4 | 5 | export async function GET(request: Request) { 6 | const requestUrl = new URL(request.url) 7 | const code = requestUrl.searchParams.get('code') 8 | 9 | if (code) { 10 | const supabase = await createSSRSassClient() 11 | const client = supabase.getSupabaseClient() 12 | 13 | // Exchange the code for a session 14 | await supabase.exchangeCodeForSession(code) 15 | 16 | // Check MFA status 17 | const { data: aal, error: aalError } = await client.auth.mfa.getAuthenticatorAssuranceLevel() 18 | 19 | if (aalError) { 20 | console.error('Error checking MFA status:', aalError) 21 | return NextResponse.redirect(new URL('/auth/login', request.url)) 22 | } 23 | 24 | // If user needs to complete MFA verification 25 | if (aal.nextLevel === 'aal2' && aal.nextLevel !== aal.currentLevel) { 26 | return NextResponse.redirect(new URL('/auth/2fa', request.url)) 27 | } 28 | 29 | // If MFA is not required or already verified, proceed to app 30 | return NextResponse.redirect(new URL('/app', request.url)) 31 | } 32 | 33 | // If no code provided, redirect to login 34 | return NextResponse.redirect(new URL('/auth/login', request.url)) 35 | } -------------------------------------------------------------------------------- /nextjs/src/app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // src/app/app/layout.tsx 2 | import AppLayout from '@/components/AppLayout'; 3 | import { GlobalProvider } from '@/lib/context/GlobalContext'; 4 | 5 | export default function Layout({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | } -------------------------------------------------------------------------------- /nextjs/src/app/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import { useGlobal } from '@/lib/context/GlobalContext'; 4 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; 5 | import { CalendarDays, Settings, ExternalLink } from 'lucide-react'; 6 | import Link from 'next/link'; 7 | 8 | export default function DashboardContent() { 9 | const { loading, user } = useGlobal(); 10 | 11 | const getDaysSinceRegistration = () => { 12 | if (!user?.registered_at) return 0; 13 | const today = new Date(); 14 | const diffTime = Math.abs(today.getTime() - user.registered_at.getTime()); 15 | return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 16 | }; 17 | 18 | if (loading) { 19 | return ( 20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | const daysSinceRegistration = getDaysSinceRegistration(); 27 | 28 | return ( 29 |
30 | 31 | 32 | Welcome, {user?.email?.split('@')[0]}! 👋 33 | 34 | 35 | Member for {daysSinceRegistration} days 36 | 37 | 38 | 39 | 40 | {/* Quick Actions */} 41 | 42 | 43 | Quick Actions 44 | Frequently used features 45 | 46 | 47 |
48 | 52 |
53 | 54 |
55 |
56 |

User Settings

57 |

Manage your account preferences

58 |
59 | 60 | 61 | 65 |
66 | 67 |
68 |
69 |

Example Page

70 |

Check out example features

71 |
72 | 73 |
74 |
75 |
76 |
77 | ); 78 | } -------------------------------------------------------------------------------- /nextjs/src/app/app/user-settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from 'react'; 3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; 4 | import { Alert, AlertDescription } from '@/components/ui/alert'; 5 | import { useGlobal } from '@/lib/context/GlobalContext'; 6 | import { createSPASassClient } from '@/lib/supabase/client'; 7 | import { Key, User, CheckCircle } from 'lucide-react'; 8 | import { MFASetup } from '@/components/MFASetup'; 9 | 10 | export default function UserSettingsPage() { 11 | const { user } = useGlobal(); 12 | const [newPassword, setNewPassword] = useState(''); 13 | const [confirmPassword, setConfirmPassword] = useState(''); 14 | const [loading, setLoading] = useState(false); 15 | const [error, setError] = useState(''); 16 | const [success, setSuccess] = useState(''); 17 | 18 | 19 | 20 | const handlePasswordChange = async (e: React.FormEvent) => { 21 | e.preventDefault(); 22 | if (newPassword !== confirmPassword) { 23 | setError("New passwords don't match"); 24 | return; 25 | } 26 | 27 | setLoading(true); 28 | setError(''); 29 | setSuccess(''); 30 | 31 | try { 32 | const supabase = await createSPASassClient(); 33 | const client = supabase.getSupabaseClient(); 34 | 35 | const { error } = await client.auth.updateUser({ 36 | password: newPassword 37 | }); 38 | 39 | if (error) throw error; 40 | 41 | setSuccess('Password updated successfully'); 42 | setNewPassword(''); 43 | setConfirmPassword(''); 44 | } catch (err: Error | unknown) { 45 | if (err instanceof Error) { 46 | console.error('Error updating password:', err); 47 | setError(err.message); 48 | } else { 49 | console.error('Error updating password:', err); 50 | setError('Failed to update password'); 51 | } 52 | } finally { 53 | setLoading(false); 54 | } 55 | }; 56 | 57 | 58 | 59 | return ( 60 |
61 |
62 |

User Settings

63 |

64 | Manage your account settings and preferences 65 |

66 |
67 | 68 | {error && ( 69 | 70 | {error} 71 | 72 | )} 73 | 74 | {success && ( 75 | 76 | 77 | {success} 78 | 79 | )} 80 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | User Details 88 | 89 | Your account information 90 | 91 | 92 |
93 | 94 |

{user?.id}

95 |
96 |
97 | 98 |

{user?.email}

99 |
100 |
101 |
102 | 103 | 104 | 105 | 106 | 107 | Change Password 108 | 109 | Update your account password 110 | 111 | 112 |
113 |
114 | 117 | setNewPassword(e.target.value)} 122 | className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 text-sm" 123 | required 124 | /> 125 |
126 |
127 | 130 | setConfirmPassword(e.target.value)} 135 | className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 text-sm" 136 | required 137 | /> 138 |
139 | 146 |
147 |
148 |
149 | 150 | { 152 | setSuccess('Two-factor authentication settings updated successfully'); 153 | }} 154 | /> 155 |
156 |
157 |
158 | ); 159 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/2fa/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/auth/2fa/page.tsx 2 | 'use client'; 3 | 4 | import { useState, useEffect } from 'react'; 5 | import { useRouter } from 'next/navigation'; 6 | import { createSPASassClient } from '@/lib/supabase/client'; 7 | import { MFAVerification } from '@/components/MFAVerification'; 8 | 9 | export default function TwoFactorAuthPage() { 10 | const router = useRouter(); 11 | const [loading, setLoading] = useState(true); 12 | const [error, setError] = useState(''); 13 | 14 | useEffect(() => { 15 | checkMFAStatus(); 16 | }, []); 17 | 18 | const checkMFAStatus = async () => { 19 | try { 20 | const supabase = await createSPASassClient(); 21 | const client = supabase.getSupabaseClient(); 22 | 23 | const { data: { user }, error: sessionError } = await client.auth.getUser(); 24 | if (sessionError || !user) { 25 | router.push('/auth/login'); 26 | return; 27 | } 28 | 29 | const { data: aal, error: aalError } = await client.auth.mfa.getAuthenticatorAssuranceLevel(); 30 | 31 | if (aalError) throw aalError; 32 | 33 | if (aal.currentLevel === 'aal2' || aal.nextLevel === 'aal1') { 34 | router.push('/app'); 35 | return; 36 | } 37 | 38 | setLoading(false); 39 | } catch (err) { 40 | setError(err instanceof Error ? err.message : 'An error occurred'); 41 | setLoading(false); 42 | } 43 | }; 44 | 45 | const handleVerified = () => { 46 | router.push('/app'); 47 | }; 48 | 49 | if (loading) { 50 | return ( 51 |
52 |
53 |
54 | ); 55 | } 56 | 57 | if (error) { 58 | return ( 59 |
60 |
{error}
61 |
62 | ); 63 | } 64 | 65 | return ( 66 |
67 | 68 |
69 | ); 70 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { createSPASassClient } from '@/lib/supabase/client'; 5 | import Link from 'next/link'; 6 | import { CheckCircle } from 'lucide-react'; 7 | 8 | export default function ForgotPasswordPage() { 9 | const [email, setEmail] = useState(''); 10 | const [error, setError] = useState(''); 11 | const [loading, setLoading] = useState(false); 12 | const [success, setSuccess] = useState(false); 13 | 14 | const handleSubmit = async (e: React.FormEvent) => { 15 | e.preventDefault(); 16 | setError(''); 17 | setLoading(true); 18 | 19 | try { 20 | const supabase = await createSPASassClient(); 21 | const { error } = await supabase.getSupabaseClient().auth.resetPasswordForEmail(email, { 22 | redirectTo: `${window.location.origin}/auth/reset-password`, 23 | }); 24 | 25 | if (error) throw error; 26 | 27 | setSuccess(true); 28 | } catch (err) { 29 | if (err instanceof Error) { 30 | setError(err.message); 31 | } else { 32 | setError('An unknown error occurred'); 33 | } 34 | } finally { 35 | setLoading(false); 36 | } 37 | }; 38 | 39 | if (success) { 40 | return ( 41 |
42 |
43 |
44 | 45 |
46 | 47 |

48 | Check your email 49 |

50 | 51 |

52 | We have sent a password reset link to your email address. 53 | Please check your inbox and follow the instructions to reset your password. 54 |

55 | 56 |
57 | 58 | Return to login 59 | 60 |
61 |
62 |
63 | ); 64 | } 65 | 66 | return ( 67 |
68 |
69 |

70 | Reset your password 71 |

72 |
73 | 74 | {error && ( 75 |
76 | {error} 77 |
78 | )} 79 | 80 |
81 |
82 | 85 |
86 | setEmail(e.target.value)} 94 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 95 | /> 96 |
97 |

98 | Enter your email address and we will send you a link to reset your password. 99 |

100 |
101 | 102 |
103 | 110 |
111 |
112 | 113 |
114 | Remember your password? 115 | {' '} 116 | 117 | Sign in 118 | 119 |
120 |
121 | ); 122 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { ArrowLeft } from 'lucide-react'; 3 | 4 | export default function AuthLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME; 10 | const testimonials = [ 11 | { 12 | quote: "This template helped us launch our SaaS product in just two weeks. The authentication and multi-tenancy features are rock solid.", 13 | author: "Sarah Chen", 14 | role: "CTO, TechStart", 15 | avatar: "SC" 16 | }, 17 | { 18 | quote: "The best part is how well thought out the organization management is. It saved us months of development time.", 19 | author: "Michael Roberts", 20 | role: "Founder, DataFlow", 21 | avatar: "MR" 22 | }, 23 | { 24 | quote: "Clean code, great documentation, and excellent support. Exactly what we needed to get our MVP off the ground.", 25 | author: "Jessica Kim", 26 | role: "Lead Developer, CloudScale", 27 | avatar: "JK" 28 | } 29 | ]; 30 | 31 | return ( 32 |
33 |
34 | 38 | 39 | Back to Homepage 40 | 41 | 42 |
43 |

44 | {productName} 45 |

46 |
47 | 48 |
49 | {children} 50 |
51 |
52 | 53 |
54 |
55 |
56 |

57 | Trusted by developers worldwide 58 |

59 | {testimonials.map((testimonial, index) => ( 60 |
64 |
65 |
66 |
67 | {testimonial.avatar} 68 |
69 |
70 |
71 |

72 | "{testimonial.quote}" 73 |

74 |
75 |

76 | {testimonial.author} 77 |

78 |

79 | {testimonial.role} 80 |

81 |
82 |
83 |
84 |
85 | ))} 86 |
87 |

88 | Join thousands of developers building with {productName} 89 |

90 |
91 |
92 |
93 |
94 |
95 | ); 96 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | // src/app/auth/login/page.tsx 2 | 'use client'; 3 | 4 | import { createSPASassClient } from '@/lib/supabase/client'; 5 | import {useEffect, useState} from 'react'; 6 | import { useRouter } from 'next/navigation'; 7 | import Link from 'next/link'; 8 | import SSOButtons from '@/components/SSOButtons'; 9 | 10 | export default function LoginPage() { 11 | const [email, setEmail] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | const [error, setError] = useState(''); 14 | const [loading, setLoading] = useState(false); 15 | const [showMFAPrompt, setShowMFAPrompt] = useState(false); 16 | const router = useRouter(); 17 | 18 | const handleSubmit = async (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | setError(''); 21 | setLoading(true); 22 | 23 | try { 24 | const client = await createSPASassClient(); 25 | const { error: signInError } = await client.loginEmail(email, password); 26 | 27 | if (signInError) throw signInError; 28 | 29 | // Check if MFA is required 30 | const supabase = client.getSupabaseClient(); 31 | const { data: mfaData, error: mfaError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel(); 32 | 33 | if (mfaError) throw mfaError; 34 | 35 | if (mfaData.nextLevel === 'aal2' && mfaData.nextLevel !== mfaData.currentLevel) { 36 | setShowMFAPrompt(true); 37 | } else { 38 | router.push('/app'); 39 | return; 40 | } 41 | } catch (err) { 42 | if (err instanceof Error) { 43 | setError(err.message); 44 | } else { 45 | setError('An unknown error occurred'); 46 | } 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | 52 | 53 | useEffect(() => { 54 | if(showMFAPrompt) { 55 | router.push('/auth/2fa'); 56 | } 57 | }, [showMFAPrompt, router]); 58 | 59 | 60 | return ( 61 |
62 | {error && ( 63 |
64 | {error} 65 |
66 | )} 67 | 68 |
69 |
70 | 73 |
74 | setEmail(e.target.value)} 82 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 83 | /> 84 |
85 |
86 | 87 |
88 | 91 |
92 | setPassword(e.target.value)} 100 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 101 | /> 102 |
103 |
104 | 105 |
106 |
107 | 108 | Forgot your password? 109 | 110 |
111 |
112 | 113 |
114 | 121 |
122 |
123 | 124 | 125 | 126 |
127 | Don't have an account? 128 | {' '} 129 | 130 | Sign up 131 | 132 |
133 |
134 | ); 135 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import {createSPASassClient} from '@/lib/supabase/client'; 4 | import { useState } from 'react'; 5 | import { useRouter } from 'next/navigation'; 6 | import Link from 'next/link'; 7 | import SSOButtons from "@/components/SSOButtons"; 8 | 9 | export default function RegisterPage() { 10 | const [email, setEmail] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const [confirmPassword, setConfirmPassword] = useState(''); 13 | const [error, setError] = useState(''); 14 | const [loading, setLoading] = useState(false); 15 | const [acceptedTerms, setAcceptedTerms] = useState(false); 16 | const router = useRouter(); 17 | 18 | const handleSubmit = async (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | setError(''); 21 | 22 | if (!acceptedTerms) { 23 | setError('You must accept the Terms of Service and Privacy Policy'); 24 | return; 25 | } 26 | 27 | if (password !== confirmPassword) { 28 | setError("Passwords don't match"); 29 | return; 30 | } 31 | 32 | setLoading(true); 33 | 34 | try { 35 | const supabase = await createSPASassClient(); 36 | const { error } = await supabase.registerEmail(email, password); 37 | 38 | if (error) throw error; 39 | 40 | router.push('/auth/verify-email'); 41 | } catch (err: Error | unknown) { 42 | if(err instanceof Error) { 43 | setError(err.message); 44 | } else { 45 | setError('An unknown error occurred'); 46 | } 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | 52 | return ( 53 |
54 | {error && ( 55 |
56 | {error} 57 |
58 | )} 59 | 60 |
61 |
62 | 65 |
66 | setEmail(e.target.value)} 74 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 75 | /> 76 |
77 |
78 | 79 |
80 | 83 |
84 | setPassword(e.target.value)} 92 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 93 | /> 94 |
95 |
96 | 97 |
98 | 101 |
102 | setConfirmPassword(e.target.value)} 110 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 111 | /> 112 |
113 |
114 | 115 |
116 |
117 |
118 | setAcceptedTerms(e.target.checked)} 124 | className="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500" 125 | /> 126 |
127 |
128 | 146 |
147 |
148 |
149 |
150 | 157 |
158 |
159 | 160 | 161 | 162 |
163 | Already have an account? 164 | {' '} 165 | 166 | Sign in 167 | 168 |
169 |
170 | ); 171 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | import { createSPASassClient } from '@/lib/supabase/client'; 5 | import { useRouter } from 'next/navigation'; 6 | import { CheckCircle, Key } from 'lucide-react'; 7 | 8 | export default function ResetPasswordPage() { 9 | const [newPassword, setNewPassword] = useState(''); 10 | const [confirmPassword, setConfirmPassword] = useState(''); 11 | const [error, setError] = useState(''); 12 | const [loading, setLoading] = useState(false); 13 | const [success, setSuccess] = useState(false); 14 | const router = useRouter(); 15 | 16 | // Check if we have a valid recovery session 17 | useEffect(() => { 18 | const checkSession = async () => { 19 | try { 20 | const supabase = await createSPASassClient(); 21 | const { data: { user }, error } = await supabase.getSupabaseClient().auth.getUser(); 22 | 23 | if (error || !user) { 24 | setError('Invalid or expired reset link. Please request a new password reset.'); 25 | } 26 | } catch { 27 | setError('Failed to verify reset session'); 28 | } 29 | }; 30 | 31 | checkSession(); 32 | }, []); 33 | 34 | const handleSubmit = async (e: React.FormEvent) => { 35 | e.preventDefault(); 36 | setError(''); 37 | 38 | if (newPassword !== confirmPassword) { 39 | setError("Passwords don't match"); 40 | return; 41 | } 42 | 43 | if (newPassword.length < 6) { 44 | setError('Password must be at least 6 characters long'); 45 | return; 46 | } 47 | 48 | setLoading(true); 49 | 50 | try { 51 | const supabase = await createSPASassClient(); 52 | const { error } = await supabase.getSupabaseClient().auth.updateUser({ 53 | password: newPassword 54 | }); 55 | 56 | if (error) throw error; 57 | 58 | setSuccess(true); 59 | setTimeout(() => { 60 | router.push('/app'); 61 | }, 3000); 62 | } catch (err) { 63 | if (err instanceof Error) { 64 | setError(err.message); 65 | } else { 66 | setError('Failed to reset password'); 67 | } 68 | } finally { 69 | setLoading(false); 70 | } 71 | }; 72 | 73 | if (success) { 74 | return ( 75 |
76 |
77 |
78 | 79 |
80 | 81 |

82 | Password reset successful 83 |

84 | 85 |

86 | Your password has been successfully reset. 87 | You will be redirected to the app in a moment. 88 |

89 |
90 |
91 | ); 92 | } 93 | 94 | return ( 95 |
96 |
97 |
98 | 99 |
100 |

101 | Create new password 102 |

103 |
104 | 105 | {error && ( 106 |
107 | {error} 108 |
109 | )} 110 | 111 |
112 |
113 | 116 |
117 | setNewPassword(e.target.value)} 125 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 126 | /> 127 |
128 |
129 | 130 |
131 | 134 |
135 | setConfirmPassword(e.target.value)} 143 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500" 144 | /> 145 |
146 |

147 | Password must be at least 6 characters long 148 |

149 |
150 | 151 |
152 | 159 |
160 |
161 |
162 | ); 163 | } -------------------------------------------------------------------------------- /nextjs/src/app/auth/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { CheckCircle } from 'lucide-react'; 4 | import Link from 'next/link'; 5 | import {useState} from "react"; 6 | import {createSPASassClient} from "@/lib/supabase/client"; 7 | 8 | export default function VerifyEmailPage() { 9 | const [email, setEmail] = useState(''); 10 | const [loading, setLoading] = useState(false); 11 | const [error, setError] = useState(''); 12 | const [success, setSuccess] = useState(false); 13 | 14 | const resendVerificationEmail = async () => { 15 | if (!email) { 16 | setError('Please enter your email address'); 17 | return; 18 | } 19 | 20 | try { 21 | setLoading(true); 22 | setError(''); 23 | const supabase = await createSPASassClient(); 24 | const {error} = await supabase.resendVerificationEmail(email); 25 | if(error) { 26 | setError(error.message); 27 | return; 28 | } 29 | setSuccess(true); 30 | } catch (err: Error | unknown) { 31 | if (err instanceof Error) { 32 | setError(err.message); 33 | } else { 34 | setError('An unknown error occurred'); 35 | } 36 | } finally { 37 | setLoading(false); 38 | } 39 | } 40 | 41 | return ( 42 |
43 |
44 |
45 | 46 |
47 | 48 |

49 | Check your email 50 |

51 | 52 |

53 | We've sent you an email with a verification link. 54 | Please check your inbox and click the link to verify your account. 55 |

56 | 57 |
58 |

59 | Didn't receive the email? Check your spam folder or enter your email to resend: 60 |

61 | 62 | {error && ( 63 |
64 | {error} 65 |
66 | )} 67 | 68 | {success && ( 69 |
70 | Verification email has been resent successfully. 71 |
72 | )} 73 | 74 |
75 | setEmail(e.target.value)} 79 | placeholder="Enter your email address" 80 | className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 text-sm" 81 | /> 82 |
83 | 84 | 91 |
92 | 93 |
94 | 98 | Return to login 99 | 100 |
101 |
102 |
103 | ); 104 | } -------------------------------------------------------------------------------- /nextjs/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Razikus/supabase-nextjs-template/24068f27f3ed50f391605191f25de1837f1be7e6/nextjs/src/app/favicon.ico -------------------------------------------------------------------------------- /nextjs/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | /* Color scheme variables */ 7 | --color-background: #ffffff; 8 | --color-foreground: #0f172a; 9 | 10 | /* Primary colors */ 11 | --color-primary-50: #f0f9ff; 12 | --color-primary-100: #e0f2fe; 13 | --color-primary-200: #bae6fd; 14 | --color-primary-300: #7dd3fc; 15 | --color-primary-400: #38bdf8; 16 | --color-primary-500: #0ea5e9; 17 | --color-primary-600: #0284c7; 18 | --color-primary-700: #0369a1; 19 | --color-primary-800: #075985; 20 | --color-primary-900: #0c4a6e; 21 | 22 | /* Secondary colors */ 23 | --color-secondary-50: #f8fafc; 24 | --color-secondary-100: #f1f5f9; 25 | --color-secondary-200: #e2e8f0; 26 | --color-secondary-300: #cbd5e1; 27 | --color-secondary-400: #94a3b8; 28 | --color-secondary-500: #64748b; 29 | --color-secondary-600: #475569; 30 | --color-secondary-700: #334155; 31 | --color-secondary-800: #1e293b; 32 | --color-secondary-900: #0f172a; 33 | 34 | /* Accent colors */ 35 | --color-accent-50: #fdf4ff; 36 | --color-accent-100: #fae8ff; 37 | --color-accent-200: #f5d0fe; 38 | --color-accent-300: #f0abfc; 39 | --color-accent-400: #e879f9; 40 | --color-accent-500: #d946ef; 41 | --color-accent-600: #c026d3; 42 | --color-accent-700: #a21caf; 43 | --color-accent-800: #86198f; 44 | --color-accent-900: #701a75; 45 | } 46 | 47 | .theme-blue { 48 | --color-primary-50: #f0f9ff; 49 | --color-primary-100: #e0f2fe; 50 | --color-primary-200: #bae6fd; 51 | --color-primary-300: #7dd3fc; 52 | --color-primary-400: #38bdf8; 53 | --color-primary-500: #0ea5e9; 54 | --color-primary-600: #0284c7; 55 | --color-primary-700: #0369a1; 56 | --color-primary-800: #075985; 57 | --color-primary-900: #0c4a6e; 58 | 59 | --color-secondary-50: #f8fafc; 60 | --color-secondary-100: #f1f5f9; 61 | --color-secondary-200: #e2e8f0; 62 | --color-secondary-300: #cbd5e1; 63 | --color-secondary-400: #94a3b8; 64 | --color-secondary-500: #64748b; 65 | --color-secondary-600: #475569; 66 | --color-secondary-700: #334155; 67 | --color-secondary-800: #1e293b; 68 | --color-secondary-900: #0f172a; 69 | 70 | --color-accent-50: #eef2ff; 71 | --color-accent-100: #e0e7ff; 72 | --color-accent-200: #c7d2fe; 73 | --color-accent-300: #a5b4fc; 74 | --color-accent-400: #818cf8; 75 | --color-accent-500: #6366f1; 76 | --color-accent-600: #4f46e5; 77 | --color-accent-700: #4338ca; 78 | --color-accent-800: #3730a3; 79 | --color-accent-900: #312e81; 80 | } 81 | 82 | .theme-sass { 83 | --color-primary-50: #fdf2f8; 84 | --color-primary-100: #fce7f3; 85 | --color-primary-200: #fbcfe8; 86 | --color-primary-300: #f9a8d4; 87 | --color-primary-400: #f472b6; 88 | --color-primary-500: #cd5c8e; 89 | --color-primary-600: #bf4b8a; 90 | --color-primary-700: #9d3c72; 91 | --color-primary-800: #802d59; 92 | --color-primary-900: #621b3f; 93 | 94 | --color-secondary-50: #f8fafc; 95 | --color-secondary-100: #f1f5f9; 96 | --color-secondary-200: #e2e8f0; 97 | --color-secondary-300: #cbd5e1; 98 | --color-secondary-400: #94a3b8; 99 | --color-secondary-500: #64748b; 100 | --color-secondary-600: #475569; 101 | --color-secondary-700: #334155; 102 | --color-secondary-800: #1e293b; 103 | --color-secondary-900: #0f172a; 104 | 105 | --color-accent-50: #f5f3ff; 106 | --color-accent-100: #ede9fe; 107 | --color-accent-200: #ddd6fe; 108 | --color-accent-300: #c4b5fd; 109 | --color-accent-400: #a78bfa; 110 | --color-accent-500: #8b5cf6; 111 | --color-accent-600: #7c3aed; 112 | --color-accent-700: #6d28d9; 113 | --color-accent-800: #5b21b6; 114 | --color-accent-900: #4c1d95; 115 | } 116 | 117 | .theme-sass2 { 118 | /* Sage/Forest Green Primary */ 119 | --color-primary-50: #f4f7f4; 120 | --color-primary-100: #e6ede6; 121 | --color-primary-200: #cfdecf; 122 | --color-primary-300: #a8c2a8; 123 | --color-primary-400: #7fa17f; 124 | --color-primary-500: #5c8a5c; 125 | --color-primary-600: #4a704a; 126 | --color-primary-700: #3d5c3d; 127 | --color-primary-800: #314831; 128 | --color-primary-900: #253825; 129 | 130 | /* Warm Gray Secondary */ 131 | --color-secondary-50: #fafaf9; 132 | --color-secondary-100: #f5f5f4; 133 | --color-secondary-200: #e7e5e4; 134 | --color-secondary-300: #d6d3d1; 135 | --color-secondary-400: #a8a29e; 136 | --color-secondary-500: #78716c; 137 | --color-secondary-600: #57534e; 138 | --color-secondary-700: #44403c; 139 | --color-secondary-800: #292524; 140 | --color-secondary-900: #1c1917; 141 | 142 | /* Terracotta Accent */ 143 | --color-accent-50: #fdf4f2; 144 | --color-accent-100: #fde8e4; 145 | --color-accent-200: #fad3cc; 146 | --color-accent-300: #f5b3a5; 147 | --color-accent-400: #e98970; 148 | --color-accent-500: #dc6547; 149 | --color-accent-600: #c54a2d; 150 | --color-accent-700: #a33b25; 151 | --color-accent-800: #863225; 152 | --color-accent-900: #6f2b22; 153 | } 154 | 155 | .theme-sass3 { 156 | /* Primary colors - Ocean Blues */ 157 | --color-primary-50: #f0f9ff; 158 | --color-primary-100: #e0f7ff; 159 | --color-primary-200: #b9ecff; 160 | --color-primary-300: #7dd5f5; 161 | --color-primary-400: #38b7e8; 162 | --color-primary-500: #0c98d0; 163 | --color-primary-600: #0887c2; 164 | --color-primary-700: #0670a3; 165 | --color-primary-800: #065b86; 166 | --color-primary-900: #074563; 167 | 168 | /* Secondary colors - Sandy Neutrals */ 169 | --color-secondary-50: #fdfcfb; 170 | --color-secondary-100: #f8f6f4; 171 | --color-secondary-200: #f0ece7; 172 | --color-secondary-300: #e2dcd4; 173 | --color-secondary-400: #cbc3b9; 174 | --color-secondary-500: #afa497; 175 | --color-secondary-600: #8c8278; 176 | --color-secondary-700: #6e665e; 177 | --color-secondary-800: #504b45; 178 | --color-secondary-900: #353230; 179 | 180 | /* Accent colors - Coral Sunset */ 181 | --color-accent-50: #fff5f2; 182 | --color-accent-100: #ffe6e0; 183 | --color-accent-200: #ffc9bc; 184 | --color-accent-300: #ffa592; 185 | --color-accent-400: #ff7a61; 186 | --color-accent-500: #ff5341; 187 | --color-accent-600: #eb3a28; 188 | --color-accent-700: #d12a1a; 189 | --color-accent-800: #b22417; 190 | --color-accent-900: #8f1e15; 191 | } 192 | 193 | .theme-purple { 194 | --color-primary-50: #faf5ff; 195 | --color-primary-100: #f3e8ff; 196 | --color-primary-200: #e9d5ff; 197 | --color-primary-300: #d8b4fe; 198 | --color-primary-400: #c084fc; 199 | --color-primary-500: #a855f7; 200 | --color-primary-600: #9333ea; 201 | --color-primary-700: #7e22ce; 202 | --color-primary-800: #6b21a8; 203 | --color-primary-900: #581c87; 204 | 205 | --color-secondary-50: #fafafa; 206 | --color-secondary-100: #f4f4f5; 207 | --color-secondary-200: #e4e4e7; 208 | --color-secondary-300: #d4d4d8; 209 | --color-secondary-400: #a1a1aa; 210 | --color-secondary-500: #71717a; 211 | --color-secondary-600: #52525b; 212 | --color-secondary-700: #3f3f46; 213 | --color-secondary-800: #27272a; 214 | --color-secondary-900: #18181b; 215 | 216 | --color-accent-50: #fdf2f8; 217 | --color-accent-100: #fce7f3; 218 | --color-accent-200: #fbcfe8; 219 | --color-accent-300: #f9a8d4; 220 | --color-accent-400: #f472b6; 221 | --color-accent-500: #ec4899; 222 | --color-accent-600: #db2777; 223 | --color-accent-700: #be185d; 224 | --color-accent-800: #9d174d; 225 | --color-accent-900: #831843; 226 | } 227 | 228 | .theme-green { 229 | --color-primary-50: #f0fdf4; 230 | --color-primary-100: #dcfce7; 231 | --color-primary-200: #bbf7d0; 232 | --color-primary-300: #86efac; 233 | --color-primary-400: #4ade80; 234 | --color-primary-500: #22c55e; 235 | --color-primary-600: #16a34a; 236 | --color-primary-700: #15803d; 237 | --color-primary-800: #166534; 238 | --color-primary-900: #14532d; 239 | 240 | --color-secondary-50: #fafaf9; 241 | --color-secondary-100: #f5f5f4; 242 | --color-secondary-200: #e7e5e4; 243 | --color-secondary-300: #d6d3d1; 244 | --color-secondary-400: #a8a29e; 245 | --color-secondary-500: #78716c; 246 | --color-secondary-600: #57534e; 247 | --color-secondary-700: #44403c; 248 | --color-secondary-800: #292524; 249 | --color-secondary-900: #1c1917; 250 | 251 | --color-accent-50: #fffbeb; 252 | --color-accent-100: #fef3c7; 253 | --color-accent-200: #fde68a; 254 | --color-accent-300: #fcd34d; 255 | --color-accent-400: #fbbf24; 256 | --color-accent-500: #f59e0b; 257 | --color-accent-600: #d97706; 258 | --color-accent-700: #b45309; 259 | --color-accent-800: #92400e; 260 | --color-accent-900: #78350f; 261 | } 262 | 263 | 264 | @layer components { 265 | .btn-primary { 266 | @apply bg-primary-600 text-white hover:bg-primary-700 transition-colors; 267 | } 268 | 269 | .btn-secondary { 270 | @apply bg-secondary-200 text-secondary-800 hover:bg-secondary-300 transition-colors; 271 | } 272 | 273 | .input-primary { 274 | @apply border-secondary-300 focus:border-primary-500 focus:ring-primary-500; 275 | } 276 | } 277 | 278 | 279 | @layer base { 280 | :root { 281 | --background: 0 0% 100%; 282 | --foreground: 0 0% 3.9%; 283 | --card: 0 0% 100%; 284 | --card-foreground: 0 0% 3.9%; 285 | --popover: 0 0% 100%; 286 | --popover-foreground: 0 0% 3.9%; 287 | --primary: 0 0% 9%; 288 | --primary-foreground: 0 0% 98%; 289 | --secondary: 0 0% 96.1%; 290 | --secondary-foreground: 0 0% 9%; 291 | --muted: 0 0% 96.1%; 292 | --muted-foreground: 0 0% 45.1%; 293 | --accent: 0 0% 96.1%; 294 | --accent-foreground: 0 0% 9%; 295 | --destructive: 0 84.2% 60.2%; 296 | --destructive-foreground: 0 0% 98%; 297 | --border: 0 0% 89.8%; 298 | --input: 0 0% 89.8%; 299 | --ring: 0 0% 3.9%; 300 | --chart-1: 12 76% 61%; 301 | --chart-2: 173 58% 39%; 302 | --chart-3: 197 37% 24%; 303 | --chart-4: 43 74% 66%; 304 | --chart-5: 27 87% 67%; 305 | --radius: 0.5rem; 306 | } 307 | .dark { 308 | --background: 0 0% 3.9%; 309 | --foreground: 0 0% 98%; 310 | --card: 0 0% 3.9%; 311 | --card-foreground: 0 0% 98%; 312 | --popover: 0 0% 3.9%; 313 | --popover-foreground: 0 0% 98%; 314 | --primary: 0 0% 98%; 315 | --primary-foreground: 0 0% 9%; 316 | --secondary: 0 0% 14.9%; 317 | --secondary-foreground: 0 0% 98%; 318 | --muted: 0 0% 14.9%; 319 | --muted-foreground: 0 0% 63.9%; 320 | --accent: 0 0% 14.9%; 321 | --accent-foreground: 0 0% 98%; 322 | --destructive: 0 62.8% 30.6%; 323 | --destructive-foreground: 0 0% 98%; 324 | --border: 0 0% 14.9%; 325 | --input: 0 0% 14.9%; 326 | --ring: 0 0% 83.1%; 327 | --chart-1: 220 70% 50%; 328 | --chart-2: 160 60% 45%; 329 | --chart-3: 30 80% 55%; 330 | --chart-4: 280 65% 60%; 331 | --chart-5: 340 75% 55%; 332 | } 333 | } 334 | 335 | 336 | @layer base { 337 | * { 338 | @apply border-border; 339 | } 340 | body { 341 | @apply bg-background text-foreground; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { Analytics } from '@vercel/analytics/next'; 4 | import CookieConsent from "@/components/Cookies"; 5 | import { GoogleAnalytics } from '@next/third-parties/google' 6 | 7 | 8 | export const metadata: Metadata = { 9 | title: process.env.NEXT_PUBLIC_PRODUCTNAME, 10 | description: "The best way to build your SaaS product.", 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: Readonly<{ 16 | children: React.ReactNode; 17 | }>) { 18 | let theme = process.env.NEXT_PUBLIC_THEME 19 | if(!theme) { 20 | theme = "theme-sass3" 21 | } 22 | const gaID = process.env.NEXT_PUBLIC_GOOGLE_TAG; 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | { gaID && ( 30 | 31 | )} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /nextjs/src/app/legal/[document]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import LegalDocument from '@/components/LegalDocument'; 5 | import { notFound } from 'next/navigation'; 6 | 7 | const legalDocuments = { 8 | 'privacy': { 9 | title: 'Privacy Notice', 10 | path: '/terms/privacy-notice.md' 11 | }, 12 | 'terms': { 13 | title: 'Terms of Service', 14 | path: '/terms/terms-of-service.md' 15 | }, 16 | 'refund': { 17 | title: 'Refund Policy', 18 | path: '/terms/refund-policy.md' 19 | } 20 | } as const; 21 | 22 | type LegalDocument = keyof typeof legalDocuments; 23 | 24 | interface LegalPageProps { 25 | document: LegalDocument; 26 | lng: string; 27 | } 28 | 29 | interface LegalPageParams { 30 | params: Promise 31 | } 32 | 33 | export default function LegalPage({ params }: LegalPageParams) { 34 | const {document} = React.use(params); 35 | 36 | if (!legalDocuments[document]) { 37 | notFound(); 38 | } 39 | 40 | const { title, path } = legalDocuments[document]; 41 | 42 | return ( 43 |
44 | 48 |
49 | ); 50 | } -------------------------------------------------------------------------------- /nextjs/src/app/legal/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { ArrowLeft, FileText, ShieldAlert, RefreshCw } from 'lucide-react'; 6 | 7 | const legalDocuments = [ 8 | { 9 | id: 'privacy', 10 | title: 'Privacy Policy', 11 | icon: ShieldAlert, 12 | description: 'How we handle and protect your data' 13 | }, 14 | { 15 | id: 'terms', 16 | title: 'Terms of Service', 17 | icon: FileText, 18 | description: 'Rules and guidelines for using our service' 19 | }, 20 | { 21 | id: 'refund', 22 | title: 'Refund Policy', 23 | icon: RefreshCw, 24 | description: 'Our policy on refunds and cancellations' 25 | } 26 | ]; 27 | 28 | export default function LegalLayout({ children } : { children: React.ReactNode }) { 29 | const router = useRouter(); 30 | 31 | return ( 32 |
33 |
34 |
35 | 42 |
43 | 44 |
45 | {/* Sidebar Navigation */} 46 |
47 |
48 |
49 |

Legal Documents

50 |

Important information about our services

51 |
52 | 69 |
70 |
71 | 72 | {/* Main Content */} 73 |
74 | {children} 75 |
76 |
77 |
78 |
79 | ); 80 | } -------------------------------------------------------------------------------- /nextjs/src/app/legal/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | 'use client'; 3 | 4 | import React from 'react'; 5 | 6 | 7 | export default function LegalPage() { 8 | 9 | 10 | return ( 11 |
12 | Select document on the left. 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import { ArrowRight, Globe, Shield, Users, Key, Database, Clock } from 'lucide-react'; 4 | import AuthAwareButtons from '@/components/AuthAwareButtons'; 5 | import HomePricing from "@/components/HomePricing"; 6 | 7 | export default function Home() { 8 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME; 9 | 10 | const features = [ 11 | { 12 | icon: Shield, 13 | title: 'Robust Authentication', 14 | description: 'Secure login with email/password, Multi-Factor Authentication, and SSO providers', 15 | color: 'text-green-600' 16 | }, 17 | { 18 | icon: Database, 19 | title: 'File Management', 20 | description: 'Built-in file storage with secure sharing, downloads, and granular permissions', 21 | color: 'text-orange-600' 22 | }, 23 | { 24 | icon: Users, 25 | title: 'User Settings', 26 | description: 'Complete user management with password updates, MFA setup, and profile controls', 27 | color: 'text-red-600' 28 | }, 29 | { 30 | icon: Clock, 31 | title: 'Task Management', 32 | description: 'Built-in todo system with real-time updates and priority management', 33 | color: 'text-teal-600' 34 | }, 35 | { 36 | icon: Globe, 37 | title: 'Legal Documents', 38 | description: 'Pre-configured privacy policy, terms of service, and refund policy pages', 39 | color: 'text-purple-600' 40 | }, 41 | { 42 | icon: Key, 43 | title: 'Cookie Consent', 44 | description: 'GDPR-compliant cookie consent system with customizable preferences', 45 | color: 'text-blue-600' 46 | } 47 | ]; 48 | 49 | const stats = [ 50 | { label: 'Active Users', value: '10K+' }, 51 | { label: 'Organizations', value: '2K+' }, 52 | { label: 'Countries', value: '50+' }, 53 | { label: 'Uptime', value: '99.9%' } 54 | ]; 55 | 56 | return ( 57 |
58 | 97 | 98 |
99 |
100 |
101 |

102 | Bootstrap Your SaaS 103 | In 5 minutes 104 |

105 |

106 | Launch your SaaS product in days, not months. Complete with authentication and enterprise-grade security built right in. 107 |

108 |
109 | 110 | 111 |
112 |
113 |
114 |
115 | 116 |
117 |
118 |
119 | {stats.map((stat, index) => ( 120 |
121 |
{stat.value}
122 |
{stat.label}
123 |
124 | ))} 125 |
126 |
127 |
128 | 129 | {/* Features Section */} 130 |
131 |
132 |
133 |

Everything You Need

134 |

135 | Built with modern technologies for reliability and speed 136 |

137 |
138 |
139 | {features.map((feature, index) => ( 140 |
144 | 145 |

{feature.title}

146 |

{feature.description}

147 |
148 | ))} 149 |
150 |
151 |
152 | 153 | 154 | 155 |
156 |
157 |

158 | Ready to Transform Your Idea into Reality? 159 |

160 |

161 | Join thousands of developers building their SaaS with {productName} 162 |

163 | 167 | Get Started Now 168 | 169 | 170 |
171 |
172 | 173 |
174 |
175 |
176 |
177 |

Product

178 |
    179 |
  • 180 | 181 | Features 182 | 183 |
  • 184 |
  • 185 | 186 | Pricing 187 | 188 |
  • 189 |
190 |
191 |
192 |

Resources

193 |
    194 |
  • 195 | 196 | Documentation 197 | 198 |
  • 199 |
200 |
201 |
202 |

Legal

203 |
    204 |
  • 205 | 206 | Privacy 207 | 208 |
  • 209 |
  • 210 | 211 | Terms 212 | 213 |
  • 214 |
215 |
216 |
217 |
218 |

219 | © {new Date().getFullYear()} {productName}. All rights reserved. 220 |

221 |
222 |
223 |
224 |
225 | ); 226 | } -------------------------------------------------------------------------------- /nextjs/src/components/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from 'react'; 3 | import Link from 'next/link'; 4 | import {usePathname, useRouter} from 'next/navigation'; 5 | import { 6 | Home, 7 | User, 8 | Menu, 9 | X, 10 | ChevronDown, 11 | LogOut, 12 | Key, Files, LucideListTodo, 13 | } from 'lucide-react'; 14 | import { useGlobal } from "@/lib/context/GlobalContext"; 15 | import { createSPASassClient } from "@/lib/supabase/client"; 16 | 17 | export default function AppLayout({ children }: { children: React.ReactNode }) { 18 | const [isSidebarOpen, setSidebarOpen] = useState(false); 19 | const [isUserDropdownOpen, setUserDropdownOpen] = useState(false); 20 | const pathname = usePathname(); 21 | const router = useRouter(); 22 | 23 | 24 | const { user } = useGlobal(); 25 | 26 | const handleLogout = async () => { 27 | try { 28 | const client = await createSPASassClient(); 29 | await client.logout(); 30 | } catch (error) { 31 | console.error('Error logging out:', error); 32 | } 33 | }; 34 | const handleChangePassword = async () => { 35 | router.push('/app/user-settings') 36 | }; 37 | 38 | const getInitials = (email: string) => { 39 | const parts = email.split('@')[0].split(/[._-]/); 40 | return parts.length > 1 41 | ? (parts[0][0] + parts[1][0]).toUpperCase() 42 | : parts[0].slice(0, 2).toUpperCase(); 43 | }; 44 | 45 | const productName = process.env.NEXT_PUBLIC_PRODUCTNAME; 46 | 47 | const navigation = [ 48 | { name: 'Homepage', href: '/app', icon: Home }, 49 | { name: 'Example Storage', href: '/app/storage', icon: Files }, 50 | { name: 'Example Table', href: '/app/table', icon: LucideListTodo }, 51 | { name: 'User Settings', href: '/app/user-settings', icon: User }, 52 | ]; 53 | 54 | const toggleSidebar = () => setSidebarOpen(!isSidebarOpen); 55 | 56 | return ( 57 |
58 | {isSidebarOpen && ( 59 |
63 | )} 64 | 65 | {/* Sidebar */} 66 |
68 | 69 |
70 | {productName} 71 | 77 |
78 | 79 | {/* Navigation */} 80 | 103 | 104 |
105 | 106 |
107 |
108 | 114 | 115 |
116 | 128 | 129 | {isUserDropdownOpen && ( 130 |
131 |
132 |

Signed in as

133 |

134 | {user?.email} 135 |

136 |
137 |
138 | 148 | 158 |
159 |
160 | )} 161 |
162 |
163 | 164 |
165 | {children} 166 |
167 |
168 |
169 | ); 170 | } -------------------------------------------------------------------------------- /nextjs/src/components/AuthAwareButtons.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from 'react'; 3 | import { createSPASassClient } from '@/lib/supabase/client'; 4 | import { ArrowRight, ChevronRight } from 'lucide-react'; 5 | import Link from "next/link"; 6 | 7 | export default function AuthAwareButtons({ variant = 'primary' }) { 8 | const [isAuthenticated, setIsAuthenticated] = useState(false); 9 | const [loading, setLoading] = useState(true); 10 | 11 | useEffect(() => { 12 | const checkAuth = async () => { 13 | try { 14 | const supabase = await createSPASassClient(); 15 | const { data: { user } } = await supabase.getSupabaseClient().auth.getUser(); 16 | setIsAuthenticated(!!user); 17 | } catch (error) { 18 | console.error('Error checking auth status:', error); 19 | } finally { 20 | setLoading(false); 21 | } 22 | }; 23 | 24 | checkAuth(); 25 | }, []); 26 | 27 | if (loading) { 28 | return null; 29 | } 30 | 31 | // Navigation buttons for the header 32 | if (variant === 'nav') { 33 | return isAuthenticated ? ( 34 | 38 | Go to Dashboard 39 | 40 | ) : ( 41 | <> 42 | 43 | Login 44 | 45 | 49 | Get Started 50 | 51 | 52 | ); 53 | } 54 | 55 | // Primary buttons for the hero section 56 | return isAuthenticated ? ( 57 | 61 | Go to Dashboard 62 | 63 | 64 | ) : ( 65 | <> 66 | 70 | Start Building Free 71 | 72 | 73 | 77 | Learn More 78 | 79 | 80 | 81 | ); 82 | } -------------------------------------------------------------------------------- /nextjs/src/components/Confetti.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useRef } from 'react'; 3 | 4 | interface ConfettiProps { 5 | active: boolean; 6 | } 7 | 8 | interface Particle { 9 | x: number; 10 | y: number; 11 | size: number; 12 | color: string; 13 | speedX: number; 14 | speedY: number; 15 | gravity: number; 16 | rotation: number; 17 | rotationSpeed: number; 18 | } 19 | 20 | const Confetti: React.FC = ({ active }) => { 21 | const canvasRef = useRef(null); 22 | const animationFrameId = useRef(null); 23 | const particles = useRef([]); 24 | 25 | useEffect(() => { 26 | if (!active || !canvasRef.current) return; 27 | 28 | const canvas = canvasRef.current; 29 | const ctx = canvas.getContext('2d'); 30 | if (!ctx) return; 31 | 32 | const colors = ['#ffd700', '#ff0000', '#00ff00', '#0000ff', '#ff00ff']; 33 | 34 | particles.current = Array.from({ length: 50 }, (): Particle => ({ 35 | x: canvas.width / 2, 36 | y: canvas.height / 2, 37 | size: Math.random() * 8 + 4, 38 | color: colors[Math.floor(Math.random() * colors.length)], 39 | speedX: (Math.random() - 0.5) * 12, 40 | speedY: -Math.random() * 15 - 5, 41 | gravity: 0.7, 42 | rotation: Math.random() * 360, 43 | rotationSpeed: (Math.random() - 0.5) * 8 44 | })); 45 | 46 | let frame = 0; 47 | const maxFrames = 120; 48 | 49 | const animate = () => { 50 | ctx.clearRect(0, 0, canvas.width, canvas.height); 51 | 52 | particles.current.forEach((particle) => { 53 | ctx.save(); 54 | ctx.translate(particle.x, particle.y); 55 | ctx.rotate((particle.rotation * Math.PI) / 180); 56 | 57 | ctx.fillStyle = particle.color; 58 | ctx.fillRect(-particle.size / 2, -particle.size / 2, particle.size, particle.size); 59 | 60 | ctx.restore(); 61 | 62 | particle.x += particle.speedX; 63 | particle.y += particle.speedY; 64 | particle.speedY += particle.gravity; 65 | particle.rotation += particle.rotationSpeed; 66 | }); 67 | 68 | frame++; 69 | 70 | if (frame < maxFrames) { 71 | animationFrameId.current = requestAnimationFrame(animate); 72 | } 73 | }; 74 | 75 | animate(); 76 | 77 | return () => { 78 | if (animationFrameId.current) { 79 | cancelAnimationFrame(animationFrameId.current); 80 | } 81 | }; 82 | }, [active]); 83 | 84 | return ( 85 | 91 | ); 92 | }; 93 | 94 | export default Confetti; -------------------------------------------------------------------------------- /nextjs/src/components/Cookies.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Button } from '@/components/ui/button'; 4 | import { Shield, X } from 'lucide-react'; 5 | import { setCookie, getCookie } from 'cookies-next/client'; 6 | import Link from 'next/link'; 7 | 8 | const COOKIE_CONSENT_KEY = 'cookie-accept'; 9 | const COOKIE_EXPIRY_DAYS = 365; 10 | 11 | const CookieConsent = () => { 12 | const [isVisible, setIsVisible] = useState(false); 13 | 14 | useEffect(() => { 15 | const consent = getCookie(COOKIE_CONSENT_KEY); 16 | if (!consent) { 17 | const timer = setTimeout(() => { 18 | setIsVisible(true); 19 | }, 1000); 20 | return () => clearTimeout(timer); 21 | } 22 | }, []); 23 | 24 | const handleAccept = () => { 25 | setCookie(COOKIE_CONSENT_KEY, 'accepted', { 26 | expires: new Date(Date.now() + COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000), 27 | sameSite: 'strict', 28 | secure: process.env.NODE_ENV === 'production', 29 | path: '/' 30 | }); 31 | setIsVisible(false); 32 | }; 33 | 34 | const handleDecline = () => { 35 | setCookie(COOKIE_CONSENT_KEY, 'declined', { 36 | expires: new Date(Date.now() + COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000), 37 | sameSite: 'strict', 38 | secure: process.env.NODE_ENV === 'production', 39 | path: '/' 40 | }); 41 | setIsVisible(false); 42 | }; 43 | 44 | if (!isVisible) return null; 45 | 46 | return ( 47 |
48 |
49 |
50 |
51 | 52 |
53 |

54 | We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. 55 | By clicking "Accept", you consent to our use of cookies. 56 |

57 |

58 | Read our{' '} 59 | 60 | Privacy Policy 61 | {' '} 62 | and{' '} 63 | 64 | Terms of Service 65 | {' '} 66 | for more information. 67 |

68 |
69 |
70 |
71 | 79 | 86 | 93 |
94 |
95 |
96 |
97 | ); 98 | }; 99 | 100 | export default CookieConsent; -------------------------------------------------------------------------------- /nextjs/src/components/HomePricing.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from 'react'; 3 | import Link from 'next/link'; 4 | import { Check } from 'lucide-react'; 5 | import PricingService from "@/lib/pricing"; 6 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; 7 | 8 | const HomePricing = () => { 9 | const tiers = PricingService.getAllTiers(); 10 | const commonFeatures = PricingService.getCommonFeatures(); 11 | 12 | return ( 13 |
14 |
15 |
16 |

Simple, Transparent Pricing

17 |

Choose the plan that's right for your business (IT'S PLACEHOLDER NO PRICING FOR THIS TEMPLATE)

18 |
19 | 20 |
21 | {tiers.map((tier) => ( 22 | 28 | {tier.popular && ( 29 |
30 | Most Popular 31 |
32 | )} 33 | 34 | 35 | {tier.name} 36 | {tier.description} 37 | 38 | 39 | 40 |
41 | {PricingService.formatPrice(tier.price)} 42 | /month 43 |
44 | 45 |
    46 | {tier.features.map((feature) => ( 47 |
  • 48 | 49 | {feature} 50 |
  • 51 | ))} 52 |
53 | 54 | 62 | Get Started 63 | 64 |
65 |
66 | ))} 67 |
68 | 69 |
70 |

71 | All plans include: {commonFeatures.join(', ')} 72 |

73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export default HomePricing; 80 | -------------------------------------------------------------------------------- /nextjs/src/components/LegalDocument.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import ReactMarkdown from 'react-markdown'; 5 | import { Card, CardHeader, CardContent } from '@/components/ui/card'; 6 | import { Loader2 } from 'lucide-react'; 7 | 8 | interface LegalDocumentProps { 9 | filePath: string; 10 | title: string; 11 | } 12 | 13 | const LegalDocument: React.FC = ({ filePath, title }) => { 14 | const [content, setContent] = useState(''); 15 | const [loading, setLoading] = useState(true); 16 | const [error, setError] = useState(null); 17 | 18 | useEffect(() => { 19 | setLoading(true); 20 | setError(null); 21 | 22 | fetch(filePath) 23 | .then(response => { 24 | if (!response.ok) { 25 | throw new Error('Failed to load document'); 26 | } 27 | return response.text(); 28 | }) 29 | .then(text => { 30 | setContent(text); 31 | setLoading(false); 32 | }) 33 | .catch(error => { 34 | console.error('Error loading markdown:', error); 35 | setError('Failed to load document. Please try again later.'); 36 | setLoading(false); 37 | }); 38 | }, [filePath]); 39 | 40 | return ( 41 | 42 | 43 |

{title}

44 |
45 | 46 | {loading ? ( 47 |
48 | 49 |
50 | ) : error ? ( 51 |
52 | {error} 53 |
54 | ) : ( 55 |

{children}

, 58 | h2: ({ children }) =>

{children}

, 59 | h3: ({ children }) =>

{children}

, 60 | ul: ({ children }) =>
    {children}
, 61 | li: ({ children }) =>
  • {children}
  • , 62 | p: ({ children }) =>

    {children}

    , 63 | }} 64 | > 65 | {content} 66 |
    67 | )} 68 |
    69 |
    70 | ); 71 | }; 72 | 73 | export default LegalDocument; -------------------------------------------------------------------------------- /nextjs/src/components/LegalDocuments.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from "react"; 3 | import {FileText, RefreshCw, Shield} from "lucide-react"; 4 | import Link from "next/link"; 5 | 6 | type LegalDocumentsParams = { 7 | minimalist: boolean; 8 | } 9 | 10 | export default function LegalDocuments({ minimalist }: LegalDocumentsParams) { 11 | if (minimalist) { 12 | return ( 13 | <> 14 |
    15 | 16 | Legal Documents 17 |
    18 |
    19 | 23 | Privacy Policy 24 | 25 | 29 | Terms of Service 30 | 31 | 35 | Refund Policy 36 | 37 |
    38 | 39 | ) 40 | } 41 | return ( 42 | <> 43 |
    44 | 45 | Legal Documents 46 |
    47 |
    48 | 52 | 53 | Privacy Policy 55 | 56 | 60 | 61 | Terms of Service 63 | 64 | 68 | 69 | Refund Policy 71 | 72 |
    73 | 74 | ) 75 | } -------------------------------------------------------------------------------- /nextjs/src/components/MFAVerification.tsx: -------------------------------------------------------------------------------- 1 | // src/components/MFAVerification.tsx 2 | import React, { useState, useEffect } from 'react'; 3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; 4 | import { Alert, AlertDescription } from '@/components/ui/alert'; 5 | import { createSPASassClient } from '@/lib/supabase/client'; 6 | import { CheckCircle, Smartphone } from 'lucide-react'; 7 | import { Factor } from '@supabase/auth-js'; 8 | 9 | interface MFAVerificationProps { 10 | onVerified: () => void; 11 | } 12 | 13 | export function MFAVerification({ onVerified }: MFAVerificationProps) { 14 | const [verifyCode, setVerifyCode] = useState(''); 15 | const [error, setError] = useState(''); 16 | const [loading, setLoading] = useState(false); 17 | const [factors, setFactors] = useState([]); 18 | const [selectedFactorId, setSelectedFactorId] = useState(''); 19 | const [loadingFactors, setLoadingFactors] = useState(true); 20 | 21 | useEffect(() => { 22 | loadFactors(); 23 | }, []); 24 | 25 | const loadFactors = async () => { 26 | try { 27 | const supabase = await createSPASassClient(); 28 | const { data, error } = await supabase.getSupabaseClient().auth.mfa.listFactors(); 29 | 30 | if (error) throw error; 31 | 32 | const totpFactors = data.totp || []; 33 | setFactors(totpFactors); 34 | console.log('totpFactors:', totpFactors); 35 | 36 | // If there's only one factor, select it automatically 37 | if (totpFactors.length === 1) { 38 | setSelectedFactorId(totpFactors[0].id); 39 | } 40 | } catch (err) { 41 | console.error('Error loading MFA factors:', err); 42 | setError('Failed to load authentication devices'); 43 | } finally { 44 | setLoadingFactors(false); 45 | } 46 | }; 47 | 48 | const handleVerification = async () => { 49 | if (!selectedFactorId) { 50 | setError('Please select an authentication device'); 51 | return; 52 | } 53 | 54 | setError(''); 55 | setLoading(true); 56 | 57 | try { 58 | const supabase = await createSPASassClient(); 59 | const client = supabase.getSupabaseClient(); 60 | 61 | // Create challenge 62 | const { data: challengeData, error: challengeError } = await client.auth.mfa.challenge({ 63 | factorId: selectedFactorId 64 | }); 65 | 66 | if (challengeError) throw challengeError; 67 | 68 | // Verify the challenge 69 | const { error: verifyError } = await client.auth.mfa.verify({ 70 | factorId: selectedFactorId, 71 | challengeId: challengeData.id, 72 | code: verifyCode 73 | }); 74 | 75 | if (verifyError) throw verifyError; 76 | 77 | onVerified(); 78 | } catch (err) { 79 | console.error('MFA verification error:', err); 80 | setError(err instanceof Error ? err.message : 'Failed to verify MFA code'); 81 | } finally { 82 | setLoading(false); 83 | } 84 | }; 85 | 86 | if (loadingFactors) { 87 | return ( 88 | 89 | 90 |
    91 |
    92 |
    93 | ); 94 | } 95 | 96 | if (factors.length === 0) { 97 | return ( 98 | 99 | 100 | 101 | 102 | No authentication devices found. Please contact support. 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | 110 | return ( 111 | 112 | 113 | Two-Factor Authentication Required 114 | 115 | Please enter the verification code from your authenticator app 116 | 117 | 118 | 119 | {error && ( 120 | 121 | {error} 122 | 123 | )} 124 | 125 |
    126 | {factors.length > 1 && ( 127 |
    128 | 131 |
    132 | {factors.map((factor) => ( 133 | 155 | ))} 156 |
    157 |
    158 | )} 159 | 160 |
    161 | 164 | setVerifyCode(e.target.value.trim())} 168 | className="block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-primary-500 focus:outline-none focus:ring-primary-500 sm:text-sm" 169 | placeholder="Enter 6-digit code" 170 | maxLength={6} 171 | /> 172 |

    173 | Enter the 6-digit code from your authenticator app 174 |

    175 |
    176 | 177 | 184 |
    185 |
    186 |
    187 | ); 188 | } -------------------------------------------------------------------------------- /nextjs/src/components/SSOButtons.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createSPAClient } from '@/lib/supabase/client'; 4 | import Link from "next/link"; 5 | 6 | type Provider = 'github' | 'google' | 'facebook' | 'apple'; 7 | 8 | interface SSOButtonsProps { 9 | onError?: (error: string) => void; 10 | } 11 | 12 | const PROVIDER_CONFIGS = { 13 | github: { 14 | name: 'GitHub', 15 | icon: ( 16 | 17 | 18 | 19 | ), 20 | bgColor: 'bg-gray-800 hover:bg-gray-700', 21 | textColor: 'text-white', 22 | borderColor: 'border-transparent' 23 | }, 24 | google: { 25 | name: 'Google', 26 | icon: ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | ), 34 | bgColor: 'bg-white hover:bg-gray-50', 35 | textColor: 'text-gray-700', 36 | borderColor: 'border-gray-300' 37 | }, 38 | facebook: { 39 | name: 'Facebook', 40 | icon: ( 41 | 42 | 43 | 44 | ), 45 | bgColor: 'bg-[#1877F2] hover:bg-[#166fe5]', 46 | textColor: 'text-white', 47 | borderColor: 'border-transparent' 48 | }, 49 | apple: { 50 | name: 'Apple', 51 | icon: ( 52 | 53 | 54 | 55 | ), 56 | bgColor: 'bg-black hover:bg-gray-900', 57 | textColor: 'text-white', 58 | borderColor: 'border-transparent' 59 | } 60 | }; 61 | 62 | function getEnabledProviders(): Provider[] { 63 | const providersStr = process.env.NEXT_PUBLIC_SSO_PROVIDERS || ''; 64 | return providersStr.split(',').filter((provider): provider is Provider => 65 | provider.trim().toLowerCase() in PROVIDER_CONFIGS 66 | ); 67 | } 68 | 69 | export default function SSOButtons({ onError }: SSOButtonsProps) { 70 | const handleSSOLogin = async (provider: Provider) => { 71 | try { 72 | const supabase = createSPAClient(); 73 | const { error } = await supabase.auth.signInWithOAuth({ 74 | provider, 75 | options: { 76 | redirectTo: `${window.location.origin}/api/auth/callback`, 77 | }, 78 | }); 79 | 80 | if (error) throw error; 81 | } catch (err: Error | unknown) { 82 | if (err instanceof Error) { 83 | onError?.(err.message); 84 | } else { 85 | onError?.('An unknown error occurred'); 86 | } 87 | } 88 | }; 89 | 90 | const enabledProviders = getEnabledProviders(); 91 | 92 | if (enabledProviders.length === 0) { 93 | return null; 94 | } 95 | 96 | return ( 97 |
    98 |
    99 |
    100 |
    101 |
    102 |
    103 | Or continue with 104 |
    105 |
    106 | 107 |
    108 | {enabledProviders.map((provider) => { 109 | const config = PROVIDER_CONFIGS[provider]; 110 | return ( 111 | 125 | ); 126 | })} 127 |
    128 |
    129 | By creating an account via selected provider, you agree to our{' '} 130 | 131 | Terms and Conditions 132 | 133 | {' '}and{' '} 134 | 135 | Privacy Policy 136 | 137 |
    138 |
    139 | ); 140 | } -------------------------------------------------------------------------------- /nextjs/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
    59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
    73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
    32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
    44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
    56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
    44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
    56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
    64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
    76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
    67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
    81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /nextjs/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |