├── .nvmrc ├── AGENTS.md ├── .husky └── pre-commit ├── .prettierignore ├── .cursorignore ├── .vscode ├── settings.json ├── extensions.json ├── launch.json └── markdown.css ├── mcp-server ├── .gitignore ├── .vscode │ └── settings.json ├── vitest.config.ts ├── tsconfig.json ├── biome.json ├── package.json └── src │ ├── data │ └── rulesProvider.ts │ └── types │ └── bindings.ts ├── src ├── pages │ ├── privacy │ │ ├── privacyPolicyVersion.ts │ │ ├── en │ │ │ └── index.astro │ │ └── pl │ │ │ └── index.astro │ ├── prompts │ │ ├── index.astro │ │ ├── admin │ │ │ ├── index.astro │ │ │ └── invites.astro │ │ └── request-access.astro │ ├── auth │ │ ├── reset-password.astro │ │ ├── update-password.astro │ │ ├── signup.astro │ │ └── login.astro │ ├── invites │ │ └── [token].astro │ ├── index.astro │ └── api │ │ ├── auth │ │ ├── logout.ts │ │ ├── verify-reset-token.ts │ │ └── reset-password.ts │ │ ├── prompt-library │ │ └── organizations.ts │ │ ├── captcha │ │ └── verify.ts │ │ ├── prompts │ │ ├── admin │ │ │ ├── invites │ │ │ │ ├── [id].ts │ │ │ │ └── [id] │ │ │ │ │ └── stats.ts │ │ │ └── prompts │ │ │ │ └── [id] │ │ │ │ └── publish.ts │ │ ├── segments │ │ │ └── by-slug.ts │ │ └── collections.ts │ │ └── invites │ │ ├── validate.ts │ │ └── redeem.ts ├── assets │ ├── demo.png │ ├── hero │ │ ├── icon-jb.png │ │ ├── icon-copilot.png │ │ ├── icon-cursor.png │ │ ├── icon-windsurf.png │ │ └── icon-claude-code.png │ └── prompt-library │ │ └── request-page-bg.png ├── components │ ├── rule-preview │ │ ├── index.ts │ │ ├── RulesPath.tsx │ │ ├── MarkdownContentRenderer.tsx │ │ └── DependencyUpload.tsx │ ├── rule-builder │ │ ├── index.ts │ │ ├── hooks │ │ │ └── useMCPDialog.ts │ │ ├── LibraryItem.tsx │ │ ├── LayerSelector.tsx │ │ └── SelectedRules.tsx │ ├── landing │ │ ├── FeatureCard.astro │ │ ├── TechStackShowcase.astro │ │ ├── LandingHeader.astro │ │ ├── ChooseYourPath.astro │ │ ├── Landing.astro │ │ └── Community.astro │ ├── ui │ │ ├── StatusBadge.tsx │ │ ├── Button.astro │ │ ├── MarkdownRenderer.tsx │ │ ├── Logo.astro │ │ ├── StepCard.astro │ │ ├── Logo.tsx │ │ ├── FormInput.tsx │ │ ├── FormTextarea.tsx │ │ └── PathCard.astro │ ├── auth │ │ ├── AuthLayout.tsx │ │ └── LoginButton.tsx │ ├── privacy │ │ └── PrivacyLayout.astro │ ├── Footer.tsx │ ├── prompt-library │ │ ├── OrganizationSelector.tsx │ │ ├── admin │ │ │ └── AdminTabs.tsx │ │ ├── LanguageSwitcher.tsx │ │ └── PromptCard.tsx │ ├── cookie-banner │ │ └── CookieBanner.tsx │ └── rule-collections │ │ ├── DeletionDialog.tsx │ │ └── UnsavedRuleCollectionChangesDialog.tsx ├── services │ ├── rules-builder │ │ ├── RulesBuilderTypes.ts │ │ ├── RulesGenerationStrategy.ts │ │ └── rules-generation-strategies │ │ │ ├── MultiFileRulesStrategy.ts │ │ │ └── SingleFileRulesStrategy.ts │ ├── captcha.ts │ └── prompt-library │ │ ├── types.ts │ │ └── access.ts ├── data │ ├── rules │ │ ├── types.ts │ │ ├── index.ts │ │ ├── helpers.ts │ │ └── database.ts │ ├── landingAnimations.ts │ ├── rules.ts │ └── landingContent.ts ├── utils │ ├── cn.ts │ ├── slugify.ts │ └── urlParams.ts ├── layouts │ ├── partials │ │ ├── Fonts.astro │ │ └── GTMContainer.astro │ └── Layout.astro ├── store │ ├── authStore.ts │ ├── navigationStore.ts │ └── storage │ │ └── urlStorage.ts ├── db │ ├── supabase-admin.ts │ └── supabase.client.ts ├── hooks │ ├── useTokenHashVerification.ts │ ├── useNavigationItems.ts │ ├── useAuth.ts │ └── useKeyboardActivation.ts ├── types │ └── ruleCollection.types.ts ├── styles │ └── animations │ │ └── reduced-motion.css └── env.d.ts ├── public ├── demo.png └── favicon.svg ├── supabase ├── .gitignore ├── migrations │ ├── 20251005120000_add_prompt_sort_order.sql │ ├── 20251005000000_add_prompt_descriptions.sql │ ├── 20251002100000_localize_prompts_table.sql │ ├── 20251003170000_user_emails_view.sql │ ├── 20250411083417_create_user_consents.sql │ ├── 20250328135512_collections.sql │ ├── 20250413093000_prompt_manager_orgs.sql │ ├── 20251006000000_fix_get_user_emails_permissions.sql │ ├── 20251006000002_create_secure_invite_emails_function.sql │ └── 20251017000001_atomic_signup_check.sql └── emails │ └── email-template.html ├── .editorconfig ├── tests ├── setup │ ├── types.d.ts │ ├── test-utils.tsx │ └── vitest.setup.ts ├── helpers │ └── index.ts ├── stubs │ └── astro-middleware.ts └── unit │ └── features │ └── featureFlags.test.ts ├── .claude ├── settings.json └── commands │ ├── shadcn-init.md │ ├── shadcn-status-checkpoint.md │ ├── shadcn-resume-implementation.md │ ├── shadcn-plan-summary.md │ └── generate-types-from-supabase.md ├── .env.example ├── tsconfig.json ├── .prettierrc ├── e2e ├── page-objects │ └── HomePage.ts ├── home.spec.ts ├── global.teardown.ts └── auth.setup.ts ├── .eslintrc.cjs ├── .gitignore ├── vitest.config.ts ├── .cursor └── rules │ ├── astro.mdc │ ├── playwright-e2e-testing.mdc │ ├── react-development.mdc │ ├── github-action.mdc │ ├── test-plan.mdc │ └── supabase-migrations.mdc ├── astro.config.mjs ├── .ai ├── project-mvp.md ├── tech-stack.md ├── prompt-library │ ├── phase-1-notes.md │ ├── phase-1-test-plan.md │ └── phase-2-test-plan.md ├── ui │ └── mobile-navigation.md └── db-migrations.md ├── .github ├── workflows │ ├── deploy-mcp-on-merge.yml │ └── cca-review.yml └── pull_request_template.md └── playwright.config.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | CLAUDE.md -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/layouts/partials/GTMContainer.astro -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.test 3 | .env.local 4 | .env.integration 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # wrangler files 4 | .wrangler 5 | .dev.vars* 6 | -------------------------------------------------------------------------------- /src/pages/privacy/privacyPolicyVersion.ts: -------------------------------------------------------------------------------- 1 | export const PRIVACY_POLICY_VERSION = '13.04.2024'; 2 | -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/public/demo.png -------------------------------------------------------------------------------- /mcp-server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "wrangler.json": "jsonc" 4 | } 5 | } -------------------------------------------------------------------------------- /src/assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/demo.png -------------------------------------------------------------------------------- /src/assets/hero/icon-jb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/hero/icon-jb.png -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /src/assets/hero/icon-copilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/hero/icon-copilot.png -------------------------------------------------------------------------------- /src/assets/hero/icon-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/hero/icon-cursor.png -------------------------------------------------------------------------------- /src/assets/hero/icon-windsurf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/hero/icon-windsurf.png -------------------------------------------------------------------------------- /src/assets/hero/icon-claude-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/hero/icon-claude-code.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode", "esbenp.prettier-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/prompt-library/request-page-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/przeprogramowani/ai-rules-builder/HEAD/src/assets/prompt-library/request-page-bg.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = crlf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | max_line_length = 120 9 | -------------------------------------------------------------------------------- /src/components/rule-preview/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RulePreview } from './RulePreview'; 2 | export { default as RulePreviewControls } from './RulePreviewTopbar'; 3 | -------------------------------------------------------------------------------- /src/services/rules-builder/RulesBuilderTypes.ts: -------------------------------------------------------------------------------- 1 | export interface RulesContent { 2 | markdown: string; 3 | label: string; 4 | fileName: `${string}.mdc`; 5 | } 6 | -------------------------------------------------------------------------------- /tests/setup/types.d.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | declare module 'vitest' { 4 | // interface Assertion extends jest.Matchers {} 5 | // interface AsymmetricMatchersContaining extends jest.Matchers {} 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/data/rules/types.ts: -------------------------------------------------------------------------------- 1 | import { Library } from '../dictionaries'; 2 | 3 | /** 4 | * Type definition for library rules 5 | * Using Partial to allow each module to contain only a subset of libraries 6 | */ 7 | export type LibraryRulesMap = Partial>; 8 | -------------------------------------------------------------------------------- /src/components/rule-builder/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RuleBuilder } from './RuleBuilder'; 2 | export { default as LayerSelector } from './LayerSelector'; 3 | export { default as StackSelector } from './StackSelector'; 4 | export { default as LibrarySelector } from './LibrarySelector'; 5 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "ask": [ 4 | "Bash(npx supabase db reset)", 5 | "Bash(supabase db reset)" 6 | ], 7 | "deny": [ 8 | "Read(./.env)", 9 | "Read(./.env.*)", 10 | "Read(./secrets/**)", 11 | "Read(./dist)" 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Include in .env & .env.test 2 | PUBLIC_ENV_NAME=### 3 | SUPABASE_URL=https://PROJECT_ID.supabase.co 4 | SUPABASE_PUBLIC_KEY=### 5 | 6 | CF_CAPTCHA_SITE_KEY=### 7 | CF_CAPTCHA_SECRET_KEY=### 8 | 9 | # Include in .env.test 10 | E2E_USERNAME_ID=### 11 | E2E_USERNAME=### 12 | E2E_PASSWORD=### -------------------------------------------------------------------------------- /src/data/rules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './frontend'; 3 | export * from './backend'; 4 | export * from './database'; 5 | export * from './infrastructure'; 6 | export * from './testing'; 7 | export * from './coding'; 8 | export * from './helpers'; 9 | export * from './accessibility'; 10 | -------------------------------------------------------------------------------- /supabase/migrations/20251005120000_add_prompt_sort_order.sql: -------------------------------------------------------------------------------- 1 | -- Add sort_order column to prompts table 2 | ALTER TABLE prompts 3 | ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; 4 | 5 | -- Create index for efficient sorting 6 | CREATE INDEX IF NOT EXISTS idx_prompts_sort_order 7 | ON prompts(collection_id, segment_id, sort_order); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "include": [".astro/types.d.ts", "**/*"], 4 | "exclude": ["dist"], 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "jsxImportSource": "react", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/helpers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Helpers 3 | * Centralized exports for all test utilities 4 | */ 5 | 6 | export { 7 | createMockSupabaseClient, 8 | createMemberMockClient, 9 | createAdminMockClient, 10 | type MockClientOptions, 11 | type MockSupabaseClient, 12 | type MockQueryBuilder, 13 | } from './mockSupabaseClient'; 14 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | /** 5 | * A utility function that merges multiple class names together 6 | * and ensures that Tailwind classes are properly merged 7 | */ 8 | export function cn(...inputs: ClassValue[]) { 9 | return twMerge(clsx(inputs)); 10 | } 11 | -------------------------------------------------------------------------------- /tests/setup/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from 'react'; 2 | import type { RenderOptions } from '@testing-library/react'; 3 | import { render } from '@testing-library/react'; 4 | 5 | const customRender = (ui: ReactElement, options?: RenderOptions) => render(ui, options); 6 | 7 | export * from '@testing-library/react'; 8 | export { customRender as render }; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "astroAllowShorthand": false, 5 | "astroSkipFrontmatter": false, 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-astro"], 8 | "overrides": [ 9 | { 10 | "files": "*.astro", 11 | "options": { 12 | "parser": "astro" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /supabase/migrations/20251005000000_add_prompt_descriptions.sql: -------------------------------------------------------------------------------- 1 | -- Add description columns to prompts table 2 | ALTER TABLE prompts 3 | ADD COLUMN description_en TEXT, 4 | ADD COLUMN description_pl TEXT; 5 | 6 | COMMENT ON COLUMN prompts.description_en IS 'Brief description/summary of the prompt in English.'; 7 | COMMENT ON COLUMN prompts.description_pl IS 'Brief description/summary of the prompt in Polish.'; 8 | -------------------------------------------------------------------------------- /src/layouts/partials/Fonts.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /.claude/commands/shadcn-init.md: -------------------------------------------------------------------------------- 1 | # shadcn-init 2 | 3 | Automatyzuje pełną inicjalizację komponentów shadcn/ui w projekcie. 4 | 5 | ``` 6 | Uruchom `npx shadcn@latest init` w projekcie 10xRules.ai. 7 | Nie używaj flagi `--defaults`. 8 | Po inicjalizacji dodaj komponenty: 9 | npx shadcn@latest add button card input tabs table dialog sheet form textarea select switch toast 10 | Potwierdź, że komponenty trafiły do `src/components/ui` i motyw to "new-york". 11 | ``` 12 | -------------------------------------------------------------------------------- /e2e/page-objects/HomePage.ts: -------------------------------------------------------------------------------- 1 | import type { Locator, Page } from '@playwright/test'; 2 | 3 | export class HomePage { 4 | readonly page: Page; 5 | readonly heading: Locator; 6 | 7 | constructor(page: Page) { 8 | this.page = page; 9 | this.heading = page.getByRole('heading', { level: 1 }); 10 | } 11 | 12 | async goto() { 13 | await this.page.goto('/'); 14 | } 15 | 16 | async getTitle(): Promise { 17 | return this.page.title(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/markdown.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: 'FiraCode Nerd Font Mono', 'Consolas', 'Source Code Pro', monospace; 3 | font-size: 12px; 4 | -webkit-font-smoothing: antialiased; 5 | } 6 | 7 | pre code { 8 | font-family: 'FiraCode Nerd Font Mono', 'Consolas', 'Source Code Pro', monospace; 9 | font-size: 12px; 10 | -webkit-font-smoothing: antialiased; 11 | } 12 | 13 | .vscode-body pre code { 14 | font-family: 'FiraCode Nerd Font Mono', 'Consolas', 'Source Code Pro', monospace !important; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/rule-builder/hooks/useMCPDialog.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useMCPDialog = () => { 4 | const [isMCPDialogOpen, setIsMCPDialogOpen] = useState(false); 5 | 6 | const showMCPInstructions = () => { 7 | setIsMCPDialogOpen(true); 8 | }; 9 | 10 | const hideMCPInstructions = () => { 11 | setIsMCPDialogOpen(false); 12 | }; 13 | 14 | return { 15 | isMCPDialogOpen, 16 | showMCPInstructions, 17 | hideMCPInstructions, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/slugify.ts: -------------------------------------------------------------------------------- 1 | export const slugify = (text: string) => 2 | text 3 | .toString() 4 | .normalize('NFD') // usuwa akcenty (diakrytyki) 5 | .replace(/[\u0300-\u036f]/g, '') // usuwa pozostałości po diakrytykach 6 | .toLowerCase() 7 | .replace(/\s+/g, '-') // zamienia spacje na myślniki 8 | .replace(/[^\w-]+/g, '') // usuwa znaki specjalne 9 | .replace(/--+/g, '-') // usuwa wielokrotne myślniki 10 | .replace(/^-+/, '') // usuwa myślnik z początku 11 | .replace(/-+$/, ''); // usuwa myślnik z końca 12 | -------------------------------------------------------------------------------- /.claude/commands/shadcn-status-checkpoint.md: -------------------------------------------------------------------------------- 1 | # shadcn-status-checkpoint 2 | 3 | Prompt do zapisania statusu implementacji widoku. 4 | 5 | ``` 6 | Podsumuj swoją pracę w pliku .ai/{nazwa-zadania}-implementation-status.md w formacie markdown: 7 | 8 | # Status implementacji widoku {nazwa widoku} 9 | 10 | ## Zrealizowane kroki 11 | [Szczegółowa lista zrealizowanych kroków] 12 | 13 | ## Kolejne kroki 14 | [Lista dalszych kroków, zgodna z planem implementacji] 15 | 16 | Po utworzeniu pliku ze statusem, napisz "Gotowe". Na tym zakończ pracę w tym wątku. 17 | ``` 18 | -------------------------------------------------------------------------------- /src/services/rules-builder/RulesGenerationStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Layer, Library, Stack } from '../../data/dictionaries.ts'; 2 | import type { RulesContent } from './RulesBuilderTypes.ts'; 3 | 4 | /** 5 | * Strategy interface for rules generation 6 | */ 7 | export interface RulesGenerationStrategy { 8 | generateRules( 9 | projectName: string, 10 | projectDescription: string, 11 | selectedLibraries: Library[], 12 | stacksByLayer: Record, 13 | librariesByStack: Record, 14 | ): RulesContent[]; 15 | } 16 | -------------------------------------------------------------------------------- /mcp-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | environment: 'node', 8 | include: ['tests/**/*.test.ts'], 9 | coverage: { 10 | provider: 'v8', 11 | reporter: ['text', 'json', 'html'], 12 | include: ['src/**/*.ts'], 13 | exclude: [ 14 | 'src/types/**', 15 | '**/*.d.ts', 16 | '**/*.test.ts' 17 | ] 18 | } 19 | }, 20 | resolve: { 21 | alias: { 22 | '@': path.resolve(__dirname, './src') 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /src/store/authStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface User { 4 | id: string; 5 | email: string | null; 6 | } 7 | 8 | interface AuthState { 9 | user: User | null; 10 | isAuthenticated: boolean; 11 | setUser: (user: User | null) => void; 12 | logout: () => void; 13 | } 14 | 15 | export const useAuthStore = create((set) => ({ 16 | user: null, 17 | isAuthenticated: false, 18 | setUser: (user) => set({ user, isAuthenticated: !!user }), 19 | logout: () => set({ user: null, isAuthenticated: false }), 20 | })); 21 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:astro/recommended', 6 | ], 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint'], 9 | overrides: [ 10 | { 11 | files: ['*.astro'], 12 | parser: 'astro-eslint-parser', 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | extraFileExtensions: ['.astro'], 16 | }, 17 | }, 18 | ], 19 | env: { 20 | browser: true, 21 | node: true, 22 | }, 23 | root: true, 24 | }; 25 | -------------------------------------------------------------------------------- /src/pages/privacy/en/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../../layouts/Layout.astro'; 3 | import PrivacyLayout from '../../../components/privacy/PrivacyLayout.astro'; 4 | 5 | const privacyContent = await import('../../../assets/privacy-policy/pp-13-04-2025-en.md'); 6 | const Content = privacyContent.Content; 7 | --- 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "lib": ["es2021"], 5 | "jsx": "react-jsx", 6 | "module": "es2022", 7 | "moduleResolution": "Bundler", 8 | "resolveJsonModule": true, 9 | "allowJs": true, 10 | "checkJs": false, 11 | "noEmit": true, 12 | "isolatedModules": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "types": [ 18 | "@cloudflare/workers-types/2023-07-01" 19 | ] 20 | }, 21 | "include": ["worker-configuration.d.ts", "src/**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/privacy/pl/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../../layouts/Layout.astro'; 3 | import PrivacyLayout from '../../../components/privacy/PrivacyLayout.astro'; 4 | 5 | const privacyContent = await import('../../../assets/privacy-policy/pp-13-04-2025-pl.md'); 6 | const Content = privacyContent.Content; 7 | --- 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/components/landing/FeatureCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | icon: string; 4 | title: string; 5 | description: string; 6 | } 7 | 8 | const { icon, title, description } = Astro.props; 9 | --- 10 | 11 |
14 |
15 | {icon} 16 |
17 | 18 |

19 | {title} 20 |

21 | 22 |

23 | {description} 24 |

25 |
26 | -------------------------------------------------------------------------------- /src/pages/prompts/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../layouts/Layout.astro'; 3 | import Topbar from '../../components/Topbar'; 4 | import Footer from '../../components/Footer'; 5 | import PromptsBrowser from '../../components/prompt-library/PromptsBrowser'; 6 | 7 | const user = Astro.locals.user; 8 | --- 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /e2e/home.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { HomePage } from './page-objects/HomePage'; 3 | 4 | test.describe('Home Page', () => { 5 | test('should have the correct title', async ({ page }) => { 6 | const homePage = new HomePage(page); 7 | await homePage.goto(); 8 | 9 | const title = await homePage.getTitle(); 10 | expect(title).toContain('10xRules.ai'); 11 | }); 12 | 13 | test('should have a heading', async ({ page }) => { 14 | const homePage = new HomePage(page); 15 | await homePage.goto(); 16 | 17 | await expect(homePage.heading).toBeVisible(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/pages/auth/reset-password.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../layouts/Layout.astro'; 3 | import AuthLayout from '../../components/auth/AuthLayout'; 4 | import ResetPasswordForm from '../../components/auth/ResetPasswordForm'; 5 | 6 | import { CF_CAPTCHA_SITE_KEY } from 'astro:env/server'; 7 | --- 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /src/pages/auth/update-password.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../layouts/Layout.astro'; 3 | import AuthLayout from '../../components/auth/AuthLayout'; 4 | import UpdatePasswordResetForm from '../../components/auth/UpdatePasswordResetForm'; 5 | 6 | import { CF_CAPTCHA_SITE_KEY } from 'astro:env/server'; 7 | --- 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /supabase/migrations/20251002100000_localize_prompts_table.sql: -------------------------------------------------------------------------------- 1 | -- Alter prompts table for localization 2 | ALTER TABLE prompts 3 | RENAME COLUMN title TO title_en; 4 | 5 | ALTER TABLE prompts 6 | RENAME COLUMN markdown_body TO markdown_body_en; 7 | 8 | ALTER TABLE prompts 9 | ADD COLUMN title_pl TEXT, 10 | ADD COLUMN markdown_body_pl TEXT; 11 | 12 | COMMENT ON COLUMN prompts.title_en IS 'Title of the prompt in English.'; 13 | COMMENT ON COLUMN prompts.markdown_body_en IS 'Markdown body of the prompt in English.'; 14 | COMMENT ON COLUMN prompts.title_pl IS 'Title of the prompt in Polish.'; 15 | COMMENT ON COLUMN prompts.markdown_body_pl IS 'Markdown body of the prompt in Polish.'; 16 | -------------------------------------------------------------------------------- /src/components/ui/StatusBadge.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface StatusBadgeProps { 4 | status: 'draft' | 'published'; 5 | className?: string; 6 | } 7 | 8 | const StatusBadge: React.FC = ({ status, className = '' }) => { 9 | const isDraft = status === 'draft'; 10 | 11 | return ( 12 | 19 | {isDraft ? 'Draft' : 'Published'} 20 | 21 | ); 22 | }; 23 | 24 | export default StatusBadge; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | .env.test 20 | .env.integration 21 | .env.local 22 | 23 | # macOS-specific files 24 | .DS_Store 25 | 26 | # jetbrains setting folder 27 | .idea/ 28 | 29 | playwright/.auth 30 | test-results 31 | coverage 32 | playwright-report/* 33 | playwright-report/index.html 34 | 35 | # Generated data files 36 | src/data/preparedRules.json 37 | mcp-server/src/preparedRules.json 38 | 39 | # Codex MCP settings 40 | codex.context7.toml -------------------------------------------------------------------------------- /src/db/supabase-admin.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import { SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY } from 'astro:env/server'; 3 | import type { Database } from './database.types'; 4 | 5 | if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) { 6 | throw new Error('Missing Supabase admin credentials'); 7 | } 8 | 9 | /** 10 | * Supabase admin client with service role key for server-side operations 11 | * that bypass RLS policies. Use with caution and only in trusted server contexts. 12 | */ 13 | export const supabaseAdmin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { 14 | auth: { 15 | autoRefreshToken: false, 16 | persistSession: false, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/services/captcha.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface CaptchaResponse { 4 | success: boolean; 5 | challenge_ts: string; 6 | hostname: string; 7 | 'error-codes': string[]; 8 | } 9 | 10 | export async function verifyCaptcha( 11 | captchaSecret: string, 12 | captchaToken: string, 13 | requestorIp: string, 14 | ): Promise { 15 | const response = await axios.post( 16 | `https://challenges.cloudflare.com/turnstile/v0/siteverify`, 17 | { 18 | secret: captchaSecret, 19 | response: captchaToken, 20 | remoteip: requestorIp, 21 | }, 22 | ); 23 | const captchaResult = response.data; 24 | return captchaResult; 25 | } 26 | -------------------------------------------------------------------------------- /src/store/navigationStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | export type Panel = 'collections' | 'builder' | 'preview'; 4 | 5 | interface NavigationState { 6 | activePanel: Panel; 7 | isSidebarOpen: boolean; 8 | setActivePanel: (panel: Panel) => void; 9 | toggleSidebar: () => void; 10 | setSidebarOpen: (isOpen: boolean) => void; 11 | } 12 | 13 | export const useNavigationStore = create((set) => ({ 14 | activePanel: 'builder', 15 | isSidebarOpen: false, 16 | setActivePanel: (panel) => set({ activePanel: panel }), 17 | toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })), 18 | setSidebarOpen: (isOpen) => set({ isSidebarOpen: isOpen }), 19 | })); 20 | -------------------------------------------------------------------------------- /src/hooks/useTokenHashVerification.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * Hook to extract and validate the token_hash from URL parameters. 5 | * Does NOT perform API verification - that happens on form submit. 6 | */ 7 | export const useTokenHashVerification = () => { 8 | const [tokenHash, setTokenHash] = useState(null); 9 | const [error, setError] = useState(null); 10 | 11 | useEffect(() => { 12 | const hash = new URLSearchParams(window.location.search).get('token_hash'); 13 | if (!hash) { 14 | setError('No reset token found'); 15 | } else { 16 | setTokenHash(hash); 17 | } 18 | }, []); 19 | 20 | return { tokenHash, error }; 21 | }; 22 | -------------------------------------------------------------------------------- /mcp-server/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.2/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": ["worker-configuration.d.ts"] 8 | }, 9 | "vcs": { 10 | "enabled": true, 11 | "clientKind": "git", 12 | "useIgnoreFile": true 13 | }, 14 | "linter": { 15 | "enabled": true, 16 | "rules": { 17 | "recommended": true, 18 | "suspicious": { 19 | "noExplicitAny": "off", 20 | "noDebugger": "off", 21 | "noConsoleLog": "off", 22 | "noConfusingVoidType": "off" 23 | }, 24 | "style": { 25 | "noNonNullAssertion": "off" 26 | } 27 | } 28 | }, 29 | "formatter": { 30 | "enabled": true, 31 | "indentWidth": 4, 32 | "lineWidth": 100 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/invites/[token].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../layouts/Layout.astro'; 3 | import InviteLanding from '../../components/invites/InviteLanding'; 4 | import { isFeatureEnabled } from '../../features/featureFlags'; 5 | 6 | export const prerender = false; 7 | 8 | // Check if org invites feature is enabled 9 | if (!isFeatureEnabled('orgInvites')) { 10 | return Astro.redirect('/'); 11 | } 12 | 13 | const { token } = Astro.params; 14 | 15 | // Check if user is authenticated 16 | const isAuthenticated = !!Astro.locals.user; 17 | 18 | if (!token) { 19 | return Astro.redirect('/'); 20 | } 21 | --- 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/rule-preview/RulesPath.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useProjectStore } from '../../store/projectStore'; 3 | import { aiEnvironmentConfig } from '../../data/ai-environments.ts'; 4 | 5 | export const RulesPath: React.FC = () => { 6 | const { selectedEnvironment } = useProjectStore(); 7 | 8 | // Get the appropriate file path based on the selected format 9 | const getFilePath = (): string => { 10 | const config = aiEnvironmentConfig[selectedEnvironment]; 11 | return config?.filePath || 'Unknown path'; 12 | }; 13 | 14 | return ( 15 |
16 | Path: {getFilePath()} 17 |
18 | ); 19 | }; 20 | 21 | export default RulesPath; 22 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro'; 3 | import Topbar from '../components/Topbar'; 4 | import TwoPane from '../components/TwoPane'; 5 | import Footer from '../components/Footer'; 6 | import Landing from '../components/landing/Landing.astro'; 7 | 8 | const user = Astro.locals.user; 9 | const initialUrl = Astro.url; 10 | --- 11 | 12 | 13 | {!user && } 14 | { 15 | user && ( 16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | ) 24 | } 25 |
26 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@astrojs/react'; 3 | import { configDefaults } from 'vitest/config'; 4 | import path from 'path'; 5 | 6 | export default defineConfig({ 7 | plugins: [react()], 8 | test: { 9 | globals: true, 10 | environment: 'jsdom', 11 | setupFiles: ['./tests/setup/vitest.setup.ts'], 12 | exclude: [...configDefaults.exclude, 'e2e/**'], 13 | coverage: { 14 | provider: 'v8', 15 | reporter: ['text', 'json', 'html'], 16 | exclude: ['**/node_modules/**', '**/tests/**', '**/dist/**'], 17 | }, 18 | }, 19 | resolve: { 20 | alias: { 21 | '@': path.resolve(__dirname, './src'), 22 | 'astro:middleware': path.resolve(__dirname, './tests/stubs/astro-middleware.ts'), 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /tests/stubs/astro-middleware.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareHandler } from 'astro'; 2 | 3 | type NextHandler = () => Promise; 4 | 5 | export function defineMiddleware(handler: T): T { 6 | return handler; 7 | } 8 | 9 | export function sequence(...handlers: MiddlewareHandler[]): MiddlewareHandler { 10 | return async function composed(context, next) { 11 | let index = -1; 12 | 13 | const dispatch = async (i: number): Promise => { 14 | if (i <= index) { 15 | throw new Error('next() called multiple times'); 16 | } 17 | 18 | index = i; 19 | const handler = handlers[i]; 20 | 21 | if (!handler) { 22 | return next(); 23 | } 24 | 25 | return handler(context, () => dispatch(i + 1)); 26 | }; 27 | 28 | return dispatch(0); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | variant?: 'primary' | 'secondary'; 4 | href: string; 5 | class?: string; 6 | } 7 | 8 | const { variant = 'primary', href, class: className = '' } = Astro.props; 9 | 10 | const baseStyles = 11 | 'inline-flex items-center justify-center px-6 py-3 rounded-lg font-medium transition-all duration-300 shadow-lg hover:shadow-xl transform '; 12 | 13 | const variantStyles = { 14 | primary: 15 | 'bg-gradient-to-r from-blue-500 to-teal-500 hover:from-teal-500 hover:to-purple-500 text-white', 16 | secondary: 17 | 'border-2 border-gray-700 hover:border-gray-600 bg-gray-900 hover:bg-gray-800 text-white', 18 | }; 19 | 20 | const classes = `${baseStyles} ${variantStyles[variant]} ${className}`; 21 | --- 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/ui/MarkdownRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { processRulesContentMarkdown } from '../../utils/markdownStyling.tsx'; 3 | 4 | interface MarkdownRendererProps { 5 | content: string; 6 | className?: string; 7 | actions?: React.ReactNode; 8 | } 9 | 10 | export const MarkdownRenderer: React.FC = ({ 11 | content, 12 | className = '', 13 | actions, 14 | }) => { 15 | return ( 16 |
17 | {actions &&
{actions}
} 18 |
19 |         {processRulesContentMarkdown(content)}
20 |       
21 |
22 | ); 23 | }; 24 | 25 | export default MarkdownRenderer; 26 | -------------------------------------------------------------------------------- /supabase/migrations/20251003170000_user_emails_view.sql: -------------------------------------------------------------------------------- 1 | -- Create a function to fetch user emails (needed because auth.users is not accessible via views) 2 | -- This function runs with SECURITY DEFINER to access auth.users 3 | CREATE OR REPLACE FUNCTION public.get_user_emails(user_ids UUID[]) 4 | RETURNS TABLE (id UUID, email TEXT) 5 | LANGUAGE plpgsql 6 | SECURITY DEFINER 7 | SET search_path = public, auth 8 | AS $$ 9 | BEGIN 10 | RETURN QUERY 11 | SELECT 12 | au.id, 13 | au.email::TEXT 14 | FROM auth.users au 15 | WHERE au.id = ANY(user_ids); 16 | END; 17 | $$; 18 | 19 | -- Grant execute permission to authenticated users 20 | GRANT EXECUTE ON FUNCTION public.get_user_emails(UUID[]) TO authenticated; 21 | 22 | COMMENT ON FUNCTION public.get_user_emails IS 'Fetches user emails for given user IDs. Used by invite stats and organization member lists.'; 23 | -------------------------------------------------------------------------------- /.cursor/rules/astro.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Building server-side Astro pages 3 | globs: 4 | alwaysApply: false 5 | --- 6 | ### Guidelines for Astro 7 | 8 | - Use content collections with type safety for blog posts, documentation, etc. 9 | - Leverage Server Endpoints for API routes 10 | - Use POST, GET - uppercase format for endpoint handlers 11 | - Use `export const prerender = false` for API routes 12 | - Use zod for input validation in API routes 13 | - Implement or reuse middleware for request/response modification 14 | - Use image optimization with the Astro Image integration 15 | - Implement hybrid rendering with server-side rendering where needed 16 | - Use Astro.cookies for server-side cookie management 17 | - Leverage import.meta.env for environment variables 18 | - Always check if you're asked to create public or private pages (if public, update `src/middleware/index.ts`) to allow non-auth browsing -------------------------------------------------------------------------------- /mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "10x-rules-mcp-server", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "format": "biome format --write", 9 | "lint:fix": "biome lint --fix", 10 | "start": "wrangler dev", 11 | "cf-typegen": "wrangler types", 12 | "test": "vitest run", 13 | "test:watch": "vitest", 14 | "test:coverage": "vitest run --coverage", 15 | "test:ui": "vitest --ui" 16 | }, 17 | "devDependencies": { 18 | "@cloudflare/workers-types": "^4.20250427.0", 19 | "@vitest/ui": "^2.0.0", 20 | "@vitest/coverage-v8": "^2.0.0", 21 | "typescript": "^5.5.2", 22 | "vitest": "^2.0.0", 23 | "wrangler": "^4.36.0" 24 | }, 25 | "dependencies": { 26 | "@modelcontextprotocol/sdk": "^1.7.0", 27 | "agents": "^0.2.14", 28 | "zod": "^3.24.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/prompts/admin/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../../layouts/Layout.astro'; 3 | import Topbar from '../../../components/Topbar'; 4 | import Footer from '../../../components/Footer'; 5 | import PromptsAdminPanel from '../../../components/prompt-library/admin/PromptsAdminPanel'; 6 | import { isFeatureEnabled } from '../../../features/featureFlags'; 7 | 8 | const user = Astro.locals.user; 9 | // Middleware already ensures admin access 10 | const showInvites = isFeatureEnabled('orgInvites'); 11 | --- 12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /src/types/ruleCollection.types.ts: -------------------------------------------------------------------------------- 1 | import { type Library } from '../data/dictionaries'; 2 | import { type Database } from '../db/database.types'; 3 | 4 | export interface RuleCollection { 5 | id: string; 6 | userId: string; 7 | name: string; 8 | description: string | null; 9 | libraries: Library[]; 10 | createdAt: string; 11 | updatedAt: string; 12 | } 13 | 14 | export function ruleCollectionMapper( 15 | collection: Database['public']['Tables']['collections']['Row'], 16 | ): RuleCollection { 17 | return { 18 | id: collection.id, 19 | userId: collection.user_id, 20 | name: collection.name, 21 | description: collection.description, 22 | libraries: collection.libraries as Library[], 23 | createdAt: collection.created_at, 24 | updatedAt: collection.updated_at, 25 | }; 26 | } 27 | 28 | export const DEFAULT_USER_ID = '899b93b3-f661-4471-9995-1165701d9f51'; 29 | -------------------------------------------------------------------------------- /src/components/ui/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | size?: 'sm' | 'md' | 'lg'; 4 | class?: string; 5 | } 6 | 7 | const { size = 'md', class: className = '' } = Astro.props; 8 | 9 | const sizeClasses = { 10 | sm: { 11 | icon: 'text-lg', 12 | text: 'text-lg md:text-xl', 13 | }, 14 | md: { 15 | icon: 'text-xl', 16 | text: 'text-xl md:text-2xl', 17 | }, 18 | lg: { 19 | icon: 'text-2xl', 20 | text: 'text-2xl md:text-3xl', 21 | }, 22 | }; 23 | 24 | const { icon, text } = sizeClasses[size]; 25 | --- 26 | 27 |
28 | 29 |

32 | 10xRules.ai 33 |

34 |
35 | -------------------------------------------------------------------------------- /supabase/migrations/20250411083417_create_user_consents.sql: -------------------------------------------------------------------------------- 1 | -- Migration: Create user consents table 2 | -- Description: Stores user consent records for privacy policy acceptance 3 | -- Tables affected: user_consents 4 | -- Special considerations: Implements RLS for data protection 5 | -- Create user consents table 6 | create table user_consents ( 7 | id uuid default gen_random_uuid() primary key, 8 | user_id uuid references auth.users(id) on delete cascade not null, 9 | privacy_policy_version text not null, 10 | consented_at timestamptz default now() not null, 11 | created_at timestamptz default now() not null 12 | ); 13 | -- Create indexes 14 | create index user_consents_user_id_idx on user_consents(user_id); 15 | -- Comments 16 | comment on table user_consents is 'Stores user consent records for privacy policy and other terms'; 17 | comment on column user_consents.privacy_policy_version is 'Version or date of the privacy policy that was accepted'; -------------------------------------------------------------------------------- /.cursor/rules/playwright-e2e-testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: E2E testing with Playwright 3 | globs: 4 | alwaysApply: false 5 | --- 6 | 7 | ## TESTING 8 | 9 | ### Guidelines for E2E 10 | 11 | #### PLAYWRIGHT 12 | 13 | - Initialize configuration only with Chromium/Desktop Chrome browser 14 | - Use browser contexts for isolating test environments 15 | - Implement the Page Object Model for maintainable tests in ./e2e/page-objects 16 | - Use locators for resilient element selection 17 | - Leverage API testing for backend validation 18 | - Implement visual comparison with expect(page).toHaveScreenshot() 19 | - Use the codegen tool for test recording 20 | - Leverage trace viewer for debugging test failures 21 | - Implement test hooks for setup and teardown 22 | - Use expect assertions with specific matchers 23 | - Leverage parallel execution for faster test runs 24 | - Follow 'Arrange', 'Act', 'Assert' approach to test structure for simplicity and readability. 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/pages/api/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from 'astro'; 2 | import { isFeatureEnabled } from '../../../features/featureFlags'; 3 | 4 | export const POST: APIRoute = async ({ locals }) => { 5 | // Check if auth feature is enabled 6 | if (!isFeatureEnabled('auth')) { 7 | return new Response(JSON.stringify({ error: 'Authentication is currently disabled' }), { 8 | status: 403, 9 | }); 10 | } 11 | 12 | try { 13 | const { error } = await locals.supabase.auth.signOut(); 14 | 15 | if (error) { 16 | return new Response(JSON.stringify({ error: error.message }), { 17 | status: 400, 18 | }); 19 | } 20 | 21 | return new Response(JSON.stringify({ success: true }), { 22 | status: 200, 23 | }); 24 | } catch (err) { 25 | console.error('Logout error:', err); 26 | return new Response(JSON.stringify({ error: 'An unexpected error occurred' }), { 27 | status: 500, 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/ui/StepCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | stepNumber: number; 4 | title: string; 5 | description: string; 6 | icon: string; 7 | } 8 | 9 | const { stepNumber, title, description, icon } = Astro.props; 10 | --- 11 | 12 |
15 | 16 |
19 | {stepNumber} 20 |
21 | 22 | 23 |
24 | {icon} 25 |
26 | 27 | 28 |

29 | {title} 30 |

31 | 32 |

33 | {description} 34 |

35 |
36 | -------------------------------------------------------------------------------- /src/pages/prompts/admin/invites.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../../../layouts/Layout.astro'; 3 | import Topbar from '../../../components/Topbar'; 4 | import Footer from '../../../components/Footer'; 5 | import InvitesAdminPanel from '../../../components/prompt-library/admin/InvitesAdminPanel'; 6 | import { isFeatureEnabled } from '../../../features/featureFlags'; 7 | 8 | export const prerender = false; 9 | 10 | const user = Astro.locals.user; 11 | // Middleware already ensures admin access 12 | 13 | // Check if org invites feature is enabled 14 | if (!isFeatureEnabled('orgInvites')) { 15 | return Astro.redirect('/prompts'); 16 | } 17 | --- 18 | 19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /supabase/migrations/20250328135512_collections.sql: -------------------------------------------------------------------------------- 1 | -- Create collections table 2 | CREATE TABLE collections ( 3 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 4 | user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, 5 | name VARCHAR(255) NOT NULL, 6 | description TEXT, 7 | libraries TEXT [] NOT NULL DEFAULT '{}', 8 | created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()) NOT NULL, 9 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW()) NOT NULL 10 | ); 11 | -- Create index on user_id for faster lookups 12 | CREATE INDEX collections_user_id_idx ON collections(user_id); 13 | -- Function to automatically update updated_at timestamp 14 | CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = TIMEZONE('utc', NOW()); 15 | RETURN NEW; 16 | END; 17 | $$ language 'plpgsql'; 18 | -- Trigger to call the update function 19 | CREATE TRIGGER update_collections_updated_at BEFORE 20 | UPDATE ON collections FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -------------------------------------------------------------------------------- /.cursor/rules/react-development.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Developing React components in Astro application 3 | globs: 4 | alwaysApply: false 5 | --- 6 | ## Frontend 7 | 8 | ### Guidelines for React 9 | 10 | #### React Coding Standards 11 | 12 | - Use functional components with hooks instead of class components 13 | - Implement React.memo() for expensive components that render often with the same props 14 | - Utilize React.lazy() and Suspense for code-splitting and performance optimization 15 | - Use the useCallback hook for event handlers passed to child components to prevent unnecessary re-renders 16 | - Prefer useMemo for expensive calculations to avoid recomputation on every render 17 | - Prefer relying on Tailwind media queries instead of manual style recalculations 18 | - If there's a need to attach React components to Astro pages and make them browser-first (i.e. using window), use client:only directive to make the component exclusively run on the client. 19 | - Use Tailwind responsive variants (sm:, md:, lg:, etc.) for adaptive designs - under no circumstances calculate this manually -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/global.css'; 3 | import SEO from './partials/SEO.astro'; 4 | import Fonts from './partials/Fonts.astro'; 5 | import GTMContainer from './partials/GTMContainer.astro'; 6 | import CookieBanner from '../components/cookie-banner/CookieBanner.tsx'; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 10xRules.ai 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/setup/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { afterAll, afterEach, beforeAll, vi } from 'vitest'; 3 | import { setupServer } from 'msw/node'; 4 | import { cleanup } from '@testing-library/react'; 5 | 6 | // Add any global mocks here that should be available for all tests 7 | const server = setupServer(); 8 | 9 | // Start MSW server before all tests 10 | beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })); 11 | 12 | // Clean up after each test 13 | afterEach(() => { 14 | cleanup(); 15 | server.resetHandlers(); 16 | }); 17 | 18 | // Close MSW server after all tests 19 | afterAll(() => server.close()); 20 | 21 | // Global mock for window.matchMedia 22 | Object.defineProperty(window, 'matchMedia', { 23 | writable: true, 24 | value: vi.fn().mockImplementation((query: string) => ({ 25 | matches: false, 26 | media: query, 27 | onchange: null, 28 | addListener: vi.fn(), 29 | removeListener: vi.fn(), 30 | addEventListener: vi.fn(), 31 | removeEventListener: vi.fn(), 32 | dispatchEvent: vi.fn(), 33 | })), 34 | }); 35 | -------------------------------------------------------------------------------- /src/components/auth/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from '../ui/Logo'; 3 | 4 | interface AuthLayoutProps { 5 | children: React.ReactNode; 6 | subtitle?: string; 7 | } 8 | 9 | export const AuthLayout: React.FC = ({ children, subtitle }) => { 10 | return ( 11 |
12 |
13 |
14 | 15 | 16 | 17 | {subtitle &&

{subtitle}

} 18 |
19 |
20 | {children} 21 |
22 |
23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | export default AuthLayout; 30 | -------------------------------------------------------------------------------- /src/components/ui/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LogoProps { 4 | size?: 'sm' | 'md' | 'lg'; 5 | className?: string; 6 | } 7 | 8 | export const Logo: React.FC = ({ size = 'md', className = '' }) => { 9 | const sizeClasses = { 10 | sm: { 11 | icon: 'text-lg', 12 | text: 'text-lg md:text-xl', 13 | }, 14 | md: { 15 | icon: 'text-xl', 16 | text: 'text-xl md:text-2xl', 17 | }, 18 | lg: { 19 | icon: 'text-2xl', 20 | text: 'text-2xl md:text-3xl', 21 | }, 22 | }; 23 | 24 | const { icon, text } = sizeClasses[size]; 25 | 26 | return ( 27 |
28 | 31 |

34 | 10xRules.ai 35 |

36 |
37 | ); 38 | }; 39 | 40 | export default Logo; 41 | -------------------------------------------------------------------------------- /src/data/rules/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Library } from '../dictionaries'; 2 | import { type LibraryRulesMap } from './types'; 3 | 4 | /** 5 | * Get rules for a specific library 6 | * @param rulesMap The rules map to get rules from 7 | * @param library The library to get rules for 8 | * @returns Array of rules for the library 9 | */ 10 | export const getRulesForLibrary = (rulesMap: LibraryRulesMap, library: Library): string[] => { 11 | return rulesMap[library] || []; 12 | }; 13 | 14 | /** 15 | * Get rules for multiple libraries 16 | * @param rulesMap The rules map to get rules from 17 | * @param libraries Array of libraries to get rules for 18 | * @returns Record with libraries as keys and arrays of rules as values 19 | */ 20 | export const getRulesForLibraries = ( 21 | rulesMap: LibraryRulesMap, 22 | libraries: Library[], 23 | ): Partial> => { 24 | const result: Partial> = {}; 25 | 26 | libraries.forEach((library) => { 27 | if (rulesMap[library]) { 28 | result[library] = getRulesForLibrary(rulesMap, library); 29 | } 30 | }); 31 | 32 | return result; 33 | }; 34 | -------------------------------------------------------------------------------- /src/data/landingAnimations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Landing page animation constants 3 | * Animation-related configuration shared across landing page scripts 4 | */ 5 | 6 | // Animation durations (in milliseconds) 7 | export const ANIMATION_DURATIONS = { 8 | // Transition durations for hover effects 9 | HOVER_TRANSITION: 300, 10 | 11 | // Tab switching animations 12 | TAB_FADE_IN: 400, 13 | TAB_FADE_OUT: 200, 14 | TAB_HIDE_DELAY: 200, 15 | 16 | // Counter animations 17 | COUNTER_DEFAULT: 1500, 18 | COUNTER_LAYER: 1200, 19 | COUNTER_STAGGER_DELAY: 100, 20 | 21 | // Copy button feedback 22 | COPY_FEEDBACK: 2000, 23 | } as const; 24 | 25 | // Root margins for Intersection Observer (in pixels) 26 | export const ROOT_MARGINS = { 27 | DEFAULT: '0px 0px -50px 0px', 28 | LARGE: '0px 0px -100px 0px', 29 | } as const; 30 | 31 | // Threshold values for Intersection Observer (0-1 scale) 32 | export const OBSERVER_THRESHOLDS = { 33 | LOW: 0.1, 34 | MEDIUM: 0.2, 35 | HIGH: 0.3, 36 | } as const; 37 | 38 | // Magnetic button effect settings 39 | export const MAGNETIC_BUTTON = { 40 | TRANSFORM_MULTIPLIER: 0.2, 41 | HOVER_SCALE: 1.05, 42 | DEFAULT_SCALE: 1, 43 | } as const; 44 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, envField } from 'astro/config'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | import react from '@astrojs/react'; 5 | 6 | import cloudflare from '@astrojs/cloudflare'; 7 | 8 | // https://astro.build/config 9 | export default defineConfig({ 10 | output: 'server', 11 | server: { 12 | port: 3000, 13 | }, 14 | env: { 15 | schema: { 16 | PUBLIC_ENV_NAME: envField.string({ context: 'server', access: 'secret' }), 17 | SUPABASE_URL: envField.string({ context: 'server', access: 'secret' }), 18 | SUPABASE_PUBLIC_KEY: envField.string({ context: 'server', access: 'secret' }), 19 | SUPABASE_SERVICE_ROLE_KEY: envField.string({ context: 'server', access: 'secret' }), 20 | CF_CAPTCHA_SITE_KEY: envField.string({ 21 | context: 'server', 22 | access: 'secret', 23 | }), 24 | CF_CAPTCHA_SECRET_KEY: envField.string({ 25 | context: 'server', 26 | access: 'secret', 27 | }), 28 | }, 29 | }, 30 | vite: { 31 | plugins: [tailwindcss()], 32 | }, 33 | devToolbar: { 34 | enabled: false, 35 | }, 36 | integrations: [react()], 37 | adapter: cloudflare(), 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/privacy/PrivacyLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Footer from '../Footer.tsx'; 3 | import Topbar from '../Topbar.tsx'; 4 | 5 | const user = Astro.locals.user; 6 | 7 | interface Props { 8 | title: string; 9 | lang: 'en' | 'pl'; 10 | } 11 | 12 | const { title, lang } = Astro.props; 13 | const otherLang = lang === 'en' ? 'pl' : 'en'; 14 | const otherLangPath = `/privacy/${otherLang}`; 15 | --- 16 | 17 |
18 | 19 |
20 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /supabase/migrations/20250413093000_prompt_manager_orgs.sql: -------------------------------------------------------------------------------- 1 | -- Prompt Manager Phase 2: foundational organization tables 2 | CREATE TABLE IF NOT EXISTS organizations ( 3 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 4 | slug TEXT NOT NULL UNIQUE, 5 | name TEXT NOT NULL, 6 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT TIMEZONE('utc', NOW()), 7 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT TIMEZONE('utc', NOW()) 8 | ); 9 | 10 | CREATE TABLE IF NOT EXISTS organization_members ( 11 | organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE, 12 | user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, 13 | role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'admin')), 14 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT TIMEZONE('utc', NOW()), 15 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT TIMEZONE('utc', NOW()), 16 | PRIMARY KEY (organization_id, user_id) 17 | ); 18 | 19 | CREATE INDEX IF NOT EXISTS organization_members_user_id_idx 20 | ON organization_members(user_id); 21 | 22 | -- Seed the launch organization; memberships are populated via the launch roster sync runbook. 23 | INSERT INTO organizations (slug, name) 24 | VALUES ('10xdevs', '10xDevs') 25 | ON CONFLICT (slug) DO NOTHING; 26 | -------------------------------------------------------------------------------- /src/components/rule-preview/MarkdownContentRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { RulesContent } from '../../services/rules-builder/RulesBuilderTypes.ts'; 3 | import RulesPreviewCopyDownloadActions from './RulesPreviewCopyDownloadActions.tsx'; 4 | import { processRulesContentMarkdown } from '../../utils/markdownStyling.tsx'; 5 | 6 | // Component for rendering markdown content 7 | export const MarkdownContentRenderer: React.FC<{ markdownContent: RulesContent[] }> = ({ 8 | markdownContent, 9 | }) => { 10 | return ( 11 |
12 | {markdownContent.map((rule, index) => ( 13 |
17 | {markdownContent.length > 1 && ( 18 |
19 | 20 |
21 | )} 22 | 23 |
24 |             {processRulesContentMarkdown(rule.markdown)}
25 |           
26 |
27 | ))} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/rule-preview/DependencyUpload.tsx: -------------------------------------------------------------------------------- 1 | import { FileUp } from 'lucide-react'; 2 | import React, { Fragment } from 'react'; 3 | 4 | import type { UploadStatus } from '../rule-parser/useDependencyUpload.ts'; 5 | 6 | // Component for the dependency dropzone overlay 7 | export const DependencyUpload: React.FC<{ uploadStatus: UploadStatus; isDragging: boolean }> = ({ 8 | uploadStatus, 9 | isDragging, 10 | }) => { 11 | if (!isDragging) return null; 12 | 13 | return ( 14 | 15 |
16 | 17 |

18 | Drop dependency file to identify libraries 19 |

20 |

Supported: package.json, requirements.txt

21 |
22 | {/* Upload status message */} 23 | {uploadStatus.message && ( 24 |
25 | {uploadStatus.message} 26 |
27 | )} 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/ui/FormInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { transitions } from '../../styles/theme'; 3 | 4 | interface FormInputProps extends React.InputHTMLAttributes { 5 | label: string; 6 | error?: string; 7 | } 8 | 9 | const FormInput = React.forwardRef( 10 | ({ label, error, id, ...inputProps }, ref) => { 11 | return ( 12 |
13 | 16 | 28 | {error &&

{error}

} 29 |
30 | ); 31 | }, 32 | ); 33 | FormInput.displayName = 'FormInput'; 34 | 35 | export default FormInput; 36 | -------------------------------------------------------------------------------- /src/styles/animations/reduced-motion.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Accessibility: Reduced motion preferences 3 | * Respects user's prefers-reduced-motion setting 4 | */ 5 | 6 | @media (prefers-reduced-motion: reduce) { 7 | *, 8 | *::before, 9 | *::after { 10 | animation-duration: 0.01ms !important; 11 | animation-iteration-count: 1 !important; 12 | transition-duration: 0.01ms !important; 13 | scroll-behavior: auto !important; 14 | } 15 | 16 | /* Disable gradient orbs entirely - too expensive for reduced motion */ 17 | .gradient-orb { 18 | display: none !important; 19 | } 20 | 21 | /* Disable shimmer effects */ 22 | .total-count, 23 | .final-cta-button::after { 24 | animation: none !important; 25 | } 26 | 27 | /* Disable pulse rings */ 28 | .cta-button::before { 29 | animation: none !important; 30 | } 31 | 32 | /* Keep focus indicators but remove animation */ 33 | a:focus-visible, 34 | button:focus-visible { 35 | animation: none !important; 36 | } 37 | 38 | /* Simplify transforms to immediate states */ 39 | .before-after-card:hover, 40 | .path-card:hover, 41 | .contributor-avatar:hover { 42 | transform: none !important; 43 | } 44 | 45 | .feature-item:hover .feature-icon { 46 | transform: none !important; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.ai/project-mvp.md: -------------------------------------------------------------------------------- 1 | # MVP dla 10xRules.ai 2 | 3 | Główny problem: Aby podnieść jakość współpracy programisty z AI, konieczne jest zdefiniowanie projektowych reguł dla sztucznej inteligencji. Niestety, współczesne edytory zintegrowane z modelami językowymi używają różnych konwencji do definiowania tych reguł, a bez tego dokumentu jakość współpracy z AI jest niska. Do tego, precyzyjne definiowanie reguł wymaga doświadczenia i wiedzy o AI. 4 | 5 | ## Najmniejszy zestaw funkcjonalności: 6 | 7 | - Katalog reguł dla AI w formacie Markdown na 3 poziomach - warstwy aplikacji, stacku i bibliotek (np. Frontend, React, Zustand) 8 | - Udostępnienie konwencji do definiowania reguł dla AI w danym edytorze 9 | - Pobieranie reguł w formacie Markdown 10 | - Kopiowanie reguł do schowka 11 | - Generowanie reguł na podstawie "dep-files" (np. package.json czy requirements.txt) 12 | 13 | ## Co NIE wchodzi w zakres MVP: 14 | 15 | - Zaawansowane funkcje udostępniania reguł między użytkownikami 16 | - Eksport do formatów innych niż Markdown 17 | - Edycja reguł w aplikacji - użytkownik dostosowuje treść już na poziomie swojego projektu 18 | 19 | ## Kryteria sukcesu: 20 | 21 | - Użytkownik może wygenerować zestaw reguł dla najpopularniejszych technologii webowych korzystając z gotowego katalogu 22 | - Użytkownik może pobrać gotowy zestaw reguł lub skopiować do schowka 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy-mcp-on-merge.yml: -------------------------------------------------------------------------------- 1 | name: Deploy MCP Server (Production) 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'mcp-server/**' 9 | 10 | jobs: 11 | deploy-mcp-worker: 12 | name: Deploy Worker (mcp-server) 13 | runs-on: ubuntu-latest 14 | environment: production 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | # We need Node.js to generate rules and run wrangler/npm 20 | - name: Set up Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version-file: '.nvmrc' # Assuming .nvmrc is in the root 24 | cache: 'npm' 25 | cache-dependency-path: '**/package-lock.json' # Cache npm deps for root and worker 26 | 27 | - name: Install root dependencies 28 | run: npm ci 29 | 30 | - name: Generate preparedRules.json 31 | run: npm run generate-rules # Assuming direct execution works 32 | 33 | - name: Install worker dependencies 34 | run: cd mcp-server && npm ci 35 | 36 | - name: Deploy Worker (mcp-server) 37 | uses: cloudflare/wrangler-action@v3 38 | with: 39 | apiToken: ${{ secrets.CLOUDFLARE_WORKER_TOKEN }} 40 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 41 | workingDirectory: './mcp-server' 42 | command: deploy 43 | -------------------------------------------------------------------------------- /e2e/global.teardown.ts: -------------------------------------------------------------------------------- 1 | import { test as teardown } from '@playwright/test'; 2 | import { createClient } from '@supabase/supabase-js'; 3 | 4 | teardown('cleanup database', async () => { 5 | console.log('Cleaning up test database...'); 6 | 7 | if (!process.env.SUPABASE_URL!.includes('ueardaqpl')) { 8 | throw new Error('Cannot run teardown on non-test database!'); 9 | } 10 | 11 | const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_PUBLIC_KEY!); 12 | 13 | try { 14 | // Sign in with test user credentials to avoid issues with RLS 15 | const { error: signInError } = await supabase.auth.signInWithPassword({ 16 | email: process.env.E2E_USERNAME!, 17 | password: process.env.E2E_PASSWORD!, 18 | }); 19 | 20 | if (signInError) { 21 | console.error('Error signing in:', signInError); 22 | throw signInError; 23 | } 24 | 25 | const { error } = await supabase 26 | .from('collections') 27 | .delete() 28 | .eq('user_id', process.env.E2E_USERNAME_ID); 29 | 30 | if (error) { 31 | console.error('Error cleaning up collections:', error); 32 | throw error; 33 | } 34 | 35 | console.log('Successfully cleaned up collections for E2E test user'); 36 | } catch (error) { 37 | console.error('Failed to clean up database:', error); 38 | throw error; 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/ui/FormTextarea.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { transitions } from '../../styles/theme'; 3 | 4 | interface FormTextareaProps extends React.TextareaHTMLAttributes { 5 | label: string; 6 | error?: string; 7 | } 8 | 9 | const FormTextarea = React.forwardRef( 10 | ({ label, error, id, className = '', ...textareaProps }, ref) => { 11 | return ( 12 |
13 | 16 |