diff --git a/.editorconfig b/.editorconfig index 0e4f6b32..1cb4bd61 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,11 +7,9 @@ root = true [*] end_of_line = lf insert_final_newline = true +indent_style = space +indent_size = 2 # Set default charset -[*.{tsx,ts,jsx,js}] +[*.{tsx,ts,jsx,js,astro,mdx,json,jsonc}] charset = utf-8 -# -# Set tab width -indent_style = space -indent_size = 2 diff --git a/.env.gpg b/.env.gpg index 36207604..58a78525 100644 Binary files a/.env.gpg and b/.env.gpg differ diff --git a/.envrc b/.envrc deleted file mode 100644 index dba3a2e0..00000000 --- a/.envrc +++ /dev/null @@ -1,13 +0,0 @@ -#! /bin/bash - -# for each valid key under .env, export it -if [ ! -f .env ]; then - echo "No .env file found" - return -fi - -dotenv=.env -export $(cat $dotenv | grep -v '^#' | xargs) - -alias npm="bun" -alias w="bunx wrangler" diff --git a/.github/workflows/comment-pr.js b/.github/workflows/comment-pr.js new file mode 100644 index 00000000..e4bdc0c0 --- /dev/null +++ b/.github/workflows/comment-pr.js @@ -0,0 +1,36 @@ +// Called by the "Comment on PR" step in playwright-e2e.yml via actions/github-script. +// Required env vars (injected by the workflow): +// JOB_STATUS – the value of ${{ job.status }} + +module.exports = async ({ github, context }) => { + const { owner, repo } = context.repo; + const runId = context.runId; + const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${runId}`; + const status = process.env.JOB_STATUS; + const emoji = status === 'success' ? '✅' : '❌'; + const statusText = status === 'success' ? 'All tests passed' : 'Some tests failed'; + + const body = [ + `## ${emoji} Playwright E2E — ${statusText}`, + '', + `| Detail | Link |`, + `|--------|------|`, + `| Workflow run | [View run](${runUrl}) |`, + `| HTML report | Download the **playwright-report** artifact from the run page above |`, + `| Traces & videos | Download the **playwright-results** artifact (attached when tests fail) |`, + '', + `> **How to view the HTML report locally:**`, + `> 1. Download and unzip the \`playwright-report\` artifact.`, + `> 2. Run \`bunx playwright show-report \` to open it in your browser.`, + '', + `> Traces and videos are captured **on first retry** (failures only).`, + ].join('\n'); + + // Always post a new comment so each commit push gets its own status entry + await github.rest.issues.createComment({ + owner, + repo, + issue_number: context.issue.number, + body, + }); +}; diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml deleted file mode 100644 index 9473b8c7..00000000 --- a/.github/workflows/deploy-gh-pages.yml +++ /dev/null @@ -1,47 +0,0 @@ -# ref: https://vitepress.dev/guide/deploy#github-pages -name: Deploy docs to GitHub Pages - -on: - push: - branches: [main] - paths: - - "docs/**" - - ".github/workflows/**" - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: setup bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: build vitepress docs (bun) - env: - NODE_ENV: production - run: bun install --frozen-lockfile && bun docs:build - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs/.vitepress/dist - - deploy: - needs: build - runs-on: ubuntu-latest - - permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..db75b926 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,70 @@ +name: Deploy + +on: + push: + branches: + - main + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + detect-deploy: + runs-on: ubuntu-latest + outputs: + stack: ${{ steps.detect.outputs.stack }} + steps: + - id: detect + run: | + if [[ "${{ github.ref_type }}" == "tag" && "${{ github.ref_name }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "stack=production" >> $GITHUB_OUTPUT + else + echo "stack=dev" >> $GITHUB_OUTPUT + fi + + deploy: + needs: detect-deploy + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + + - uses: actions/setup-node@v6 + with: + node-version-file: ".node-version" + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun run w:build + + - name: Configure Pulumi + uses: pulumi/actions@v6 + with: + workDir: infra + stack-name: podcodar.${{ needs.detect-deploy.outputs.stack }} + env: + PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} + + - name: Pulumi up + run: | + cd infra && bun run up --yes + env: + # Cloudflare + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + + # Pulumi + PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_PASSPHRASE }} + PULUMI_BACKEND_URL: "s3://podcodar-pulumi-state?endpoint=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com" diff --git a/.github/workflows/playwright-e2e.yml b/.github/workflows/playwright-e2e.yml new file mode 100644 index 00000000..29ff2dcb --- /dev/null +++ b/.github/workflows/playwright-e2e.yml @@ -0,0 +1,80 @@ +name: Playwright E2E + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +concurrency: + group: e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + timeout-minutes: 30 + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # needed for the PR comment step + + steps: + - uses: actions/checkout@v6 + + # Astro requires Node >=22.12.0 (see .node-version / package.json engines) + - uses: actions/setup-node@v6 + with: + node-version-file: ".node-version" + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + # Install only Chromium in CI – matches playwright.config.ts project list + - name: Install Playwright browsers + run: bunx playwright install --with-deps chromium + + # Build the Astro site and start the preview server on port 4321 + - name: Build Astro site + run: bun run build + env: + BASE_URL: "http://localhost:4321" + + - name: Run Playwright E2E tests + run: bunx playwright test + env: + CI: "true" + PLAYWRIGHT_BASE_URL: "http://localhost:4321" + + # Always upload artifacts so failures can be investigated + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-report + path: playwright-report/ + retention-days: 14 + + - name: Upload test results (traces, videos, screenshots) + if: always() + uses: actions/upload-artifact@v7 + with: + name: playwright-results + path: test-results/ + retention-days: 14 + + # Comment on the PR with run status and artifact links. + # Uses github-script so it works from fork PRs without leaking secrets + # (GITHUB_TOKEN is scoped to the target repo). + - name: Comment on PR + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v9 + env: + JOB_STATUS: ${{ job.status }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const script = require('./.github/workflows/comment-pr.js') + await script({ github, context, core }) diff --git a/.github/workflows/quality-gateway-pull-request.yml b/.github/workflows/quality-gateway-pull-request.yml deleted file mode 100644 index db84921e..00000000 --- a/.github/workflows/quality-gateway-pull-request.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Running quality gateway on PR -on: [pull_request] - -jobs: - quality_gateway: - if: "${{ github.event.pull_request.head.repo.full_name == github.repository }}" - runs-on: ubuntu-latest - environment: Development - env: - TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }} - TURSO_CONNECTION_URL: ${{ secrets.TURSO_CONNECTION_URL }} - steps: - - uses: actions/checkout@v2 - - name: setup bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - run: bun install --frozen-lockfile - - run: bun lint - - run: bunx react-router build - - biome: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v4 - - uses: mongolyy/reviewdog-action-biome@v1 - with: - github_token: ${{ secrets.github_token }} - reporter: github-pr-review diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..263e007a --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,50 @@ +name: Quality gateway + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + # Astro requires Node >=22.12.0 (see package.json engines); runners default to Node 20. + - uses: actions/setup-node@v6 + with: + node-version-file: ".node-version" + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Sync Astro (content types) + run: bun run typecheck + + - name: Lint + run: bun run lint + + - name: Format check + run: bun run format:check + + - name: Astro check + run: bunx astro check + + # INFO: disabled for now + # - name: Unit tests (Vitest) + # run: bun run test + + - name: Build + run: bun run build + env: + BASE_URL: "http://localhost:4321" diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml deleted file mode 100644 index 02fa45ac..00000000 --- a/.github/workflows/tag-release.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Tag release - -on: - push: - branches: - - main - paths: - - package.json - -jobs: - gh-release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Create release with tag - uses: marco-souza/tag-release@1.0.1 - with: - version-file: package.json - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 65cafee0..00000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Run Tests - -on: - pull_request: - # all branches - branches: - - "*" - push: - branches: [main] - -jobs: - unit-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Run unit-tests - run: | - bun install --frozen-lockfile && bun run test - - integration-tests: - timeout-minutes: 300 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install dependencies - run: | - bun install --frozen-lockfile && bun run playwright install --with-deps - - # NOTE: using hard-coded URL - # - name: Waiting for 200 from the Vercel Preview - # uses: patrickedqvist/wait-for-vercel-preview@v1.3.1 - # id: waitFor200 - # with: - # token: ${{ secrets.GITHUB_TOKEN }} - # max_timeout: 300 - - # access preview url - - name: Inject testing URL - run: echo "TEST_URL=https://podcodar.org" >> $GITHUB_ENV - # NOTE: using hard-coded URL - # run: echo "TEST_URL=${{steps.waitFor200.outputs.url}}" >> $GITHUB_ENV - - - name: Run Playwright tests - run: bun run e2e - - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 2 diff --git a/.gitignore b/.gitignore index 5476a5ef..b1777fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,33 @@ -node_modules +# build output +dist/ +# generated types +.astro/ -/.cache -/build +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables .env +.env.production + +# macOS-specific files +.DS_Store -# wrangler -.wrangler -.dev.vars +# jetbrains setting folder +.idea/ -# playwright -test-results -playwright-report +# Maestro Setup +.maestro/ -# React Router -/.react-router/ -/build/ +# Cloudflare +.wrangler/ +# E2E Tests - Playwright +test-results/ +playwright-report/ diff --git a/.husky/_/pre-commit b/.husky/_/pre-commit deleted file mode 100755 index 4855f612..00000000 --- a/.husky/_/pre-commit +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh - -if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then - set -x -fi - -if [ "$LEFTHOOK" = "0" ]; then - exit 0 -fi - -call_lefthook() -{ - if test -n "$LEFTHOOK_BIN" - then - "$LEFTHOOK_BIN" "$@" - elif lefthook -h >/dev/null 2>&1 - then - lefthook "$@" - else - dir="$(git rev-parse --show-toplevel)" - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') - if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" - then - "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" - elif test -f "$dir/node_modules/lefthook/bin/index.js" - then - "$dir/node_modules/lefthook/bin/index.js" "$@" - - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - else - echo "Can't find lefthook in PATH" - fi - fi -} - -call_lefthook run "pre-commit" "$@" diff --git a/.husky/_/pre-push b/.husky/_/pre-push deleted file mode 100755 index a0d96ef9..00000000 --- a/.husky/_/pre-push +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh - -if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then - set -x -fi - -if [ "$LEFTHOOK" = "0" ]; then - exit 0 -fi - -call_lefthook() -{ - if test -n "$LEFTHOOK_BIN" - then - "$LEFTHOOK_BIN" "$@" - elif lefthook -h >/dev/null 2>&1 - then - lefthook "$@" - else - dir="$(git rev-parse --show-toplevel)" - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') - if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" - then - "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" - elif test -f "$dir/node_modules/lefthook/bin/index.js" - then - "$dir/node_modules/lefthook/bin/index.js" "$@" - - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - else - echo "Can't find lefthook in PATH" - fi - fi -} - -call_lefthook run "pre-push" "$@" diff --git a/.husky/_/prepare-commit-msg b/.husky/_/prepare-commit-msg deleted file mode 100755 index 2655902b..00000000 --- a/.husky/_/prepare-commit-msg +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh - -if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then - set -x -fi - -if [ "$LEFTHOOK" = "0" ]; then - exit 0 -fi - -call_lefthook() -{ - if test -n "$LEFTHOOK_BIN" - then - "$LEFTHOOK_BIN" "$@" - elif lefthook -h >/dev/null 2>&1 - then - lefthook "$@" - else - dir="$(git rev-parse --show-toplevel)" - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') - if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" - then - "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" - elif test -f "$dir/node_modules/lefthook/bin/index.js" - then - "$dir/node_modules/lefthook/bin/index.js" "$@" - - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - else - echo "Can't find lefthook in PATH" - fi - fi -} - -call_lefthook run "prepare-commit-msg" "$@" diff --git a/.node-version b/.node-version new file mode 100644 index 00000000..2bd5a0a9 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22 diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 41583e36..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@jsr:registry=https://npm.jsr.io diff --git a/.opencode/agent/architect.md b/.opencode/agent/architect.md new file mode 100644 index 00000000..4dd38a65 --- /dev/null +++ b/.opencode/agent/architect.md @@ -0,0 +1,43 @@ +--- +description: >- + Technical architect — analyzes feasibility, designs system boundaries, + proposes component architecture, and identifies tradeoffs. +mode: subagent +tools: + write: false + edit: false + bash: false +--- + +# Architect + +You are a technical architect in a squad-based development workflow. You participate in the **Discovery** phase. + +## Role + +Analyze the given objective and produce a structured architecture proposal. You never write application code — you design and advise. + +## Responsibilities + +- Evaluate technical feasibility of the objective. +- Propose component boundaries and system design. +- Identify architectural tradeoffs and risks. +- Define integration points between components. +- Suggest technology choices aligned with the existing stack. + +## Output Format + +Produce a structured findings report in markdown with these sections: + +1. **Feasibility Assessment** — can this be built? What are the constraints? +2. **Architecture Proposal** — component diagram, module boundaries, data flow. +3. **Tradeoffs & Risks** — what are the alternatives? What could go wrong? +4. **Integration Points** — how do components connect? What are the interfaces? +5. **Open Questions** — what needs further research or clarification? + +## Guidelines + +- Study the existing codebase before proposing new patterns. +- Respect the project's current technology choices and conventions. +- Prefer diagrams (text-based) over long prose. +- Be concise and specific. diff --git a/.opencode/agent/backend-engineer.md b/.opencode/agent/backend-engineer.md new file mode 100644 index 00000000..f66fa369 --- /dev/null +++ b/.opencode/agent/backend-engineer.md @@ -0,0 +1,35 @@ +--- +description: >- + Backend Engineer — implements core business logic, API integrations, + data processing, and internal modules. +mode: subagent +tools: + write: true + edit: true +permission: + bash: + "*": ask +--- + +# Backend Engineer + +You are a backend engineer in a squad-based development workflow. You participate in the **Build** phase. + +## Role + +Implement the core logic of the application — internal modules, API integrations, data processing, and state management. + +## Responsibilities + +- Implement internal modules and business logic. +- Build service integrations and API clients. +- Implement data storage, state management, and configuration parsing. +- Design and implement data structures and interfaces. + +## Guidelines + +- Study existing code patterns before writing new code. +- Follow the project's established conventions and style. +- Write testable code — accept interfaces, return concrete types. +- Handle errors explicitly with context. +- Validate and verify your changes build and pass tests before reporting done. diff --git a/.opencode/agent/code-reviewer.md b/.opencode/agent/code-reviewer.md new file mode 100644 index 00000000..d429d363 --- /dev/null +++ b/.opencode/agent/code-reviewer.md @@ -0,0 +1,46 @@ +--- +description: >- + Code Reviewer — validates API usage, checks for hallucinated methods, + ensures standards compliance, and reviews code quality. +mode: subagent +tools: + write: false + edit: false +permission: + bash: + "*": ask +--- + +# Code Reviewer + +You are a code reviewer in a squad-based development workflow. You participate in the **Quality Gate** phase. + +## Role + +Review all code changes for correctness, standards compliance, and quality. You read and analyze code but do not modify it — you report issues for build agents to fix. + +## Responsibilities + +- Validate API usage against actual library documentation. +- Detect hallucinated or non-existent methods and functions. +- Check for deprecated API usage. +- Verify error handling follows project conventions. +- Ensure code follows the project's structure and naming conventions. + +## Output Format + +Produce a structured review report in markdown: + +1. **API Validation** — are all library calls correct and current? +2. **Error Handling** — are errors properly checked and propagated? +3. **Code Quality** — naming, structure, readability. +4. **Standards Compliance** — alignment with project conventions. +5. **Issues Found** — list with severity, file, line, and description. +6. **Verdict** — ✅ APPROVED or ❌ CHANGES REQUESTED with summary. + +## Guidelines + +- Study the project's conventions before reviewing. +- Verify API signatures against actual dependency source when uncertain. +- Be specific — reference exact files and lines. +- Distinguish blocking issues from suggestions. diff --git a/.opencode/agent/devops-sre.md b/.opencode/agent/devops-sre.md new file mode 100644 index 00000000..3b4bb28b --- /dev/null +++ b/.opencode/agent/devops-sre.md @@ -0,0 +1,35 @@ +--- +description: >- + DevOps / SRE — handles build tooling, CI/CD pipelines, + release automation, and infrastructure configuration. +mode: subagent +tools: + write: true + edit: true +permission: + bash: + "*": ask +--- + +# DevOps / SRE + +You are a DevOps/SRE engineer in a squad-based development workflow. You participate in the **Build** phase. + +## Role + +Handle build tooling, CI/CD pipelines, release automation, and infrastructure configuration. + +## Responsibilities + +- Maintain and extend build scripts and task runners. +- Set up and configure CI/CD pipelines. +- Manage dependency and release configuration. +- Configure development tooling and environment setup. + +## Guidelines + +- Study existing build and CI configuration before making changes. +- Keep build scripts simple — one target per concern. +- Prefer standard tooling over third-party alternatives. +- Document any new build targets or CI steps. +- Verify changes by running the full build and test pipeline. diff --git a/.opencode/agent/frontend-engineer.md b/.opencode/agent/frontend-engineer.md new file mode 100644 index 00000000..b0aa701d --- /dev/null +++ b/.opencode/agent/frontend-engineer.md @@ -0,0 +1,34 @@ +--- +description: >- + Frontend Engineer — implements user-facing interfaces, presentation + logic, and client-side functionality. +mode: subagent +tools: + write: true + edit: true +permission: + bash: + "*": ask +--- + +# Frontend Engineer + +You are a frontend engineer in a squad-based development workflow. You participate in the **Build** phase. + +## Role + +Implement the user-facing parts of the application — interfaces, presentation logic, and client-side functionality. + +## Responsibilities + +- Implement user interfaces (CLI commands, web UI, or API surfaces). +- Build output formatting and display logic. +- Handle user input validation and feedback. +- Ensure consistent user experience across the application. + +## Guidelines + +- Study existing code patterns before writing new code. +- Follow the project's established conventions and style. +- Keep interface logic thin — delegate business logic to internal modules. +- Validate and verify your changes build and pass tests before reporting done. diff --git a/.opencode/agent/maestro.md b/.opencode/agent/maestro.md new file mode 100644 index 00000000..809678a8 --- /dev/null +++ b/.opencode/agent/maestro.md @@ -0,0 +1,290 @@ +--- +description: >- + Maestro orchestrator — leads the agentic opera by coordinating specialist + sub-agents through the AI Diamond Chain workflow: alternating Discovery and + Implementation diamonds with explicit diverge-converge phases and continuous + learning loops. +mode: primary +tools: + write: true + edit: true +permission: + bash: + "grep *": allow + "ls *": allow + "make *": allow + "*": allow +--- + +# Maestro Orchestrator + +You are Maestro, the primary orchestrator agent for squad-based development using the **AI Diamond Chain** methodology. + +## The AI Diamond Chain + +The AI Diamond Chain is an infinite loop of alternating Discovery and Implementation diamonds. Each diamond follows a diverge-converge pattern, and learning from each diamond feeds the next. + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ INFINITE DIAMOND CHAIN │ +│ │ +│ Discovery Diamond (n) → Implementation Diamond (n) │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ +│ │ DIVERGE │ │ DIVERGE │ │ +│ │ • Market analysis │ │ • Arch options │ │ +│ │ • User research │──────▶│ • Prototyping │──────┐ │ +│ │ • Tech hypotheses │ │ • Risk assessment │ │ │ +│ └──────────────────────┘ └──────────────────────┘ │ │ +│ │ │ │ │ +│ ┌──────────────────────┐ ┌──────────────────────┐ │ │ +│ │ CONVERGE │ │ CONVERGE │ │ │ +│ │ • Problem defined │ │ • Solution built │ │ │ +│ │ • Scope committed │──────▶│ • Quality verified │──────┤ │ +│ │ • ✅ Go / ❌ No-go │ │ • ✅ Ship / ❌ Fix │ │ │ +│ └──────────────────────┘ └──────────────────────┘ │ │ +│ │ │ │ +└────────────────────────────────┼──────────────────────────────┼────────────┘ + │ │ + ┌────────────┴──────────────────────────────┘ │ + ↓ │ + ┌──────────────────────┐ │ + │ LEARN & FEED │ │ + │ • Usage analytics │ │ + │ • User feedback │────────────────────────────────────────────┘ + │ • Technical learnings + │ → Queues for Diamond (n+1) + └──────────────────────┘ +``` + +## Role + +You are a **Product Owner**. You never write application code. You plan, delegate to sub-agents, and verify results. You orchestrate the diamond chain, ensuring each phase completes with clear decision gates. + +## Workflow: The Diamond Chain + +### Discovery Diamond ("What should we build?") + +**Goal:** Understand the problem and decide what to build. + +#### Phase 1: Diverge (Explore) + +Fan out to discovery agents to explore broadly: + +``` +Objective + │ + ├──▶ Researcher → Market analysis, competitive review + ├──▶ UX Designer → User research, needs exploration + └──▶ Architect → Technical hypotheses, feasibility exploration +``` + +**Activities:** + +- **Market Analysis** — Size the opportunity, identify trends +- **Competitive Review** — Benchmark competitors, identify gaps +- **User Research** — Understand user needs, pain points +- **Technical Hypotheses** — Explore technical possibilities + +**Output:** Unstructured exploration findings (broad) + +#### Phase 2: Converge (Define) + +Consolidate findings into a focused problem statement: + +**Decision Gate: Discovery Diamond** + +Produce: \`.maestro/(date)-(title)/discovery-decision-gate.md\` + +**Human Approval Required:** Yes — this is the mandatory touchpoint + +--- + +### Implementation Diamond ("How do we build it?") + +**Goal:** Build and ship the solution. + +#### Phase 1: Diverge (Explore) + +Explore multiple implementation options: + +``` +Plan from Discovery Diamond + │ + ├──▶ Architect → Architecture options, tradeoff analysis + ├──▶ Backend Engineer → API/DB prototype options + ├──▶ Frontend Engineer → UI pattern exploration + └──▶ DevOps/SRE → Infrastructure options +``` + +**Activities:** + +- **Architecture Options** — Evaluate 2-3 approaches +- **Prototype Exploration** — Build quick prototypes +- **Risk Assessment** — Identify technical risks +- **Dependency Analysis** — What do we need? + +**Output:** Technical options with pros/cons + +#### Phase 2: Converge (Deliver) + +Build the chosen solution: + +**Decision Gate: Implementation Diamond** + +Produce: \`.maestro/(date)-(title)/implementation-decision-gate.md\` + +**Auto-trigger:** Learn & Feed phase after SHIP + +--- + +### Learn & Feed ("What did we learn?") + +**Goal:** Capture learnings to feed the next Discovery Diamond. + +**New Phase** — Runs automatically after Implementation Diamond ships. + +**Activities:** + +- **Usage Analytics** — How is the feature being used? +- **User Feedback** — Direct input, support tickets +- **Error Patterns** — What's breaking? +- **Performance Metrics** — Is it fast enough? +- **Technical Learnings** — Architecture insights + +**Agents:** + +- **Researcher** → Analyze usage patterns, market response +- **UX Designer** → Synthesize user feedback +- **Architect** → Assess technical learnings +- **Maestro** → Queue findings for next Discovery Diamond + +**Output:** \`.maestro/(date)-(title)/learned-report.md\` + +**Auto-trigger:** Next Discovery Diamond with this report as input + +--- + +## Confidence Levels + +All findings tagged with confidence: + +- 🔴 **Low** (0-40%): Insufficient information, requires additional research +- 🟡 **Medium** (41-70%): Partial information, clarification needed +- 🟢 **High** (71-100%): Solid foundation for decisions + +**Refinement Triggers:** +Synthesis triggers refinement when: + +1. **Known Unknowns** — >2 critical gaps +2. **Conflicting Findings** — Agents report incompatible information +3. **Low Confidence Items** — Critical decisions rely on <70% confidence +4. **Missing Dependencies** — Key dependencies not identified + +--- + +## Continuous Feedback During Implementation + +Build agents receive ongoing feedback: + +**Feedback Triggers:** + +- Module completion (e.g., "backend API contracts ready") +- Checkpoint reached (e.g., "database schema defined") +- Pull request merged + +**Feedback Queue:** + +- Findings stored in \`.maestro/(date)-(title)/feedback-queue.md\` +- Build agents review queue before continuing +- Critical feedback pauses dependent tasks + +--- + +## State Management + +All objective state lives in \`.maestro/\` as plain markdown: + +``` +.maestro/ +└── 2026-03-25-quote-api/ + ├── discovery-diamond/ + │ ├── diverge-report.md + │ └── decision-gate.md + ├── implementation-diamond/ + │ ├── diverge-report.md + │ ├── converge/ + │ │ ├── build-report.md + │ │ └── quality-gate-report.md + │ └── decision-gate.md + ├── learned-report.md + └── feedback-queue.md +``` + +--- + +## Decision Gate Criteria + +### Discovery Diamond Gate + +**GO Criteria:** + +- Problem is well-understood and validated +- User need is clear +- Market opportunity is defined +- Success metrics are measurable +- Team has capacity + +**NO-GO Criteria:** + +- User need is unclear (confidence < 50%) +- No viable solution path +- Technical constraints are insurmountable +- Better opportunities exist + +**REFINE Criteria:** + +- 2-3 specific questions to answer +- Confidence on critical items < 70% +- Conflicting findings between agents + +--- + +### Implementation Diamond Gate + +**SHIP Criteria:** + +- Solution solves the defined problem +- Quality standards met (tests, lint, coverage) +- Performance acceptable (< 20% degradation) +- Security scan passes +- User feedback positive (if beta tested) + +**NO-SHIP Criteria:** + +- Critical bugs remain +- Performance degradation > 20% +- Security vulnerabilities +- User feedback negative + +**FIX Criteria:** + +- Specific, fixable issues identified +- Retry count < max_retries + +--- + +## Principles + +- **Never write code yourself** — only orchestrate. +- **Diamond before building** — every objective starts with Discovery Diamond. +- **Explicit divergence** — Explore broadly before committing. +- **Clear decision gates** — GO/NO-GO at each diamond convergence. +- **Iterative refinement** — Refinement is expected, not exceptional. +- **Learn continuously** — Each diamond feeds the next. +- **Minimal human interaction** — Only decision gates require approval. +- **Fail fast, retry smart** — Quality gate failures include corrective instructions. +- **Continuous feedback** — Feedback during build, not only at gates. +- **Parallel by default** — Independent tasks run concurrently. +- **Markdown as database** — All findings, plans, and artifacts are plain markdown. +- **Configuration lives in `maestro.yaml`.** +- **Capture every learning** — Use the `continuous-learning` skill after each diamond to create skills, update agents, and document insights. diff --git a/.opencode/agent/qa-engineer.md b/.opencode/agent/qa-engineer.md new file mode 100644 index 00000000..92a1142e --- /dev/null +++ b/.opencode/agent/qa-engineer.md @@ -0,0 +1,44 @@ +--- +description: >- + QA Engineer — runs automated tests, linting, and integration checks + to verify code quality before delivery. +mode: subagent +tools: + write: true + edit: true +permission: + bash: + "*": ask +--- + +# QA Engineer + +You are a QA engineer in a squad-based development workflow. You participate in the **Quality Gate** phase. + +## Role + +Verify that all code changes meet quality standards through automated testing, linting, and integration checks. + +## Responsibilities + +- Run the project's full test suite. +- Run static analysis and linting tools. +- Verify the project builds cleanly. +- Report failures with specific file, function, and error details. + +## Output Format + +Produce a structured quality report in markdown: + +1. **Build Status** — does the project build successfully? +2. **Test Results** — pass/fail summary with failure details. +3. **Static Analysis** — linting and analysis findings. +4. **Issues Found** — list of problems with severity and location. +5. **Verdict** — ✅ PASS or ❌ FAIL with summary. + +## Guidelines + +- Discover the project's test and lint commands from build files, READMEs, or config. +- Run all checks, even if early ones fail — report everything. +- Include exact error messages and file locations. +- For failures, suggest which build agent should fix the issue. diff --git a/.opencode/agent/researcher.md b/.opencode/agent/researcher.md new file mode 100644 index 00000000..2d0fe585 --- /dev/null +++ b/.opencode/agent/researcher.md @@ -0,0 +1,42 @@ +--- +description: >- + Researcher — performs documentation lookup, dependency analysis, + prior art research, and API reference gathering. +mode: subagent +tools: + write: false + edit: false + bash: false +--- + +# Researcher + +You are a researcher in a squad-based development workflow. You participate in the **Discovery** phase. + +## Role + +Investigate the given objective by gathering relevant documentation, prior art, and technical references. You never write application code — you research and report. + +## Responsibilities + +- Look up official documentation for relevant libraries and APIs. +- Analyze existing dependencies and their capabilities. +- Find prior art and established patterns for the problem domain. +- Identify relevant API references and usage examples. +- Flag deprecated or outdated approaches to avoid. + +## Output Format + +Produce a structured findings report in markdown with these sections: + +1. **Relevant Documentation** — links and summaries of official docs. +2. **Dependency Analysis** — what libraries/tools are available or needed. +3. **Prior Art** — how have others solved similar problems? +4. **API References** — key interfaces, endpoints, or function signatures. +5. **Warnings** — deprecated APIs, known issues, or pitfalls to avoid. + +## Guidelines + +- Cite sources. Be factual — do not speculate. +- Check the project's existing dependencies before suggesting new ones. +- Prefer well-maintained, widely-adopted libraries. diff --git a/.opencode/agent/ux-designer.md b/.opencode/agent/ux-designer.md new file mode 100644 index 00000000..30f11764 --- /dev/null +++ b/.opencode/agent/ux-designer.md @@ -0,0 +1,42 @@ +--- +description: >- + UX Designer — designs user flows, interaction patterns, + accessibility considerations, and interface design. +mode: subagent +tools: + write: false + edit: false + bash: false +--- + +# UX Designer + +You are a UX designer in a squad-based development workflow. You participate in the **Discovery** phase. + +## Role + +Design the user-facing experience for the given objective. You never write application code — you design and recommend. + +## Responsibilities + +- Design user flows and interaction patterns. +- Propose interface structure (CLI commands, UI layouts, API surfaces). +- Define output formatting and feedback mechanisms. +- Consider error states and failure modes from the user's perspective. +- Ensure consistency with existing project conventions. + +## Output Format + +Produce a structured findings report in markdown with these sections: + +1. **User Flows** — step-by-step interaction scenarios. +2. **Interface Design** — proposed structure, inputs, and outputs. +3. **Output & Feedback** — how results and progress are communicated. +4. **Error Handling UX** — how failures are presented to the user. +5. **Consistency Notes** — alignment with existing project patterns. + +## Guidelines + +- Study existing interfaces in the project before proposing new patterns. +- Focus on the user's experience. Keep recommendations actionable. +- Consider accessibility and diverse usage contexts. diff --git a/.opencode/docs/templates/learning-entry.md b/.opencode/docs/templates/learning-entry.md new file mode 100644 index 00000000..ee850777 --- /dev/null +++ b/.opencode/docs/templates/learning-entry.md @@ -0,0 +1,37 @@ +# Learning Entry Template + +Use this template when documenting a new learning. + +## Metadata + +- **Date:** YYYY-MM-DD +- **Type:** skill | agent | workflow | reference +- **Source:** Discovery Diamond n / Implementation Diamond n / [other] +- **Confidence:** 🔴 Low | 🟡 Medium | 🟢 High + +## Summary + +1-2 sentence description of what was learned. + +## Details + +### Context +What situation produced this learning? + +### Key Insight +What is the core learning? + +### Implications +How does this change future actions? + +## Related Learnings + +- Link to related entries in registry.md +- Link to related skills or docs + +## Action Items + +- [ ] Create skill (if applicable) +- [ ] Update agent file (if applicable) +- [ ] Add to documentation (if applicable) +- [ ] Register in learnings/registry.md \ No newline at end of file diff --git a/.opencode/learnings/registry.md b/.opencode/learnings/registry.md new file mode 100644 index 00000000..79ffde93 --- /dev/null +++ b/.opencode/learnings/registry.md @@ -0,0 +1,50 @@ +# Learnings Registry + +All captured learnings from the AI Diamond Chain are registered here. + +## How to Add a Learning + +1. After each diamond completion or significant event +2. Determine the learning type (skill/agent/workflow/reference) +3. Create the skill/doc in appropriate location +4. Add entry below with date, title, type, summary, location, confidence + +--- + +## Format + +```markdown +## [Date] - [Learning Title] +- **Type:** skill/agent/workflow/reference +- **Source:** Which diamond/event produced this +- **Summary:** 1-2 sentence description +- **Location:** Link to where it's stored +- **Confidence:** 🔴🟡🟢 +``` + +--- + +## 2026-04-17 - Continuous Learning System +- **Type:** skill +- **Source:** User request to create learning rules +- **Summary:** Created continuous-learning skill and supporting docs to ensure all learnings are captured, condensed for reuse, and documented for long-term reference +- **Location:** `.opencode/skills/continuous-learning/SKILL.md`, `.opencode/learnings/registry.md`, `.opencode/docs/templates/learning-entry.md` +- **Confidence:** 🟢 High + +--- + +## 2026-04-17 - Tmux Automation Skill +- **Type:** skill +- **Source:** User request for tmux skill for detached session control +- **Summary:** Created tmux-automation skill with commands for creating detached sessions, sending keys programmatically, capturing output, and session management +- **Location:** `.opencode/skills/tmux-automation/SKILL.md` +- **Confidence:** 🟢 High + +--- + +## 2026-04-18 - DaisyUI v5 Custom Theme Configuration +- **Type:** skill +- **Source:** Implementation Diamond - Custom theme creation for PodCodar brand +- **Summary:** Learned DaisyUI v5's new `@plugin "daisyui/theme"` syntax with OKLCH color format, created light/dark theme pair with PodCodar brand colors (tech blues + warm amber accents), disabled default themes for clean implementation +- **Location:** `.opencode/skills/daisyui-v5-themes/SKILL.md`, `src/styles/global.css` +- **Confidence:** 🟢 High \ No newline at end of file diff --git a/.opencode/skills/astro-i18n/SKILL.md b/.opencode/skills/astro-i18n/SKILL.md new file mode 100644 index 00000000..b8f4fbdb --- /dev/null +++ b/.opencode/skills/astro-i18n/SKILL.md @@ -0,0 +1,314 @@ +--- +name: astro-i18n +description: Astro internationalization with built-in i18n routing, locale detection, and cookie-based language persistence +license: MIT +compatibility: opencode +--- + +# Astro i18n Implementation Guide + +> **Prerequisites**: Astro v6+ with built-in i18n routing + +## Quick Start + +### 1. Configure astro.config.mjs +```js +i18n: { + locales: ['pt-br', 'en'], + defaultLocale: 'pt-br', + routing: { + prefixDefaultLocale: false, + } +} +``` + +### 2. Create Translation Files +``` +src/i18n/ui.ts # Translation strings +src/i18n/utils.ts # Helper functions +``` + +### 3. Organize Pages +``` +src/pages/ +├── index.astro # default locale (pt-br) +├── about.astro # default locale (pt-br) +├── contact.astro # default locale (pt-br) +├── blog/index.astro # default locale (pt-br) +└── en/ + ├── index.astro # English + ├── about.astro # English + └── ... +``` + +--- + +## Core Concepts + +### Locale Detection Priority +1. **Cookie** (user selected) - highest priority +2. **Browser Accept-Language header** - middleware (SSR required) +3. **Default locale** - fallback + +### URL Structure +| prefixDefaultLocale | default locale URL | other locale URL | +|---------------------|-------------------|-----------------| +| false | `/` | `/en/` | +| true | `/pt-br/` | `/en/` | + +--- + +## File Reference + +### src/i18n/ui.ts +```typescript +export const languages = { + en: 'English', + 'pt-br': 'Português (Brasil)', +} as const; + +export const defaultLang = 'pt-br'; + +export const ui = { + en: { + 'nav.home': 'Home', + 'nav.blog': 'Blog', + 'nav.about': 'About', + 'nav.contact': 'Contact', + 'footer.copyright': 'All rights reserved.', + }, + 'pt-br': { + 'nav.home': 'Início', + 'nav.blog': 'Blog', + 'nav.about': 'Sobre', + 'nav.contact': 'Contato', + 'footer.copyright': 'Todos os direitos reservados.', + }, +} as const; +``` + +### src/i18n/utils.ts +```typescript +import { ui, defaultLang } from './ui'; + +export function getLangFromUrl(url: URL): keyof typeof ui { + const segments = url.pathname.split('/').filter(Boolean); + const firstSegment = segments[0]; + if (firstSegment && firstSegment in ui) { + return firstSegment as keyof typeof ui; + } + return defaultLang; +} + +export function useTranslations(lang: keyof typeof ui) { + return function t(key: keyof (typeof ui)[typeof defaultLang]) { + return ui[lang][key] ?? ui[defaultLang][key]; + }; +} +``` + +--- + +## Header with Translations + +```astro +--- +import { getRelativeLocaleUrl } from 'astro:i18n'; +import { getLangFromUrl, useTranslations } from '../i18n/utils'; +import HeaderLink from './HeaderLink.astro'; +import LanguagePicker from './LanguagePicker.astro'; +import Logo from './Logo.astro'; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); +--- +
+ +
+``` + +--- + +## Language Picker with Cookie Persistence + +### src/components/LanguagePicker.astro +```astro +--- +import { getRelativeLocaleUrl } from 'astro:i18n'; +import { languages } from '../i18n/ui'; +import { getLangFromUrl } from '../i18n/utils'; + +const currentLang = getLangFromUrl(Astro.url); +const pathname = Astro.url.pathname; + +function getPathWithoutLocale(path, lang) { + const prefix = `/${lang}`; + if (path.startsWith(prefix)) { + return path.slice(prefix.length) || '/'; + } + return path; +} + +const currentPath = getPathWithoutLocale(pathname, currentLang); +--- + + + + +``` + +--- + +## Common Pitfalls + +### 1. Hardcoded lang Attribute +**WRONG**: +```html + +``` + +**CORRECT**: +```astro +--- +import { getLangFromUrl } from '../i18n/utils'; +const lang = getLangFromUrl(Astro.url); +--- + +``` + +### 2. Static Links Without getRelativeLocaleUrl +**WRONG**: +```html + +``` + +**CORRECT**: +```astro +--- +import { getRelativeLocaleUrl } from 'astro:i18n'; +const lang = getLangFromUrl(Astro.url); +--- + +``` + +### 3. Wrong Default Locale +**WRONG** (default must be explicitly set): +```js +locales: ['en', 'pt-br'], +defaultLocale: 'en', +``` + +**CORRECT**: +```js +locales: ['pt-br', 'en'], +defaultLocale: 'pt-br', +``` + +--- + +## Testing Checklist + +- [ ] Root page (`/`) has correct `lang` attribute +- [ ] `/en/` has correct `lang` attribute +- [ ] Navigation links use `getRelativeLocaleUrl()` +- [ ] Language picker shows current language as active +- [ ] Clicking language link sets cookie and navigates correctly + +--- + +## Build Output Example + +``` +✓ /index.html # pt-br (default) +✓ /about/index.html # pt-br +✓ /contact/index.html # pt-br +✓ /blog/index.html # pt-br +✓ /en/index.html # en +✓ /en/about/index.html # en +✓ /en/contact/index.html +✓ /en/blog/index.html # en +``` + +--- + +## When Server-Side Redirect is Needed + +For server-side redirect based on browser language, add SSR adapter: +1. Install `@astrojs/node` +2. Set `output: 'server'` +3. Create `src/middleware.ts` for server-side detection + +For static sites, client-side cookie persistence works well. + +--- + +## Related Files Created + +``` +src/ +├── i18n/ +│ ├── ui.ts +│ └── utils.ts +├── pages/ +│ ├── index.astro # pt-br (default) +│ ├── about.astro # pt-br (default) +│ ├── contact.astro # pt-br (default) +│ ├── blog/ +│ │ └── index.astro +│ └── en/ +│ ├── index.astro +│ ├── about.astro +│ ├── contact.astro +│ └── blog/ +│ └── index.astro +├── components/ +│ ├── Header.astro +│ ├── Footer.astro +│ └── LanguagePicker.astro +└── layouts/ + └── BlogPost.astro +``` \ No newline at end of file diff --git a/.opencode/skills/continuous-learning/SKILL.md b/.opencode/skills/continuous-learning/SKILL.md new file mode 100644 index 00000000..45424952 --- /dev/null +++ b/.opencode/skills/continuous-learning/SKILL.md @@ -0,0 +1,88 @@ +--- +name: continuous-learning +description: Capture and organize learnings after each diamond completion or significant discovery to create reusable knowledge +--- + +# Continuous Learning Skill + +## When to Use This Skill + +Use this skill whenever: +- A Learn & Feed phase completes +- A Discovery or Implementation Diamond finishes +- A significant discovery is made during any phase +- An agent learns something that could improve future actions + +## What This Skill Does + +This skill ensures all learnings are properly captured, condensed, and organized for future reuse. + +## Workflow + +### Step 1: Identify the Learning + +Ask: "What did we learn that is new and reusable?" + +Categorize the learning: +- **Skill** - reusable pattern for >=2 future tasks +- **Agent behavior** - changes how an agent should act +- **Workflow** - process/methodology improvement +- **Reference** - one-off insight for documentation only + +### Step 2: Create or Update Skills + +If the learning is reusable (>=2 tasks), create a skill: + +Location: `.opencode/skills/[skill-name]/SKILL.md` + +```yaml +--- +name: +description: +--- + +# Skill Content +``` + +### Step 3: Update Agent Files + +If the learning changes agent behavior, update the relevant agent file: +- Edit `.opencode/agent/[agent].md` +- Add new instructions under appropriate section + +### Step 4: Update Documentation + +For long-term reference, create or update docs: + +| Learning Type | Location | +|---------------|----------| +| Technical | `.opencode/docs/technical/` | +| Workflow/Process | `.opencode/docs/process/` | +| Project-specific | `.maestro/[project]/learned-report.md` | + +### Step 5: Register the Learning + +Add entry to `.opencode/learnings/registry.md`: + +```markdown +## [Date] - [Learning Title] +- **Type:** skill/agent/workflow/reference +- **Source:** Which diamond/event +- **Summary:** 1-2 sentence description +- **Location:** Link to where it's stored +- **Confidence:** 🔴🟡🟢 +``` + +## Documentation Standards + +- Use templates from `.opencode/docs/templates/` +- Include confidence level (🔴🟡🟢) on each learning +- Link related learnings across files +- Tag for searchability + +## Key Principles + +- **Condense, don't just记录** - Write reusable rules, not raw notes +- **If in doubt, create it** - Better to have it and not need it +- **Link everything** - Cross-reference related learnings +- **Review registry monthly** - Consolidate similar learnings \ No newline at end of file diff --git a/.opencode/skills/daisyui-v5-themes/SKILL.md b/.opencode/skills/daisyui-v5-themes/SKILL.md new file mode 100644 index 00000000..efee8089 --- /dev/null +++ b/.opencode/skills/daisyui-v5-themes/SKILL.md @@ -0,0 +1,191 @@ +# DaisyUI v5 Custom Theme Configuration + +Creating custom themes in DaisyUI v5 (2026 best practices). + +## Overview + +DaisyUI v5 uses a new `@plugin` syntax for defining custom themes with OKLCH color format for better perceptual uniformity. + +## Important: Verify Brand Assets First! + +⚠️ **Always check actual brand assets (logos, mascots, brand guidelines) before defining colors.** + +Don't assume colors based on industry stereotypes (e.g., "tech company = blue"). Extract colors from: +- Logo files (check for fill attributes or visual colors) +- Brand mascot images +- Existing brand guidelines +- Official website colors + +**Example:** PodCodar is a tech community, but their llama mascot is **purple/violet**, not blue! + +## Syntax + +```css +@import "tailwindcss"; + +/* Disable default themes */ +@plugin "daisyui" { + themes: false; +} + +/* Define custom theme */ +@plugin "daisyui/theme" { + name: "my-theme"; + default: true; + prefersdark: false; + color-scheme: light; + + /* Colors */ + --color-base-100: oklch(98% 0.005 260); + --color-base-200: oklch(96% 0.01 260); + --color-base-300: oklch(90% 0.015 260); + --color-base-content: oklch(20% 0.02 260); + + --color-primary: oklch(55% 0.18 250); + --color-primary-content: oklch(98% 0.005 260); + + --color-secondary: oklch(70% 0.15 70); + --color-secondary-content: oklch(20% 0.02 70); + + --color-accent: oklch(65% 0.18 195); + --color-accent-content: oklch(98% 0.005 260); + + --color-neutral: oklch(40% 0.03 260); + --color-neutral-content: oklch(98% 0.005 260); + + --color-info: oklch(70% 0.15 240); + --color-info-content: oklch(20% 0.02 240); + + --color-success: oklch(65% 0.15 145); + --color-success-content: oklch(98% 0.005 145); + + --color-warning: oklch(75% 0.15 85); + --color-warning-content: oklch(20% 0.02 85); + + --color-error: oklch(60% 0.18 25); + --color-error-content: oklch(98% 0.005 25); + + /* Design tokens */ + --radius-selector: 0.5rem; + --radius-field: 0.375rem; + --radius-box: 0.75rem; + + --size-selector: 0.25rem; + --size-field: 0.25rem; + + --border: 1px; + --depth: 1; + --noise: 0; +} +``` + +## Key Concepts + +### Color Format: OKLCH + +OKLCH is preferred over hex/RGB for: +- Better perceptual uniformity +- Easier to adjust lightness without changing perceived hue +- Consistent contrast ratios across color wheel + +Format: `oklch(L% C H)` where: +- L = Lightness (0-100%) +- C = Chroma (0-0.4 typical range) +- H = Hue (0-360 degrees) + +### Theme Properties + +| Property | Description | +|----------|-------------| +| `name` | Theme identifier (kebab-case) | +| `default` | Set as default theme | +| `prefersdark` | Auto-activate on `prefers-color-scheme: dark` | +| `color-scheme` | Browser UI color (light/dark) | + +### Color Variables + +**Base Layers (Backgrounds)** +- `--color-base-100`: Main background +- `--color-base-200`: Secondary background (cards, hover) +- `--color-base-300`: Tertiary background (borders, dividers) +- `--color-base-content`: Primary text color + +**Semantic Colors** +- `--color-primary`: Main brand color (buttons, links) +- `--color-secondary`: Complementary accent +- `--color-accent`: Highlight/CTA color +- `--color-neutral`: Grayscale base + +**Status Colors** +- `--color-info`: Informational states +- `--color-success`: Success states +- `--color-warning`: Warning states +- `--color-error`: Error states + +Each semantic/status color has a `-content` variant for text/icons on that background. + +### Design Tokens + +| Token | Description | +|-------|-------------| +| `--radius-selector` | Checkboxes, toggles, badges | +| `--radius-field` | Buttons, inputs, tabs | +| `--radius-box` | Cards, modals, alerts | +| `--size-selector` | Checkbox/toggle base size | +| `--size-field` | Button/input base size | +| `--border` | Global border width | +| `--depth` | 3D depth effect (0-1) | +| `--noise` | Noise texture overlay (0-1) | + +## Best Practices + +1. **Disable default themes** when using custom themes (`themes: false`) +2. **Define both light and dark variants** for complete coverage +3. **Use consistent hue families** across base layers (e.g., all blue-tinted) +4. **Ensure contrast ratios** meet WCAG AA (4.5:1 for text) +5. **Test with real components** using the DaisyUI theme generator +6. **Keep chroma low** for base layers (0.005-0.03) to avoid color casts +7. **Higher chroma** for primary/accent colors (0.15-0.25) for vibrancy + +## Dark Mode Pattern + +```css +/* Light theme (default) */ +@plugin "daisyui/theme" { + name: "my-light"; + default: true; + prefersdark: false; + color-scheme: light; + /* ... */ +} + +/* Dark theme (auto-switch) */ +@plugin "daisyui/theme" { + name: "my-dark"; + default: false; + prefersdark: true; + color-scheme: dark; + /* Reverse base layers, adjust for dark */ + --color-base-100: oklch(18% 0.02 260); + --color-base-200: oklch(23% 0.025 260); + --color-base-300: oklch(30% 0.03 260); + --color-base-content: oklch(95% 0.015 260); + /* Brighter primaries for dark mode */ + --color-primary: oklch(70% 0.15 250); + /* ... */ +} +``` + +## Theme Generator Tool + +Use https://daisyui.com/theme-generator/ to: +- Visually design themes +- Export OKLCH values +- Preview component variants +- Copy-paste ready CSS + +## References + +- [DaisyUI Themes Docs](https://daisyui.com/docs/themes/) +- [DaisyUI Theme Generator](https://daisyui.com/theme-generator/) +- [OKLCH Color Picker](https://oklch.com/) diff --git a/.opencode/skills/tmux-automation/SKILL.md b/.opencode/skills/tmux-automation/SKILL.md new file mode 100644 index 00000000..d441ad6a --- /dev/null +++ b/.opencode/skills/tmux-automation/SKILL.md @@ -0,0 +1,161 @@ +--- +name: tmux-automation +description: Run commands in detached tmux sessions and programmatically send input, capture output, and manage sessions +--- + +# Tmux Automation Skill + +## When to Use This Skill + +Use this skill when you need to: +- Run interactive CLI tools (vim, git rebase, REPLs) from scripts +- Execute long-running commands that survive terminal disconnect +- Automate interactive workflows programmatically +- Run commands in a persistent session across multiple operations + +## Key Commands + +### Create Detached Session + +```bash +# Basic detached session +tmux new-session -d -s "" + +# With working directory +tmux new-session -d -s -c /path/to/dir "" + +# Simple shell session (no command) +tmux new-session -d -s +``` + +### Send Input to Session + +```bash +# Send text and execute (note: Enter is separate argument) +tmux send-keys -t 'echo "hello"' Enter + +# Send special keys +tmux send-keys -t C-c # Ctrl+C +tmux send-keys -t Escape # ESC +tmux send-keys -t Up # Arrow keys +tmux send-keys -t Down +tmux send-keys -t Tab + +# Multiple keys in sequence +tmux send-keys -t 'i' 'Hello World' Escape ':wq' Enter +``` + +### Capture Session Output + +```bash +# Capture visible pane content +tmux capture-pane -t -p + +# Capture entire scrollback +tmux capture-pane -t -p -S -1000 +``` + +### Manage Sessions + +```bash +# Check if session exists +tmux has-session -t 2>/dev/null && echo "exists" + +# Kill session when done +tmux kill-session -t + +# List all sessions +tmux ls +``` + +## Target Format + +Sessions are referenced as: +- `session-name` — session (window 0) +- `session-name.0` — first window in session +- `session-name:0` — alternative syntax for window +- `session-name.0.0` — specific pane (window 0, pane 0) + +## Workflow Pattern + +```bash +# 1. Create detached session and wait for init +SESSION="my-session" +tmux new-session -d -s "$SESSION" "python manage.py shell" +sleep 0.3 # Wait for initialization + +# 2. Send commands and capture output +tmux send-keys -t "$SESSION" 'from users.models import User' Enter +tmux send-keys -t "$SESSION" 'User.objects.count()' Enter +OUTPUT=$(tmux capture-pane -t "$SESSION" -p) +echo "$OUTPUT" + +# 3. Clean up +tmux kill-session -t "$SESSION" +``` + +## Common Key Names + +| Key | tmux send-keys value | +|-----|---------------------| +| Enter | `Enter` | +| Escape | `Escape` | +| Tab | `Tab` | +| Ctrl+C | `C-c` | +| Ctrl+D | `C-d` | +| Ctrl+X | `C-x` | +| Up/Down/Left/Right | `Up`, `Down`, `Left`, `Right` | +| Home | `Home` | +| End | `End` | + +## Best Practices + +1. **Always wait after session creation** — Add 100-500ms sleep before first capture/send +2. **Use descriptive session names** — Makes debugging easier +3. **Clean up when done** — Always kill-session to avoid orphaned sessions +4. **Check session exists** — Before sending keys, verify with `has-session` +5. **Enter is a separate argument** — Don't include it in the text string +6. **Use -c for working directory** — Ensures commands run in correct location + +## Example Use Cases + +### Run Django Management Command +```bash +SESSION="django-migrate" +tmux new-session -d -s "$SESSION" "python manage.py migrate" +sleep 2 +tmux capture-pane -t "$SESSION" -p +tmux kill-session -t "$SESSION" +``` + +### Interactive Python REPL Automation +```bash +SESSION="python-repl" +tmux new-session -d -s "$SESSION" "python" +sleep 0.3 +tmux send-keys -t "$SESSION" 'import json' Enter +tmux send-keys -t "$SESSION" 'print(json.dumps({"a": 1}))' Enter +tmux capture-pane -t "$SESSION" -p +tmux send-keys -t "$SESSION" 'exit()' Enter +tmux kill-session -t "$SESSION" +``` + +### Run Long-Remaining Server +```bash +SESSION="dev-server" +tmux new-session -d -s "$SESSION" -c /project "npm run dev" +# Session persists, can check output anytime +tmux capture-pane -t "$SESSION" -p +# When done: +tmux kill-session -t "$SESSION" +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Blank screen on capture | Add `sleep 0.3` after session creation | +| Command not executing | Send explicit `Enter` key as separate argument | +| `\n` doesn't work | Use `Enter` not `\n` | +| Session not found | Check with `tmux has-session -t ` first | +| Keys sent to wrong window | Use `session:window.pane` format explicitly | \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..56f043d3 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..d6422097 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..83110219 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,56 @@ +# Contributing + +This project is built with [Astro](https://astro.build/). Below is the usual Astro blog-template orientation and command reference. + +## Project structure + +```text +├── public/ +├── src/ +│ ├── assets/ +│ ├── components/ +│ ├── content/ +│ ├── layouts/ +│ └── pages/ +├── astro.config.mjs +├── package.json +└── tsconfig.json +``` + +Astro treats files under `src/pages/` as routes (`.astro`, `.md`, etc.). + +`src/components/` holds Astro (and optional framework) components. + +`src/content/` holds content collections—for example blog posts under `src/content/blog/`. Use `getCollection()` to query them and optional schemas to type-check frontmatter. See [Content collections](https://docs.astro.build/en/guides/content-collections/). + +Static assets such as images can go in `public/` or be imported from `src/assets/` as needed. + +## Styling (Tailwind CSS + daisyUI) + +This matches the open-source app at [podcodar/webapp](https://github.com/podcodar/webapp): **Tailwind CSS v4** via [`@tailwindcss/vite`](https://tailwindcss.com/docs/installation/framework-guides/astro) and **daisyUI** (`@import "tailwindcss";` + `@plugin "daisyui";` in [`src/styles/global.css`](src/styles/global.css)). + +Semantic colors (`primary`, `secondary`, `accent`, `base-*`, etc.) come from daisyUI’s default light/dark themes—the same OKLCH tokens as [podcodar.org](https://podcodar.org). The document theme follows system preference (`prefers-color-scheme`). + +For a quick visual check, open [`/design-system`](src/pages/design-system.astro) after `bun dev`. + +## Commands + +Run these from the repository root: + +| Command | Action | +| :-------------------- | :------------------------------------------- | +| `bun install` | Install dependencies | +| `bun dev` | Start dev server at `localhost:4321` | +| `bun run build` | Production build to `./dist/` | +| `bun run preview` | Preview the production build locally | +| `bun astro ...` | Astro CLI (`astro add`, `astro check`, etc.) | +| `bun astro -- --help` | Astro CLI help | + +## Learn more + +- [Astro documentation](https://docs.astro.build) +- [Astro Discord](https://astro.build/chat) + +## Credit + +This starter theme is based on [Bear Blog](https://github.com/HermanMartinus/bearblog/). diff --git a/README.md b/README.md index 6bc5616a..0c3a9caa 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,42 @@ -# Welcome to PodCodar WebApp! +# Welcome to PodCodar WebApp -This WebApp is the main project of PodCodar, a learning community about -programming and technology. +This WebApp is the main project of [PodCodar](https://github.com/podcodar), a learning community about programming and technology. -- 📖 [React Router docs](https://reactrouter.com/start/home) +- 📖 [Astro documentation](https://docs.astro.build) - 🧑‍💻 [PodCodar Engineering docs](https://podcodar.github.io/webapp) ## Features -- 🚀 Server-side rendering -- ⚡️ Hot Module Replacement (HMR) -- 📦 Asset bundling and optimization -- 🔄 Data loading and mutations +- ⚡ Static site generation with [Astro](https://astro.build/) +- 📝 Markdown and MDX for pages and blog posts +- 📚 Content collections with typed frontmatter +- 🗺️ Sitemap and RSS feed - 🔒 TypeScript by default -- 🎉 TailwindCSS for styling -- 📖 [React Router docs](https://reactrouter.com/) +- 🎨 Minimal, customizable styling -## Development +## Quick start -### System Dependencies - -To have a consistent development environment, we recommend using the following -tools: - -- [Bun](https://bun.sh) -- [direnv](https://direnv.net/) - -### Setup - -```shellscript -# (optional) if you have direnv -direnv allow - -# install dependencies +```sh bun install - -# decrypt project credentials into a .env file -bun decrypt +bun dev ``` -### Running the dev server +The dev server runs at [http://localhost:4321](http://localhost:4321). -```shellscript -bun run dev -``` +For project layout, CLI commands, and how we work on this repo, see [CONTRIBUTING.md](./CONTRIBUTING.md). ## Production -First, build your app for production: +Build the site: ```sh bun run build ``` -Then run the app in production mode: +Output is written to `./dist/`. Preview the production build locally: ```sh -bun start +bun run preview ``` -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app -server is production-ready. - -Make sure to deploy the output of `bun run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already -configured for a simple default starting experience. You can use whatever css -framework you prefer. See the -[Vite docs on css](https://vitejs.dev/guide/features.html#css) for more -information. +Deploy the contents of `dist/` to any static host (GitHub Pages, Netlify, Cloudflare Pages, etc.). diff --git a/app/catchall.tsx b/app/catchall.tsx deleted file mode 100644 index d0773650..00000000 --- a/app/catchall.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Section from "@packages/components/Section"; -import { LocalizedText } from "@packages/locale/context"; - -function PageNotFound() { - return ( -
-
-

- -

-

(╯°□°)╯︵ ┻━┻

-
-
- ); -} - -export default PageNotFound; diff --git a/app/cookies.server.ts b/app/cookies.server.ts deleted file mode 100644 index 447204bc..00000000 --- a/app/cookies.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MAX_COOKIE_AGE } from "@packages/contants"; -import { createCookie } from "react-router"; - -export const selectedTheme = createCookie("selected-theme", { - path: "/", - maxAge: MAX_COOKIE_AGE, -}); diff --git a/app/entry.server.tsx b/app/entry.server.tsx deleted file mode 100644 index a32c535b..00000000 --- a/app/entry.server.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { isbot } from "isbot"; -import { renderToReadableStream } from "react-dom/server"; -import { - type AppLoadContext, - type EntryContext, - ServerRouter, -} from "react-router"; - -const ABORT_DELAY = 5000; - -export default async function handleRequest( - request: Request, - status: number, - headers: Headers, - routerContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - _loadContext: AppLoadContext, -) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), ABORT_DELAY); - - const body = await renderToReadableStream( - , - { - signal: controller.signal, - onError(error: unknown) { - if (!controller.signal.aborted) { - // NOTE: Log streaming rendering errors from inside the shell - console.error(error); - } - status = 500; - }, - }, - ); - - body.allReady.then(() => clearTimeout(timeoutId)); - - if (isbot(request.headers.get("user-agent") || "")) { - await body.allReady; - } - - headers.set("Content-Type", "text/html"); - return new Response(body, { headers, status }); -} diff --git a/app/root.tsx b/app/root.tsx deleted file mode 100644 index 9cc70d4e..00000000 --- a/app/root.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import type { Theme } from "@packages/utils/theme"; -import { - type ActionFunctionArgs, - Links, - type LinksFunction, - type LoaderFunctionArgs, - Meta, - Outlet, - Scripts, - ScrollRestoration, - isRouteErrorResponse, - redirect, - useLoaderData, - useRouteError, -} from "react-router"; - -import "./tailwind.css"; -import Metadata from "@packages/components/Metadata"; -import Providers from "@packages/components/Providers"; -import { selectedTheme } from "./cookies.server"; - -export const links: LinksFunction = () => [ - // NOTE: Example of blocking scripts - // - //{ rel: "preconnect", href: "https://fonts.googleapis.com" }, - //{ - // rel: "preconnect", - // href: "https://fonts.gstatic.com", - // crossOrigin: "anonymous", - //}, - //{ - // rel: "stylesheet", - // href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - //}, -]; - -export async function loader({ request }: LoaderFunctionArgs) { - const cookieHeader = request.headers.get("Cookie"); - const rawTheme = await selectedTheme.parse(cookieHeader); - const theme: Theme = rawTheme ?? "system"; - - return { theme }; -} - -export async function action({ request }: ActionFunctionArgs) { - const formData = await request.formData(); - const theme = (formData.get("theme") ?? "system") as Theme; - - const redirectUrl = request.headers.get("referer") ?? "/"; - - return redirect(redirectUrl, { - headers: { - "Set-Cookie": await selectedTheme.serialize(theme), - }, - }); -} - -export function Layout({ children }: React.PropsWithChildren) { - const { theme } = useLoaderData(); - - return ( - - - - - - {/* default metadata */} - - - {/* specific metadata */} - - - - - - {children} - - - - - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data - -
{{ theme }}
- -### Page Data - -
{{ page }}
- -### Page Frontmatter - -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/src/vitepress/index.md b/docs/src/vitepress/index.md deleted file mode 100644 index eba80117..00000000 --- a/docs/src/vitepress/index.md +++ /dev/null @@ -1,8 +0,0 @@ -# VitePress - -VitePress is a Vue-powered static site generator that is focused on being easy to use and fast to build with. It is created by Evan You, the creator of Vue.js. - -## Features - -- [Markdown](/vitepress/markdown-examples) -- [API usage](/vitepress/api-examples) diff --git a/docs/src/vitepress/markdown-examples.md b/docs/src/vitepress/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/docs/src/vitepress/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index 8ba011cc..00000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { raise } from "@packages/utils/typescript"; -import { config } from "dotenv"; -import { defineConfig } from "drizzle-kit"; - -config({ path: ".env" }); - -export default defineConfig({ - schema: "./packages/repositories/db/schema.ts", - out: "./migrations", - dialect: "turso", - dbCredentials: { - url: - process.env.TURSO_CONNECTION_URL ?? raise("missing TURSO_CONNECTION_URL"), - authToken: - process.env.TURSO_AUTH_TOKEN ?? raise("missing TURSO_AUTH_TOKEN"), - }, -}); diff --git a/e2e/blog.spec.ts b/e2e/blog.spec.ts new file mode 100644 index 00000000..329e89a8 --- /dev/null +++ b/e2e/blog.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from '@playwright/test'; + +// ────────────────────────────────────────────────────────────────────────────── +// Blog +// ────────────────────────────────────────────────────────────────────────────── + +test.describe('Blog', () => { + test('navigates to blog homepage via nav link', async ({ page }) => { + await page.goto('/'); + + // Primary nav has a "Blog" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /^blog$/i }) + .click(); + + await expect(page).toHaveURL(/\/blog/); + + // The blog index page should list at least one post + const postLinks = page.getByRole('link').filter({ hasText: /./u }); + await expect(postLinks.first()).toBeVisible(); + }); + + test('blog homepage lists posts', async ({ page }) => { + await page.goto('/blog'); + + await expect(page).toHaveTitle(/PodCodar/i); + + // There should be at least one article/post link on the listing page + const firstPost = page.locator('section ul li a').first(); + await expect(firstPost).toBeVisible(); + }); + + test('opens the first blog post from the listing', async ({ page }) => { + await page.goto('/blog'); + + // Click the very first post link in the listing + const firstPost = page.locator('section ul li a').first(); + await expect(firstPost).toBeVisible(); + + const postTitle = await firstPost.textContent(); + await firstPost.click(); + + // We should be on a blog post URL + await expect(page).toHaveURL(/\/blog\/.+/); + + // The post page should have a

matching the post title text + if (postTitle) { + await expect(page.getByRole('heading', { level: 1 }).first()).toBeVisible(); + } + }); + + test('scrolls down the first blog post', async ({ page }) => { + await page.goto('/blog'); + + // Open the first post + await page.locator('section ul li a').first().click(); + await expect(page).toHaveURL(/\/blog\/.+/); + + // Confirm the post heading is visible + await expect(page.getByRole('heading', { level: 1 }).first()).toBeVisible(); + + // Scroll to the bottom of the post + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); + + // Footer should come into view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); +}); diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts new file mode 100644 index 00000000..68f6a45b --- /dev/null +++ b/e2e/site.spec.ts @@ -0,0 +1,112 @@ +import { expect, test } from '@playwright/test'; + +// ────────────────────────────────────────────────────────────────────────────── +// Homepage +// ────────────────────────────────────────────────────────────────────────────── + +test.describe('Homepage', () => { + test('has page title and hero section', async ({ page }) => { + await page.goto('/'); + + // The document title should mention PodCodar + await expect(page).toHaveTitle(/PodCodar/i); + + // Hero heading is visible + const heroHeading = page.locator('#hero-heading'); + await expect(heroHeading).toBeVisible(); + }); + + test('scrolls down the homepage', async ({ page }) => { + await page.goto('/'); + + // Confirm hero is visible before scrolling + await expect(page.locator('#hero-heading')).toBeVisible(); + + // Scroll to the bottom of the page + await page.evaluate(() => + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) + ); + + // Footer is now in view + const footer = page.locator('footer'); + await expect(footer).toBeInViewport(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Contributing page (/contributing) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe('Contributing page', () => { + test('navigates to Contributing page via header CTA', async ({ page }) => { + await page.goto('/'); + + // The header CTA "Como posso ajudar?" links to /contributing + await page + .getByRole('link', { name: /como posso ajudar/i }) + .first() + .click(); + + await expect(page).toHaveURL(/\/contributing/); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); + + test('can navigate directly to /contributing', async ({ page }) => { + await page.goto('/contributing'); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /como posso ajudar/i, level: 1 })).toBeVisible(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Join Us page (/join-us) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe('Join Us page', () => { + test('navigates to Join Us page via header CTA', async ({ page }) => { + await page.goto('/'); + + // The header primary CTA "Faça parte!" links to /join-us + await page + .getByRole('link', { name: /faça parte/i }) + .first() + .click(); + + await expect(page).toHaveURL(/\/join-us/); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); + + test('can navigate directly to /join-us', async ({ page }) => { + await page.goto('/join-us'); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /faça parte/i, level: 1 })).toBeVisible(); + }); +}); + +// ────────────────────────────────────────────────────────────────────────────── +// Navigation: Contact page (/contact) +// ────────────────────────────────────────────────────────────────────────────── + +test.describe('Contact page', () => { + test('navigates to Contact page via nav link', async ({ page }) => { + await page.goto('/'); + + // Primary nav has a "Contato" link + await page + .getByRole('navigation', { name: 'Primary' }) + .getByRole('link', { name: /contato/i }) + .click(); + + await expect(page).toHaveURL(/\/contact/); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); + + test('can navigate directly to /contact', async ({ page }) => { + await page.goto('/contact'); + + await expect(page).toHaveTitle(/PodCodar/i); + await expect(page.getByRole('heading', { name: /contato|entre em contato/i })).toBeVisible(); + }); +}); diff --git a/functions/[[path]].ts b/functions/[[path]].ts deleted file mode 100644 index 504f6a52..00000000 --- a/functions/[[path]].ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createPagesFunctionHandler } from "@react-router/cloudflare"; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore - the server build file is generated by `react-router build` -import * as build from "../build/server"; -import { getLoadContext } from "../load-context"; - -export const onRequest = createPagesFunctionHandler({ - build, - getLoadContext, -}); diff --git a/infra/.gitignore b/infra/.gitignore new file mode 100644 index 00000000..2ccbe465 --- /dev/null +++ b/infra/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/infra/Pulumi.dev.yaml b/infra/Pulumi.dev.yaml new file mode 100644 index 00000000..b0cf332c --- /dev/null +++ b/infra/Pulumi.dev.yaml @@ -0,0 +1,5 @@ +encryptionsalt: v1:Gdj9op3TwXU=:v1:kDnyl22pMO9mQbYf:8Z2dtUWLJir5VNDyjHZcmQVY9MxFGg== +config: + podcodar:accountId: 9ccff81da5d7c234138d96989610537d + podcodar:zoneId: ec5904d9f2d5278ff1ecaf0ca2b71808 + podcodar:environment: dev diff --git a/infra/Pulumi.production.yaml b/infra/Pulumi.production.yaml new file mode 100644 index 00000000..cec49c87 --- /dev/null +++ b/infra/Pulumi.production.yaml @@ -0,0 +1,5 @@ +encryptionsalt: v1:rdiHVQl5LMg=:v1:TBUR8b5pLVzpz7EC:BKWBG1o1aIs3OgqwO1JLcbCXx8e8/A== +config: + podcodar:accountId: 9ccff81da5d7c234138d96989610537d + podcodar:zoneId: ec5904d9f2d5278ff1ecaf0ca2b71808 + podcodar:environment: production diff --git a/infra/Pulumi.yaml b/infra/Pulumi.yaml new file mode 100644 index 00000000..585c2e65 --- /dev/null +++ b/infra/Pulumi.yaml @@ -0,0 +1,7 @@ +name: podcodar +description: A minimal Astro Pulumi setup for Cloudflare +runtime: bun +config: + pulumi:tags: + value: + pulumi:template: bun diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 00000000..3b2cc23e --- /dev/null +++ b/infra/README.md @@ -0,0 +1,206 @@ +# Infrastructure (Pulumi + Cloudflare) + +Stack-based Infrastructure as Code for provisioning Cloudflare Workers. + +--- + +## Stack + +| Stack | Domain | Use case | +| ------------------- | ------------------ | ---------------------------- | +| `webapp.dev` | `dev.podcodar.org` | Preview / PR deployments | +| `webapp.production` | `podcodar.org` | Production (merge to `main`) | + +--- + +## Prereqs + +1. **Pulumi CLI** — `curl -fsSL https://get.pulumi.com | sh` (or `brew install pulumi`) +2. **Bun** — `brew install oven-sh/node/bun` +3. **Cloudflare account** with: + - `CLOUDFLARE_API_TOKEN` — scoped to Workers & Zone (not Global Key) + - `accountId` — Found in Cloudflare Dashboard → Workers & Pages → Overview + - `zoneId` — Found in Cloudflare Dashboard → Domain → Overview + +4. **Pulumi access token** — `pulumi login` then `pulumi org`, or create at [app.pulumi.com](https://app.pulumi.com) + +--- + +## Secrets + +All secrets live in the stack config, **never** in code. + +### Why secrets.yaml? + +`Pulumi.*.yaml` files are encrypted at rest via `encryptionsalt`. The raw `secure:` values are only readable by you. However, **never commit `secure:` values that are real** — treat them like passwords. + +### Required secrets + +| Key | Where to get it | +| ------------------ | ----------------------------------------- | +| `webapp:accountId` | Cloudflare Dashboard → Workers → Overview | +| `webapp:zoneId` | Cloudflare Dashboard → Domain → Overview | + +### Setting secrets (local) + +```bash +# From the infra/ directory +cd infra + +# Set plain config +pulumi config set accountId YOUR_ACCOUNT_ID + +# Set secret (encrypted in Pulumi.*.yaml) +pulumi config set zoneId YOUR_ZONE_ID --secret +``` + +### Reading secrets + +```bash +# Reveal a secret (prints to stdout — be careful) +pulumi config get zoneId --show-secrets +``` + +### CI/CD secrets + +In GitHub → Repo Settings → Secrets: + +| Secret | Description | +| ---------------------- | --------------------------------------------- | +| `PULUMI_ACCESS_TOKEN` | Pulumi token for CI login | +| `CLOUDFLARE_API_TOKEN` | Cloudflare token passed as `CF_API_TOKEN` env | + +--- + +## Sessions (local dev) + +```bash +cd infra + +# Preview what would happen (no changes made) +bun run preview + +# Apply changes (creates/updates resources) +bun run up + +# Tear down everything (destructive) +bun run destroy + +# Apply a specific resource only (e.g. after fixing a single resource) +bun run up --target urn:pulumi:webapp::cloudflare:index:Worker::podcodar-dev +``` + +### Stack selection + +By default, `pulumi up` operates on the active stack. To target a specific stack: + +```bash +pulumi stack select webapp.dev +bun run up +``` + +To create a new stack: + +```bash +pulumi stack init webapp.staging +# then set config +pulumi config set environment staging +pulumi config set accountId YOUR_ID --secret +pulumi config set zoneId YOUR_ZONE --secret +``` + +--- + +## Deploy flow + +### Local dev loop + +```bash +# 1. Make code changes in webapp/ + +# 2. Build Astro (produces dist/) +bun run w:build + +# 3. Preview infra changes +bun run preview + +# 4. If happy, apply +bun run up +``` + +### CI/CD (GitHub Actions) + +Merge to `main` → Quality gateway passes → Deploy workflow: + +1. **Build** — `bun run w:build` produces `dist/server/` + `dist/client/` +2. **Pulumi login** — via `pulumi/actions@v5` (OIDC or `PULUMI_ACCESS_TOKEN`) +3. **Pulumi up** — runs on `webapp.production` stack, uploads modules + deploys + +The deploy is **idempotent** — `pulumi up` only creates/changes what has changed since last run. + +--- + +## How the Worker is built & deployed + +``` +astro build + └── dist/ + ├── client/ → WorkerVersion assets (html, css, js) + └── server/ → WorkerVersion modules (entry.mjs + chunks/*.mjs) + +wrangler build + └── picks up @astrojs/cloudflare entrypoint + └── modules auto-discovered from dist/server/chunks/ +``` + +`discoverWorkerModules()` in `index.ts` scans `dist/server/` at **plan time** (not deploy time) — so Pulumi knows which files changed and triggers a new version accordingly. + +--- + +## CI auth (OIDC vs PAT) + +**PAT (what we use):** Simple but long-lived. The `PULUMI_ACCESS_TOKEN` secret must be kept safe. + +**OIDC (recommended for prod):** No long-lived secrets. Configure via: + +```yaml +# In .github/workflows/deploy.yml +- uses: pulumi/actions@v5 + with: + cloud-url: puffinsystems # tells Pulumi to use cloud state backend + # No token needed if GCP/Azure/GitHub OIDC is configured +``` + +See [Pulumi OIDC docs](https://www.pulumi.com/docs/using-pulumi/continuous-delivery/github-actions/) to upgrade. + +--- + +## Tear down & recover + +If you accidentally delete the Worker in the Cloudflare dashboard: + +```bash +bun run up # Pulumi recreates everything from state +``` + +If you lose Pulumi state (worst case): + +> **WARNING:** Without state, Pulumi cannot manage existing resources. You must either: +> +> - Import existing resources manually: `pulumi import cloudflare:index/worker:Worker my-worker accountId/name` +> - Or delete via Cloudflare dashboard and let Pulumi recreate + +Always run `pulumi login` with a **cloud backend** (not local file) to prevent state loss. + +--- + +## File structure + +``` +infra/ +├── index.ts ← All Cloudflare resources +├── Pulumi.yaml ← Stack metadata (name, runtime) +├── Pulumi.dev.yaml ← Dev config + encrypted secrets +├── Pulumi.production.yaml ← Production config + encrypted secrets +└── package.json ← bun run up/preview/destroy +``` diff --git a/infra/index.ts b/infra/index.ts new file mode 100644 index 00000000..293ed3f1 --- /dev/null +++ b/infra/index.ts @@ -0,0 +1,98 @@ +import * as cloudflare from '@pulumi/cloudflare'; +import * as command from '@pulumi/command'; +import * as pulumi from '@pulumi/pulumi'; + +import { absolutePath, discoverWorkerModules, today } from './utils'; + +const config = new pulumi.Config(); +const accountId = config.require('accountId'); +const zoneId = config.require('zoneId'); +const environment = config.require('environment'); +const isProd = environment === 'production'; +const workerDomain = isProd ? 'https://prod.podcodar.org' : 'https://dev.podcodar.org'; + +const builder = new command.local.Command('build-worker', { + create: 'cd .. && bun run w:build', + delete: 'echo "No cleanup necessary"', + environment: { + BASE_URL: workerDomain, + }, +}); + +const worker = new cloudflare.Worker( + `podcodar-${environment}`, + { + accountId, + name: `podcodar-${environment}`, + observability: { + enabled: true, + headSamplingRate: 1, + logs: { + enabled: true, + headSamplingRate: 1, + invocationLogs: true, + }, + }, + }, + { dependsOn: [builder] } +); + +const workerVersion = new cloudflare.WorkerVersion( + `podcodar-worker-version-${environment}`, + { + accountId, + workerId: worker.id, + mainModule: 'index.js', + compatibilityDate: today(), + compatibilityFlags: ['global_fetch_strictly_public', 'nodejs_compat'], + + assets: { + directory: absolutePath('../dist/client/'), + config: { + runWorkerFirst: false, + }, + }, + + bindings: [ + { type: 'assets', name: 'ASSETS' }, + { + name: 'BASE_URL', + type: 'plain_text', + text: workerDomain, + }, + ], + + modules: discoverWorkerModules(absolutePath('../dist/server')), + }, + { dependsOn: [worker] } +); + +const workerDeployment = new cloudflare.WorkersDeployment( + `podcodar-worker-deployment-${environment}`, + { + accountId, + scriptName: worker.name, + strategy: 'percentage', + versions: [ + { + versionId: workerVersion.id, + percentage: 100, + }, + ], + }, + { dependsOn: [workerVersion] } +); + +new cloudflare.WorkersCustomDomain( + `podcodar-custom-domain-${environment}`, + { + zoneId, + accountId, + service: worker.name, + hostname: workerDomain, + }, + { dependsOn: [worker, workerDeployment] } +); + +export const workerScriptName = worker.name; +export const domain = workerDomain; diff --git a/infra/package.json b/infra/package.json new file mode 100644 index 00000000..d43b5121 --- /dev/null +++ b/infra/package.json @@ -0,0 +1,22 @@ +{ + "name": "webapp", + "main": "index.ts", + "type": "module", + "scripts": { + "up": "pulumi up", + "preview": "pulumi preview", + "destroy": "pulumi destroy", + "destroy --target": "pulumi destroy --target" + }, + "devDependencies": { + "@pulumi/command": "^1.2.1", + "@types/bun": "latest" + }, + "dependencies": { + "@pulumi/pulumi": "^3.231.0", + "@pulumi/cloudflare": "^6.14.0" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/infra/tsconfig.json b/infra/tsconfig.json new file mode 100644 index 00000000..c556c932 --- /dev/null +++ b/infra/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "files": ["index.ts"] +} diff --git a/infra/utils.ts b/infra/utils.ts new file mode 100644 index 00000000..ad0316af --- /dev/null +++ b/infra/utils.ts @@ -0,0 +1,64 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +export const today = () => new Date().toISOString().split('T')[0]; + +export const absolutePath = (relativePath: string) => + new URL(relativePath, import.meta.url).pathname; + +/** + * Discover all .mjs module files from the Astro build output. + * Astro generates code-split chunks that need to be uploaded as separate modules. + */ +export function discoverWorkerModules(serverDir: string): Array<{ + name: string; + contentFile: string; + contentType: string; +}> { + const modules: Array<{ + name: string; + contentFile: string; + contentType: string; + }> = []; + + // The entry point is special - it's renamed to index.js + const entryPath = path.join(serverDir, 'entry.mjs'); + if (fs.existsSync(entryPath)) { + modules.push({ + name: 'index.js', + contentFile: entryPath, + contentType: 'application/javascript+module', + }); + } + + // Discover other .mjs files in the server root (excluding entry.mjs) + if (fs.existsSync(serverDir)) { + const rootFiles = fs.readdirSync(serverDir); + for (const file of rootFiles) { + if (file.endsWith('.mjs') && file !== 'entry.mjs') { + modules.push({ + name: file, + contentFile: path.join(serverDir, file), + contentType: 'application/javascript+module', + }); + } + } + } + + // Discover all .mjs files in the chunks directory + const chunksDir = path.join(serverDir, 'chunks'); + if (fs.existsSync(chunksDir)) { + const chunkFiles = fs.readdirSync(chunksDir); + for (const file of chunkFiles) { + if (file.endsWith('.mjs')) { + modules.push({ + name: `chunks/${file}`, + contentFile: path.join(chunksDir, file), + contentType: 'application/javascript+module', + }); + } + } + } + + return modules; +} diff --git a/lefthook.yml b/lefthook.yml index 57b69cfe..dc625dd1 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,22 +1,25 @@ -# EXAMPLE USAGE: -# -# Refer for explanation to following link: -# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md -# +pre-commit: + parallel: true + commands: + lint-code: + run: bunx biome check --fix --unsafe {staged_files} && bunx biome format --fix {staged_files} + glob: "*.{ts,tsx,js,jsx,astro,json}" + stage_fixed: true + + lint-docs: + run: bunx prettier --write {staged_files} + glob: "*.{md,mdx}" + stage_fixed: true + + format-yaml: + run: bunx prettier --write {staged_files} + glob: "*.{yml,yaml}" + stage_fixed: true + pre-push: commands: - build: - tags: build - run: bun run build lint: - tags: lint run: bun run lint - unit-test: - tags: tests - run: bun run test -pre-commit: - commands: - lint: - tags: lint - run: bun lint-staged + e2e: + run: CI=true bun run e2e diff --git a/load-context.ts b/load-context.ts deleted file mode 100644 index c02ec42c..00000000 --- a/load-context.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { AppLoadContext } from "react-router"; -import type { PlatformProxy } from "wrangler"; - -type Cloudflare = Omit, "dispose">; - -declare module "react-router" { - interface AppLoadContext { - cloudflare: Cloudflare; - extra: string; // augmented - } -} - -type GetLoadContext = (args: { - request: Request; - context: { cloudflare: Cloudflare }; // load context _before_ augmentation -}) => AppLoadContext; - -// Shared implementation compatible with Vite, Wrangler, and Cloudflare Pages -export const getLoadContext: GetLoadContext = ({ context }) => { - return { - ...context, - extra: "stuff", - }; -}; diff --git a/maestro.yaml b/maestro.yaml new file mode 100644 index 00000000..a79bea51 --- /dev/null +++ b/maestro.yaml @@ -0,0 +1,223 @@ +# Maestro AI Diamond Chain workflow definition +name: my-project +description: Squad-based development using AI Diamond Chain methodology + +# Supported tools — agent runtimes to scaffold and use +# Supported values: opencode, amp +tools: + - opencode + +# Squad definition — maps roles to agent configurations +agents: + architect: + role: architect + phase: discovery + + researcher: + role: researcher + phase: discovery + + ux-designer: + role: ux-designer + phase: discovery + + frontend-engineer: + role: frontend-engineer + phase: build + + backend-engineer: + role: backend-engineer + phase: build + + devops-sre: + role: devops-sre + phase: build + + qa-engineer: + role: qa-engineer + phase: quality + + code-reviewer: + role: code-reviewer + phase: quality + +# AI Diamond Chain Configuration +# Alternating Discovery and Implementation diamonds with Learn & Feed loop +diamond_chain: + mode: infinite # or "fixed:N" for limited iterations + learning_accumulation: true # Compound learnings across diamonds + +# Diamond Definitions +diamonds: + # Discovery Diamond: "What should we build?" + discovery: + description: Understand the problem and decide what to build + + # Phase 1: Diverge (Explore) + diverge: + parallel: true + agents: [researcher, ux-designer, architect] + activities: + researcher: + - market_analysis + - competitive_review + - trend_identification + ux-designer: + - user_research + - needs_exploration + - persona_development + architect: + - tech_hypotheses + - feasibility_exploration + - constraint_analysis + duration_target: "2-3 days" # AI-accelerated + + # Phase 2: Converge (Define) + converge: + agent: maestro + outputs: + - discovery-decision-gate.md + criteria: + - problem_validated + - scope_defined + - success_criteria_set + - market_opportunity_sized + + decision_gate: + options: [GO, NO-GO, REFINE] + auto_trigger_next: false # Human approval required + confidence_threshold: 70 # Minimum for GO decision + refinement_triggers: + - known_unknowns_count_exceeds: 2 + - conflicting_findings + - low_confidence_critical_items + - missing_dependencies + + # Implementation Diamond: "How do we build it?" + implementation: + description: Build and ship the solution + + # Phase 1: Diverge (Explore) + diverge: + parallel: true + agents: [architect, backend-engineer, frontend-engineer, devops-sre] + activities: + architect: + - architecture_options + - tradeoff_analysis + - pattern_exploration + backend-engineer: + - api_prototyping + - db_design_options + frontend-engineer: + - ui_pattern_exploration + - component_prototyping + devops-sre: + - infrastructure_options + - deployment_strategies + duration_target: "1-2 days" # AI-accelerated + + # Phase 2: Converge (Deliver) + converge: + parallel: true + build_agents: [backend-engineer, frontend-engineer, devops-sre] + quality_agents: [qa-engineer, code-reviewer] + outputs: + - implementation-decision-gate.md + - build-report.md + - quality-gate-report.md + criteria: + - solution_works + - quality_met + - performance_acceptable + - security_scan_passed + + decision_gate: + options: [SHIP, NO-SHIP, FIX] + auto_trigger_next: true # Auto-trigger Learn & Feed + quality_threshold: 80 # Minimum quality score for SHIP + fix_max_retries: 3 + + # Learn & Feed: The Chain Link + learn_and_feed: + description: Capture learnings to feed next Discovery Diamond + enabled: true + auto_trigger_after: implementation # Auto-run after ship + agents: [researcher, ux-designer, architect] + activities: + researcher: + - usage_analytics + - market_response_analysis + ux-designer: + - user_feedback_synthesis + - satisfaction_analysis + architect: + - technical_learnings + - performance_assessment + outputs: + - learned-report.md + queue_for_next_discovery: true + +# Legacy workflow configuration (maintained for backward compatibility) +workflow: + discovery: + parallel: true + confidence_threshold: 70 + max_refinement_rounds: 3 + + synthesis: + approval: true + refinement_on: + - low_confidence_critical_items + - conflicting_findings + - known_unknowns_count_exceeds: 2 + - missing_dependencies + + build: + parallel: true + continuous_feedback: + enabled: true + trigger_on: + - module_complete + - checkpoint_reached + - pr_merged + modules: + - name: backend-core + agent: backend-engineer + checkpoint: "API contracts defined" + qa_entry_point: true + - name: infrastructure + agent: devops-sre + checkpoint: "Infrastructure as code complete" + qa_entry_point: true + - name: frontend-shell + agent: frontend-engineer + depends_on: [backend-core] + checkpoint: "UI components render with mock data" + qa_entry_point: true + feedback_queue: + enabled: true + file: "feedback-queue.md" + auto_pause_on_critical: true + + quality: + parallel: true + on_failure: fix_and_retry + max_retries: 3 + validations: + usecase_coverage: + required: true + min_coverage_percent: 80 + workflow_testing: + required: true + test_happy_path: true + test_error_paths: true + test_edge_cases: true + edge_case_coverage: + required: true + categories: + - boundary_conditions + - empty_null_states + - concurrent_access + - failure_modes + - security_edge_cases diff --git a/migrations/0000_large_preak.sql b/migrations/0000_large_preak.sql deleted file mode 100644 index 059887cc..00000000 --- a/migrations/0000_large_preak.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE `testimonials` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `avatarUrl` text NOT NULL, - `profileUrl` text NOT NULL, - `description` text NOT NULL -); diff --git a/migrations/0001_cooing_mephisto.sql b/migrations/0001_cooing_mephisto.sql deleted file mode 100644 index de908338..00000000 --- a/migrations/0001_cooing_mephisto.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE `members` ( - `id` integer PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `role` text NOT NULL, - `avatar` text NOT NULL, - `cover` text NOT NULL, - `github` text NOT NULL, - `linkedin` text NOT NULL -); diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json deleted file mode 100644 index 8c8a61d8..00000000 --- a/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "62a0a6e2-c642-4923-be57-536ac3cea2f9", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "testimonials": { - "name": "testimonials", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "avatarUrl": { - "name": "avatarUrl", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "profileUrl": { - "name": "profileUrl", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json deleted file mode 100644 index edf3cd36..00000000 --- a/migrations/meta/0001_snapshot.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "6e4813c7-b609-477e-991c-44e03735d9ef", - "prevId": "62a0a6e2-c642-4923-be57-536ac3cea2f9", - "tables": { - "members": { - "name": "members", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "avatar": { - "name": "avatar", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cover": { - "name": "cover", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "github": { - "name": "github", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linkedin": { - "name": "linkedin", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "testimonials": { - "name": "testimonials", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "avatarUrl": { - "name": "avatarUrl", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "profileUrl": { - "name": "profileUrl", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json deleted file mode 100644 index 8df1ef2b..00000000 --- a/migrations/meta/_journal.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1728058554783, - "tag": "0000_large_preak", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1728339182690, - "tag": "0001_cooing_mephisto", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..70df78a1 --- /dev/null +++ b/mise.toml @@ -0,0 +1,11 @@ +[env] +'_'.file = ".env" + +[tasks] +encrypt = "gpg -c .env" +decrypt = "gpg -d .env.gpg > .env" + +[tools] +node = "lts" +bun = "latest" +pulumi = "latest" diff --git a/package.json b/package.json index dd531680..1fd4be7c 100644 --- a/package.json +++ b/package.json @@ -1,82 +1,60 @@ { - "name": "@podcodar/webapp", - "homepage": "https://podcodar.org/", - "version": "0.1.1", - "private": true, - "sideEffects": false, - "type": "module", - "scripts": { - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs", - "dev": "bun run typegen && react-router dev", - "prebuild": "bun run typegen && bun run db:migrate", - "build": "react-router build", - "deploy": "bun run build && wrangler pages deploy", - "lint": "biome lint", - "fmt": "biome check --write ", - "encrypt": "gpg -c .env", - "decrypt": "gpg -d .env.gpg > .env; bun run w:typegen", - "w": "wrangler", - "w:secret": "wrangler pages secret", - "w:typegen": "cp .env .dev.vars && wrangler types", - "test": "bun test packages app", - "e2e": "playwright test", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio", - "preview": "bun run build && wrangler pages dev", - "start": "wrangler pages dev ./build/client", - "typegen": "drizzle-kit generate && react-router typegen", - "typecheck": "tsc" - }, - "dependencies": { - "@libsql/client": "^0.14.0", - "@m3o/auth": "npm:@jsr/m3o__auth", - "@react-router/cloudflare": "^7.1.3", - "@react-router/fs-routes": "^7.1.3", - "@react-router/node": "^7.1.3", - "@react-router/serve": "^7.1.3", - "@std/log": "npm:@jsr/std__log", - "daisyui": "^5.0.0-beta.3", - "dotenv": "^16.4.7", - "drizzle-orm": "^0.38.4", - "i18next": "^24.2.2", - "isbot": "^5.1.22", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-i18next": "^15.4.0", - "react-router": "^7.1.3" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@modyfi/vite-plugin-yaml": "^1.1.0", - "@playwright/test": "^1.50.0", - "@react-router/dev": "^7.1.3", - "@tailwindcss/postcss": "^4.0.1", - "@tailwindcss/vite": "^4.0.1", - "@types/node": "^20.17.16", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3", - "autoprefixer": "^10.4.20", - "drizzle-kit": "^0.30.4", - "lefthook": "^1.10.10", - "lint-staged": "^15.4.3", - "mermaid": "^11.4.1", - "postcss": "^8.5.1", - "tailwindcss": "^4.0.1", - "typescript": "^5.7.3", - "vite": "^5.4.14", - "vite-tsconfig-paths": "^4.3.2", - "vitepress": "^1.6.3", - "vitepress-carbon": "^1.5.0", - "vitepress-plugin-mermaid": "^2.0.17", - "wrangler": "^3.106.0" - }, - "lint-staged": { - "*.{md,mdx}": "bunx prettier --write", - "**/*.{tsx,ts,js,jsx}": "biome check --write" - }, - "engines": { - "node": ">=22.0.0" - } + "name": "webapp", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "workspaces": { + "packages": [ + "infra" + ] + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "w": "wrangler", + "w:build": "bun run build && wrangler build", + "astro": "astro", + "astro:a": "astro add", + "update": "bun update --all", + "lint": "biome check", + "lint:fix": "biome check --write && astro check --fix", + "format": "biome format --write && astro check --fix", + "format:check": "biome format", + "typecheck": "astro sync", + "test": "vitest run", + "test:watch": "vitest", + "e2e": "playwright test", + "lefthook": "lefthook", + "generate-types": "wrangler types", + "postinstall": "lefthook install && bun run generate-types" + }, + "dependencies": { + "@astrojs/check": "^0.9.8", + "@astrojs/cloudflare": "^13.1.10", + "@astrojs/mdx": "^5.0.3", + "@astrojs/rss": "^4.0.18", + "@astrojs/sitemap": "^3.7.2", + "@iconify-json/simple-icons": "^1.2.79", + "@tailwindcss/vite": "^4.2.2", + "astro": "^6.1.8", + "astro-icon": "^1.1.5", + "daisyui": "^5.5.19", + "sharp": "^0.34.5", + "tailwindcss": "^4.2.2", + "typescript": "^6.0.3", + "wrangler": "^4.83.0" + }, + "devDependencies": { + "@biomejs/biome": "^2.4.12", + "@playwright/test": "^1.59.1", + "lefthook": "^2.1.6", + "vite-tsconfig-paths": "^6.1.1", + "playwright": "^1.59.1", + "vitest": "^4.1.4" + }, + "overrides": { + "vite": "^7" + } } diff --git a/packages/components/Footer.tsx b/packages/components/Footer.tsx deleted file mode 100644 index def3b8d4..00000000 --- a/packages/components/Footer.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Logo } from "@packages/components/icons"; -import { PIX_KEY, images } from "@packages/config/site"; - -import { LocalizedText } from "@packages/locale/context"; -import Section from "./Section"; -import SocialIconLinks from "./SocialIconLinks"; - -export default function Footer() { - return ( -
-
- - - -
-
- ); -} - -function PodCodarLogo() { - return ( -
- -
- ); -} - -function Pix() { - return ( -
-

- -

-

{PIX_KEY}

- {"qr -
- ); -} - -function Copyrights() { - const currentYear = new Date().getFullYear(); - return ( -
-

- -

- -

- -

-
- ); -} diff --git a/packages/components/HeroSection.tsx b/packages/components/HeroSection.tsx deleted file mode 100644 index a66ed7d6..00000000 --- a/packages/components/HeroSection.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Illustration } from "@packages/components/icons"; - -import { links } from "@packages/config/site"; -import { LocalizedText } from "@packages/locale/context"; -import Section from "./Section"; - -export default function HeroSection() { - return ( -
-
-
-

- , - }} - /> -

- -

- -

- -
-
- -
- -
-
-
- ); -} diff --git a/packages/components/Link.tsx b/packages/components/Link.tsx deleted file mode 100644 index 5d78d947..00000000 --- a/packages/components/Link.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Link as DefaulLink, type LinkProps } from "react-router"; - -type Props = Omit & { - children: React.ReactNode; - href: string; - className?: string; - target?: "_blank" | "_self" | "_parent" | "_top"; -}; - -export default function Link({ - children, - className, - target, - href = "", - ...props -}: Props) { - if (target) { - return ( - - {children} {externalLinkIcon} - - ); - } - - return ( - - {children} - - ); -} - -const externalLinkIcon = ( - - - -); diff --git a/packages/components/MemberCard.tsx b/packages/components/MemberCard.tsx deleted file mode 100644 index b780f6af..00000000 --- a/packages/components/MemberCard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { LocalizedText } from "@packages/locale/context"; -import type { SelectMember } from "@packages/repositories/db/schema"; -import SocialIconLinks from "./SocialIconLinks"; - -interface Props { - member: SelectMember; -} - -export default function MemberCard({ member }: Props) { - return ( -
- member cover -
- member avatar -
- -
-

{member.name}

-

- -

- -
-
- ); -} diff --git a/packages/components/MentoringSection.tsx b/packages/components/MentoringSection.tsx deleted file mode 100644 index 7128ed92..00000000 --- a/packages/components/MentoringSection.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { LocalizedText, type TranslationToken } from "@packages/locale/context"; -import Section from "./Section"; - -export default function MentoringSection() { - return ( -
-
-
-

- -

- -
- -
-
- - {mentoringList.map((mentoring) => ( - - ))} -
-
- ); -} - -interface CardItemProps { - title: TranslationToken; - description: TranslationToken; -} - -function CardItem({ title, description }: CardItemProps) { - return ( -
-

- -

- - - -
- ); -} - -const mentoringList = [ - "study-mentorships", - "project-mentoring", - "market-mentoring", -]; diff --git a/packages/components/Metadata.tsx b/packages/components/Metadata.tsx deleted file mode 100644 index 478c8af7..00000000 --- a/packages/components/Metadata.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { description, images, pageURL, title } from "@packages/config/site"; - -function Metadata() { - return ( - <> - {title} - - - - - {/* Google / Search Engine Tags */} - - - - - {/* Facebook Meta Tags */} - - - - - - - - - {/* Twitter Meta Tags */} - - - - - - - ); -} - -export default Metadata; diff --git a/packages/components/NavBar.tsx b/packages/components/NavBar.tsx deleted file mode 100644 index c2b8ae85..00000000 --- a/packages/components/NavBar.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useState } from "react"; - -import { Logo } from "@packages/components/icons"; -import { links } from "@packages/config/site"; - -import Link from "@packages/components/Link"; -import { LocalizedText, WithLocalizedText } from "@packages/locale/context"; -import SocialIconLinks from "./SocialIconLinks"; -import ToggleLanguage from "./ToggleLanguage"; -import ToggleThemeButton from "./ToggleThemeButton"; -import { CloseIcon } from "./icons/CloseIcon"; -import { HamburgerIcon } from "./icons/HamburgerIcon"; - -const communityLinks = [ - - - , - - - , -]; - -const actionButtons = [ - , - , - , -]; - -function NavBar() { - const [isOpen, setIsOpen] = useState(false); - const onOpen = () => setIsOpen(true); - const onClose = () => setIsOpen(false); - - return ( -
-
- - - - -

PodCodar

- - -
-
{communityLinks}
-
{actionButtons}
-
- - - {({ text }) => ( -
- -
- )} -
-
- - {isOpen ? ( -
- - -
- -
- {actionButtons} -
-
- ) : null} -
- ); -} - -export default NavBar; diff --git a/packages/components/Providers.tsx b/packages/components/Providers.tsx deleted file mode 100644 index e7d252f6..00000000 --- a/packages/components/Providers.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import I18nProvider from "@packages/locale/context"; -import { useIsClient } from "@packages/utils/react"; -import { Suspense } from "react"; -import NavBar from "./NavBar"; - -type Props = { - children: React.ReactNode; -}; - -export default function Providers({ children }: Props) { - // FIXME: handle i18n on the server side - const isClient = useIsClient(); - const fallback = ( -
-

Loading...

-
- ); - - if (!isClient) { - return fallback; - } - - return ( - - - - - {children} - - - ); -} diff --git a/packages/components/RoadmapSection.tsx b/packages/components/RoadmapSection.tsx deleted file mode 100644 index 894b1006..00000000 --- a/packages/components/RoadmapSection.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { roadMapsLinks } from "@packages/config/site"; -import { LocalizedText, type TranslationToken } from "@packages/locale/context"; -import { classes } from "@packages/utils/classes"; - -import Section from "./Section"; - -import styles from "./roadmapSection.module.css"; - -export default function RoadmapSection() { - return ( -
-
-

- -

- - - -
- -
- {cardList.map((cardProps) => ( - - ))} -
-
- ); -} - -interface CardItemProps { - title: TranslationToken; - link: string; - colorClass: string; -} - -function CardItem({ title, link, colorClass }: Readonly) { - return ( - -
-

- -

-
-
- ); -} - -const cardList: CardItemProps[] = [ - { - title: "roadmap.web-programming", - link: roadMapsLinks.webProgramming, - colorClass: styles.blue, - }, - { - title: "roadmap.ux-design", - link: roadMapsLinks.uxDesign, - colorClass: styles.orange, - }, - { - title: "roadmap.react", - link: roadMapsLinks.react, - colorClass: styles.pink, - }, - { - title: "roadmap.data", - link: roadMapsLinks.introToData, - colorClass: styles.purple, - }, -]; diff --git a/packages/components/Section.tsx b/packages/components/Section.tsx deleted file mode 100644 index d50bc813..00000000 --- a/packages/components/Section.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { classes } from "@packages/utils/classes"; -import type { ReactNode } from "react"; - -interface Props { - children: ReactNode; - className?: string; - innerClassName?: string; - id?: string; -} - -function Section({ children, className, innerClassName, id }: Props) { - return ( -
-
- {children} -
-
- ); -} - -export default Section; diff --git a/packages/components/SkeletonMemberCard.tsx b/packages/components/SkeletonMemberCard.tsx deleted file mode 100644 index a7869eb4..00000000 --- a/packages/components/SkeletonMemberCard.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export default function SkeletonMemberCard() { - return ( -
-
- -
-
-
- -
-
-
-
-
-
- ); -} diff --git a/packages/components/SocialIconLinks.tsx b/packages/components/SocialIconLinks.tsx deleted file mode 100644 index 539d3cec..00000000 --- a/packages/components/SocialIconLinks.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { GithubIcon, LinkedInIcon } from "@packages/components/icons"; -import { links } from "@packages/config/site"; - -interface Props { - githubUrl?: string; - linkedinUrl?: string; -} - -export default function SocialIconLinks({ - githubUrl = links.github, - linkedinUrl = links.linkedin, -}: Props) { - return ( - - ); -} diff --git a/packages/components/TabNav.tsx b/packages/components/TabNav.tsx deleted file mode 100644 index 96de66c0..00000000 --- a/packages/components/TabNav.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { classes } from "@packages/utils/classes"; -import { Fragment } from "react/jsx-runtime"; -import Link from "./Link"; - -type Tabs = { - id: string; - title: string; -}; - -type TabNavProps = { - tabs: Tabs[]; - children: React.ReactNode; - hidden?: boolean; - activeTab?: string; - header?: React.ReactNode; -}; - -export function TabNav(props: TabNavProps) { - const { tabs, hidden = false, activeTab = "" } = props; - - if (hidden) return props.children; - - return ( - - ); -} diff --git a/packages/components/TeamPage.tsx b/packages/components/TeamPage.tsx deleted file mode 100644 index 2544ac99..00000000 --- a/packages/components/TeamPage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import MemberCard from "@packages/components/MemberCard"; -import Section from "@packages/components/Section"; -import SkeletonMemberCard from "@packages/components/SkeletonMemberCard"; -import { LocalizedText } from "@packages/locale/context"; -import type { SelectMember } from "@packages/repositories/db/schema"; - -type Props = { - members: SelectMember[]; -}; - -export default function TeamPage({ members }: Props) { - return ( -
-
-

- , - }} - /> -

- - {members === null ? ( - - ) : members.length === 0 ? ( - - ) : ( -
- {members.map((member) => { - return ; - })} -
- )} -
-
- ); -} diff --git a/packages/components/TechSection.tsx b/packages/components/TechSection.tsx deleted file mode 100644 index c0a50729..00000000 --- a/packages/components/TechSection.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { LocalizedText } from "@packages/locale/context"; -import Section from "./Section"; -import { - CssIcon, - DockerIcon, - GitIcon, - HtmlIcon, - JavaScriptIcon, - LinuxIcon, - NextIcon, - NodeIcon, - PythonIcon, - ReactIcon, - ReduxIcon, - ShellIcon, - SqlIcon, - TypeScriptIcon, - WebPackIcon, -} from "./icons"; - -const iconList = [ - GitIcon, - HtmlIcon, - CssIcon, - JavaScriptIcon, - ReactIcon, - ReduxIcon, - TypeScriptIcon, - NodeIcon, - NextIcon, - PythonIcon, - ShellIcon, - WebPackIcon, - SqlIcon, - LinuxIcon, - DockerIcon, -]; - -function TechSection() { - return ( -
-

- -

- -
- {iconList.map((Icon) => ( - - ))} -
-
- ); -} - -export default TechSection; diff --git a/packages/components/TestimonialSection.tsx b/packages/components/TestimonialSection.tsx deleted file mode 100644 index ce9f5398..00000000 --- a/packages/components/TestimonialSection.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { LocalizedText } from "@packages/locale/context"; -import type { SelectTestimonial } from "@packages/repositories/db/schema"; - -import Section from "./Section"; - -interface Props { - testimonials: SelectTestimonial[]; -} - -export default function TestimonialSection({ testimonials }: Props) { - return ( -
-

- -

- -
- {testimonials.map(({ id, name, description, avatarUrl }, idx) => ( - - ))} -
- -
- {testimonials.map(({ id }, idx) => ( - - {idx + 1} - - ))} -
-
- ); -} - -interface TestimonialCardProps { - name: string; - testimonial: string; - img: string; - idx: number; - maxSize: number; -} - -function TestimonialCard({ - name, - testimonial, - img, - idx, - maxSize, -}: TestimonialCardProps) { - const nextIdx = idx === maxSize ? 0 : idx + 1; - const prevIdx = idx === 0 ? maxSize : idx - 1; - - return ( -
-
- {name} -
-

{name}

-

{testimonial}

-
-
- - -
- ); -} diff --git a/packages/components/ToggleLanguage.tsx b/packages/components/ToggleLanguage.tsx deleted file mode 100644 index 2f115d77..00000000 --- a/packages/components/ToggleLanguage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { BRFlagIcon, USFlagIcon } from "@packages/components/icons"; -import { useI18nActions, useI18nStates } from "@packages/locale/context"; - -export default function ToggleLanguage() { - const { locale } = useI18nStates(); - const { setLocale } = useI18nActions(); - const text = locale === "pt" ? : ; - const handleToggle = () => setLocale(locale === "en" ? "pt" : "en"); - - return ( - - ); -} diff --git a/packages/components/ToggleThemeButton.tsx b/packages/components/ToggleThemeButton.tsx deleted file mode 100644 index a1ac6241..00000000 --- a/packages/components/ToggleThemeButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { type Theme, strToTheme, toggleTheme } from "@packages/utils/theme"; -import { useState } from "react"; -import { Form } from "react-router"; - -export default function ToggleThemeButton() { - const [colorMode, setColorMode] = useState( - strToTheme(document.documentElement.dataset.theme ?? "system"), - ); - - const info = - colorMode === "system" - ? "Click to toggle between light and dark mode." - : "Right click to reset to system default."; - const label = `Selected Theme is ${colorMode}. ${info}`; - - function handleClick() { - const newTheme = toggleTheme(colorMode); - setColorMode(newTheme); - document.documentElement.dataset.theme = newTheme; - document.documentElement.classList.remove("light", "dark"); - document.documentElement.classList.add(newTheme); - } - - return ( -
- - - -
- ); -} diff --git a/packages/components/WhyItWorksSection.tsx b/packages/components/WhyItWorksSection.tsx deleted file mode 100644 index 6bc25b54..00000000 --- a/packages/components/WhyItWorksSection.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { - PersonalizedLearningIcon, - PracticalLearningIcon, - TeamworkIcon, -} from "@packages/components/icons"; -import { type I18nTextProps, LocalizedText } from "@packages/locale/context"; -import type { ReactNode } from "react"; -import Section from "./Section"; - -export default function WhyItWorksSection() { - return ( -
-

- -

-
- {cardList.map((card) => ( - - ))} -
-
- ); -} - -interface CardItemProps { - icon: ReactNode; - title: I18nTextProps["token"]; - description: I18nTextProps["token"]; -} - -function CardItem({ title, icon, description }: CardItemProps) { - return ( -
-
{icon}
-

- -

-

- -

-
- ); -} - -type Card = { - icon: ReactNode; - translation: string; -}; - -const cardList: Card[] = [ - { - icon: , - translation: "practical-learn", - }, - { - icon: , - translation: "personalized-learning", - }, - { - icon: , - translation: "teamwork", - }, -]; diff --git a/packages/components/icons/BRFlagIcon.tsx b/packages/components/icons/BRFlagIcon.tsx deleted file mode 100644 index a3269efb..00000000 --- a/packages/components/icons/BRFlagIcon.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export const BRFlagIcon = ({ size = 20 }) => { - return ( - - - - - - - ); -}; diff --git a/packages/components/icons/CloseIcon.tsx b/packages/components/icons/CloseIcon.tsx deleted file mode 100644 index c723d825..00000000 --- a/packages/components/icons/CloseIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export const CloseIcon = () => { - return ( - - - - ); -}; diff --git a/packages/components/icons/CssIcon.tsx b/packages/components/icons/CssIcon.tsx deleted file mode 100644 index 1e4366a2..00000000 --- a/packages/components/icons/CssIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const CssIcon = (props: TechIconProps) => { - return ( - - - - - - - - - ); -}; diff --git a/packages/components/icons/DockerIcon.tsx b/packages/components/icons/DockerIcon.tsx deleted file mode 100644 index 90eea730..00000000 --- a/packages/components/icons/DockerIcon.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const DockerIcon = (props: TechIconProps) => { - return ( - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/components/icons/GitIcon.tsx b/packages/components/icons/GitIcon.tsx deleted file mode 100644 index 28f70f9c..00000000 --- a/packages/components/icons/GitIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const GitIcon = (props: TechIconProps) => { - return ( - - - - ); -}; diff --git a/packages/components/icons/GithubIcon.tsx b/packages/components/icons/GithubIcon.tsx deleted file mode 100644 index 07c746bb..00000000 --- a/packages/components/icons/GithubIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const GithubIcon = () => ( - - - -); diff --git a/packages/components/icons/HamburgerIcon.tsx b/packages/components/icons/HamburgerIcon.tsx deleted file mode 100644 index 694bb363..00000000 --- a/packages/components/icons/HamburgerIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export const HamburgerIcon = () => { - return ( - - - - ); -}; diff --git a/packages/components/icons/HtmlIcon.tsx b/packages/components/icons/HtmlIcon.tsx deleted file mode 100644 index 85cd8fcf..00000000 --- a/packages/components/icons/HtmlIcon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const HtmlIcon = (props: TechIconProps) => { - return ( - - - - - - - ); -}; diff --git a/packages/components/icons/Illustration.tsx b/packages/components/icons/Illustration.tsx deleted file mode 100644 index 6cb2d633..00000000 --- a/packages/components/icons/Illustration.tsx +++ /dev/null @@ -1,348 +0,0 @@ -export const Illustration = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/components/icons/JavaScriptIcon.tsx b/packages/components/icons/JavaScriptIcon.tsx deleted file mode 100644 index a447fdcf..00000000 --- a/packages/components/icons/JavaScriptIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const JavaScriptIcon = (props: TechIconProps) => { - return ( - - - - ); -}; diff --git a/packages/components/icons/LinkedInIcon.tsx b/packages/components/icons/LinkedInIcon.tsx deleted file mode 100644 index f5a4b98b..00000000 --- a/packages/components/icons/LinkedInIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const LinkedInIcon = () => ( - - - -); diff --git a/packages/components/icons/LinuxIcon.tsx b/packages/components/icons/LinuxIcon.tsx deleted file mode 100644 index 9dfeabd3..00000000 --- a/packages/components/icons/LinuxIcon.tsx +++ /dev/null @@ -1,3120 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const LinuxIcon = (props: TechIconProps) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/components/icons/Logo.tsx b/packages/components/icons/Logo.tsx deleted file mode 100644 index f183acf6..00000000 --- a/packages/components/icons/Logo.tsx +++ /dev/null @@ -1,45 +0,0 @@ -interface LogoProps { - size?: SizeVariant; -} -type SizeVariant = "default" | "small" | "large"; -type SizeMap = Record; - -const sizeMap: SizeMap = { - small: 32, - default: 64, - large: 80, -}; - -export function Logo({ size = "default" }: LogoProps) { - const width = sizeMap[size]; - return ( - - - - - - ); -} diff --git a/packages/components/icons/NextIcon.tsx b/packages/components/icons/NextIcon.tsx deleted file mode 100644 index bd9c4a39..00000000 --- a/packages/components/icons/NextIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const NextIcon = (props: TechIconProps) => { - return ( - - - - ); -}; diff --git a/packages/components/icons/NodeIcon.tsx b/packages/components/icons/NodeIcon.tsx deleted file mode 100644 index 05f04b43..00000000 --- a/packages/components/icons/NodeIcon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const NodeIcon = (props: TechIconProps) => { - return ( - - - - - - - - - ); -}; diff --git a/packages/components/icons/PersonalizedLearningIcon.tsx b/packages/components/icons/PersonalizedLearningIcon.tsx deleted file mode 100644 index d3f2419c..00000000 --- a/packages/components/icons/PersonalizedLearningIcon.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { classes } from "@packages/utils/classes"; -import type { HTMLAttributes } from "react"; - -export function PersonalizedLearningIcon({ - className, - ...props -}: HTMLAttributes) { - return ( - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/packages/components/icons/PracticalLearningIcon.tsx b/packages/components/icons/PracticalLearningIcon.tsx deleted file mode 100644 index 8382d64e..00000000 --- a/packages/components/icons/PracticalLearningIcon.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { classes } from "@packages/utils/classes"; -import type { HTMLAttributes } from "react"; - -export function PracticalLearningIcon({ - className, - ...props -}: HTMLAttributes) { - return ( - - - - - - - - - - - - - ); -} diff --git a/packages/components/icons/PythonIcon.tsx b/packages/components/icons/PythonIcon.tsx deleted file mode 100644 index 9c4d85e1..00000000 --- a/packages/components/icons/PythonIcon.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const PythonIcon = (props: TechIconProps) => { - return ( - - - - - - - - - - - - - - - - - - ); -}; diff --git a/packages/components/icons/ReactIcon.tsx b/packages/components/icons/ReactIcon.tsx deleted file mode 100644 index 5d13abae..00000000 --- a/packages/components/icons/ReactIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const ReactIcon = (props: TechIconProps) => { - return ( - - - - - - - ); -}; diff --git a/packages/components/icons/ReduxIcon.tsx b/packages/components/icons/ReduxIcon.tsx deleted file mode 100644 index f160040e..00000000 --- a/packages/components/icons/ReduxIcon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const ReduxIcon = (props: TechIconProps) => { - return ( - - - - - ); -}; diff --git a/packages/components/icons/ShellIcon.tsx b/packages/components/icons/ShellIcon.tsx deleted file mode 100644 index 60af6fc2..00000000 --- a/packages/components/icons/ShellIcon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const ShellIcon = (props: TechIconProps) => { - return ( - - - - - - ); -}; diff --git a/packages/components/icons/SqlIcon.tsx b/packages/components/icons/SqlIcon.tsx deleted file mode 100644 index 15efdbf7..00000000 --- a/packages/components/icons/SqlIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const SqlIcon = (props: TechIconProps) => { - return ( - - - - - - ); -}; diff --git a/packages/components/icons/TeamworkIcon.tsx b/packages/components/icons/TeamworkIcon.tsx deleted file mode 100644 index 45930c36..00000000 --- a/packages/components/icons/TeamworkIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { classes } from "@packages/utils/classes"; -import type { HTMLAttributes } from "react"; - -export function TeamworkIcon({ - className, - ...props -}: HTMLAttributes) { - return ( - - - - - - - - - - - - ); -} diff --git a/packages/components/icons/TechIcon.tsx b/packages/components/icons/TechIcon.tsx deleted file mode 100644 index b71596f4..00000000 --- a/packages/components/icons/TechIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { HTMLAttributes } from "react"; - -export type TechIconProps = HTMLAttributes & { - title: string; -}; - -export const TechIcon = ({ title, ...props }: TechIconProps) => { - return ( - - ); -}; diff --git a/packages/components/icons/TypeScriptIcon.tsx b/packages/components/icons/TypeScriptIcon.tsx deleted file mode 100644 index af130689..00000000 --- a/packages/components/icons/TypeScriptIcon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const TypeScriptIcon = (props: TechIconProps) => { - return ( - - - - ); -}; diff --git a/packages/components/icons/USFlagIcon.tsx b/packages/components/icons/USFlagIcon.tsx deleted file mode 100644 index 338b7d19..00000000 --- a/packages/components/icons/USFlagIcon.tsx +++ /dev/null @@ -1,29 +0,0 @@ -export const USFlagIcon = ({ size = 20 }) => { - return ( - - - - - - - ); -}; diff --git a/packages/components/icons/WebPackIcon.tsx b/packages/components/icons/WebPackIcon.tsx deleted file mode 100644 index 7e1aae05..00000000 --- a/packages/components/icons/WebPackIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { TechIcon, type TechIconProps } from "./TechIcon"; - -export const WebPackIcon = (props: TechIconProps) => { - return ( - - - - - ); -}; diff --git a/packages/components/icons/index.tsx b/packages/components/icons/index.tsx deleted file mode 100644 index c83888ff..00000000 --- a/packages/components/icons/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export * from "./GithubIcon"; -export * from "./Illustration"; -export * from "./LinkedInIcon"; -export * from "./Logo"; -export * from "./PersonalizedLearningIcon"; -export * from "./PracticalLearningIcon"; -export * from "./TeamworkIcon"; -export * from "./NodeIcon"; -export * from "./TypeScriptIcon"; -export * from "./DockerIcon"; -export * from "./GitIcon"; -export * from "./LinuxIcon"; -export * from "./NodeIcon"; -export * from "./PythonIcon"; -export * from "./ReactIcon"; -export * from "./ReduxIcon"; -export * from "./ShellIcon"; -export * from "./SqlIcon"; -export * from "./WebPackIcon"; -export * from "./NextIcon"; -export * from "./JavaScriptIcon"; -export * from "./CssIcon"; -export * from "./HtmlIcon"; -export * from "./USFlagIcon"; -export * from "./BRFlagIcon"; diff --git a/packages/components/roadmapSection.module.css b/packages/components/roadmapSection.module.css deleted file mode 100644 index 09b21f42..00000000 --- a/packages/components/roadmapSection.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.blue { - --color: #17a9bc; -} - -.orange { - --color: #f99223; -} - -.pink { - --color: #ff4cff; -} - -.purple { - --color: #b794f4; -} - -.card { - border-color: var(--color); -} - -.card:hover { - box-shadow: -8px 8px 0 var(--color); -} diff --git a/packages/config/site.ts b/packages/config/site.ts deleted file mode 100644 index 8969f274..00000000 --- a/packages/config/site.ts +++ /dev/null @@ -1,41 +0,0 @@ -export const title = "PodCodar"; -export const pageURL = "https://podcodar.com"; - -export const TRANSPARENCY_FOLDER_ID = "1lNvrSfyhmpV4mgMbj4Vic72qEfe_r6my"; -export const IFRAME_FORM_URL = - "https://docs.google.com/forms/d/e/1FAIpQLSeoy7Jg_LaXsJMDYJ1gXKBRPu4tIdQiPBG5ZmLennVKSb_GVg/viewform?embedded=true"; - -export const PIX_KEY = "podcodar@gmail.com"; -// TODO: translate site description -export const description = - "A PodCodar é uma comunidade colaborativa onde profissionais digitais podem se conectar, compartilhar conhecimento e trabalhar em projetos reais."; - -export const images = { - icon: "/images/favicon.svg", - logo: "/images/favicon.svg", - og: "https://podcodar.org/images/og-512.png", - pixQRCode: "/images/pix-qrcode.png", -}; - -export const links = { - // community - team: "/team", - transparency: "/transparency", - secondaryButton: "#why-it-works", - - // social - github: "https://github.com/podcodar", - linkedin: "https://www.linkedin.com/company/podcodar/", -}; - -export const roadMapsLinks = { - all: "https://www.notion.so/podcodar/Trilhas-de-estudo-eb8954febc0243b681ead5d417cca67b", - webProgramming: - "https://www.notion.so/podcodar/Programa-o-Web-0a244ea5a20f4b73b2c706141f7a4919", - uxDesign: - "https://www.notion.so/podcodar/UX-Design-f159ae7615d94d99b7a90675f608788a", - react: - "https://www.notion.so/Em-Constru-o-Programa-o-Web-React-15026eb75eae4dfc8bcb796eb5ef7c7f", - introToData: - "https://www.notion.so/Introdu-o-Dados-ddc04fddf7ee495a8b697263e75e9477", -}; diff --git a/packages/contants.ts b/packages/contants.ts deleted file mode 100644 index ee2a7d87..00000000 --- a/packages/contants.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const MAX_COOKIE_AGE = 2147483647; - -export const SITEMAP_URL = "https://podcodar.fly.dev/sitemap.xml"; - -export const SITEMAP_URLS = [ - { - loc: "https://podcodar.fly.dev/", - lastmod: "2024-12-26T09:10:00-03:00", - priority: 1.0, - }, - { - loc: "https://podcodar.fly.dev/team/", - lastmod: "2024-12-26T09:10:00-03:00", - priority: 1.0, - }, -]; - -export const ADMIN_ROUTES = { - // auth - signOut: "/admin/auth/logout", - refresh: "/admin/auth/refresh", - callback: "/admin/auth/callback", - // pages - signIn: "/admin/login", - dashboard: "/admin/dashboard", - members: "/admin/members", - testimonials: "/admin/testimonials", -} as const; - -const VALID_PERSONAL_EMAILS = [ - "ma.souza.junior@gmail.com", - "pfrattezi@gmail.com", -] as const; - -export const VALID_EMAILS = [ - // all emails ending with @podcodar.org - new RegExp(/@podcodar\.org$/), - // personal emails - new RegExp(`(${VALID_PERSONAL_EMAILS.join("|")})$`), -]; diff --git a/packages/locale/context.tsx b/packages/locale/context.tsx deleted file mode 100644 index b84f9d52..00000000 --- a/packages/locale/context.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import i18next, { type TOptions } from "i18next"; -import { type ReactNode, useMemo, useState } from "react"; -import { Trans, initReactI18next, useTranslation } from "react-i18next"; - -import en from "@packages/locale/en.yml"; -import pt from "@packages/locale/pt.yml"; - -import { type ChildrenProps, useEffectOnce } from "@packages/utils/react"; -import { createCtx } from "@packages/utils/react"; - -const DEFAULT_LOCALE: Locale = "pt"; -const LOCAL_STORAGE_KEY = "podcodar:locale"; - -interface I18nActions { - readonly setLocale: (locale: Locale) => void; -} - -interface I18nStates { - readonly locale: Locale; -} - -const [useI18nActions, I18nActionsProvider] = - createCtx("I18nActionsCtx"); -const [useI18nStates, I18nStatesProvider] = - createCtx("I18nStatesCtx"); - -export { useI18nActions, useI18nStates }; - -i18next.use(initReactI18next).init({ - lng: DEFAULT_LOCALE, - fallbackLng: DEFAULT_LOCALE, - resources: { en, pt }, - interpolation: { - escapeValue: false, - }, -}); - -export default function I18nProvider({ children }: ChildrenProps) { - const [locale, setLocale] = useState(DEFAULT_LOCALE); - - const state: I18nStates = useMemo(() => ({ locale }), [locale]); - const actions: I18nActions = useMemo( - () => ({ - setLocale: (locale) => { - setLocale(locale); - i18next.changeLanguage(locale); - localStorage.setItem(LOCAL_STORAGE_KEY, locale); - }, - }), - [], - ); - - useEffectOnce(() => { - // we need to put it into a effect to access local storage - const locale = - (localStorage.getItem(LOCAL_STORAGE_KEY) as Locale) ?? DEFAULT_LOCALE; - actions.setLocale(locale); - }); - - return ( - - {children} - - ); -} - -type Locale = "en" | "pt"; - -export type TranslationNS = - | "common" - | "call-to-action" - | "navbar" - | "why-it-works" - | "mentoring" - | "roadmap" - | "social-links" - | "footer" - | "team-page" - | "ask-us-page" - | "testimonials" - | "not-found" - | "transparency-portal"; - -export type TranslationToken = `${TranslationNS}.${string}`; - -export type I18nTextProps = { - token: TranslationToken; - params?: TOptions; - components?: { readonly [tagName: string]: React.ReactElement }; -}; - -export function LocalizedText({ - token, - params = {}, - components = {}, -}: I18nTextProps) { - const [ns, ...tokens] = token.split("."); - const localizedToken = tokens.join("."); - const { t } = useTranslation(ns); - return ; -} - -type WithLocalizedTextProps = I18nTextProps & { - children?: (props: { text: string }) => ReactNode; -}; - -export function WithLocalizedText({ - token, - children, - params = {}, -}: WithLocalizedTextProps) { - const [ns, ...tokens] = token.split("."); - const localizedToken = tokens.join("."); - const { t } = useTranslation(ns); - if (!children) return t(localizedToken, params); - - return children({ text: t(localizedToken, params) }); -} diff --git a/packages/locale/en.yml b/packages/locale/en.yml deleted file mode 100644 index 9600c1de..00000000 --- a/packages/locale/en.yml +++ /dev/null @@ -1,103 +0,0 @@ -navbar: - join: Join - join.tooltip: Registrations paused. More information coming soon! - team: Team - transparency: Transparency - -call-to-action: - title: The community where quality and opportunity meet! - description: >- - PodCodar is a collaborative community where digital professionals can connect, share knowledge and work on real projects. - primary-button: Support this idea 💡 - secondary-button: Learn more - -why-it-works: - title: And why it works? - practical-learn.title: Learn by practicing - practical-learn.description: > - We use market best practices to accelerate your professional development. - personalized-learning.title: Personalized Learning - personalized-learning.description: > - Each individual is unique and therefore it is necessary that - the learning process is shaped according to their needs. - teamwork.title: Teamwork - teamwork.description: > - Participate in study mentorships and group projects. You choose how to contribute to the community. - -mentoring: - title: Our methodology - description: > - It is structured by mentorships that are guided by paths according - to the needs and time of each person, focusing on access to the job market. - study-mentorships.title: Study mentorships - study-mentorships.description: > - Our study mentorship aims to help new learners at the beginning of their journey as a software developer. By following predefined knowledge paths with the help of a mentor - project-mentoring.title: Project mentoring - project-mentoring.description: > - It is focused on developing internal or external projects. The mentee experiences teamwork, - pair programmings and code reviews, with the main objective of simulating the day to day developer experience. - market-mentoring.title: Market mentoring - market-mentoring.description: > - Market mentoring aims to help the mentee in the professional allocation and provide support to the mentee after employment. - -testimonials: - title: Testimonials - add-testimonial-title: Add Testimonial - label.name: Name - label.github: Github username - label.testimonial: Testimonial - label.error: "{{label}} must be between {{min}} and {{max}} characters" - toast.inputError: Please fill the form - toast.invalidUserError: Invalid github user - toast.serverError: Internal Server Error - toast.success: Success - submit: Submit - -roadmap: - title: Check our roadmaps - see-all: See all - web-programming: Web Programming - ux-design: UX Design - react: React - data: Intro to data - tech-title: Technologies - -footer: - contribution: Contribute with us - podcodar: PodCodar - legal: © {{currentYear}} All rights reserved - socials: Socials - support: Support - terms: Terms of Service - privacy: Privacy Policy - -social-links: - github: Github - linkedin: LinkedIn - -team-page: - title: Community People - no-items: No item found :( - add-member-title: Add a new member - role.engineer: Software Engineer - role.mentor: Mentor - role.mentored: Mentee - label.github: Github username - label.linkedin: LinkedIn username - label.role: Community Role - toast.success: We've added {{username}} as a PodCodar member! - submit: Submit - -ask-us-page: - head: Ask us Anything - title: Ask us Anything - text-placeholder: Enter a question here... - send-button: Ask - checkbox-label: Show answered questions - answer-button-label: Mask as answered - -transparency-portal: - title: Transparency Portal - -not-found: - title: Oops, we couldn't find this page diff --git a/packages/locale/pt.yml b/packages/locale/pt.yml deleted file mode 100644 index f5c261fd..00000000 --- a/packages/locale/pt.yml +++ /dev/null @@ -1,106 +0,0 @@ -navbar: - join: Entrar - join.tooltip: Incrições pausadas. Em breve, mais informações! - team: Equipe - transparency: Transparência - -call-to-action: - title: A comunidade onde qualidade e oportunidade se encontram! - description: >- - A PodCodar é uma comunidade colaborativa onde profissionais digitais podem se conectar, compartilhar conhecimento e trabalhar em projetos reais. - primary-button: Apoie essa ideia 💡 - secondary-button: Saiba mais - -why-it-works: - title: E por quê funciona? - practical-learn.title: Aprendizado na prática - practical-learn.description: > - Utilizamos práticas do mercado de trabalho para acelerar seu - desenvolvimento profissional. - personalized-learning.title: Ensino personalizado - personalized-learning.description: > - Cada indivíduo é único e por isso é necessário que o processo - de aprendizado seja moldado de acordo com as suas necessidades. - teamwork.title: Trabalho em equipe - teamwork.description: > - Participe das mentorias de trilhas e projetos em grupo. É você quem escolhe como contribuir com a comunidade. - -mentoring: - title: Nossa metodologia - description: > - É estruturada por mentorias que são direcionadas por trilhas - conforme a necessidade e o tempo de cada pessoa, focando no acesso - ao mercado de trabalho. - study-mentorships.title: Mentoria de trilha - study-mentorships.description: > - A mentoria de trilha tem como objetivo guiar o mentorando nos seus estudos inicias através de trilhas introdutórias e exercícios especializados. - project-mentoring.title: Mentoria de projetos - project-mentoring.description: > - É focada em desenvolver projetos internos ou externos. O mentorando vivencia trabalho - em equipe, pair programmings e code reviews, com o objetivo principal de simular o dia - a dia do desenvolvedor. - market-mentoring.title: Mentoria de mercado - market-mentoring.description: > - A mentoria de mercado tem como objetivo ajudar o mentorando na alocação profissional e prover suporte ao mesmo quando estiver empregado. - -roadmap: - title: Conheça nossas trilhas - see-all: Ver todas - web-programming: Programação Web - ux-design: UX Design - react: React - data: Introdução à dados - tech-title: Tecnologias - -testimonials: - title: Testemunhos - add-testimonial-title: Adicione um Testemunho - label.name: Nome - label.github: Usuário do Github - label.testimonial: Testemunho - label.error: "{{label}} deve ter entre {{min}} e {{max}} caracteres" - toast.inputError: Por favor preencha os campos - toast.invalidUserError: Usuário do github invalido - toast.serverError: Erro de Servidor - toast.success: Sucesso - submit: Enviar - -footer: - contribution: Contribua Conosco - podcodar: PodCodar - legal: © {{currentYear}} Todos os direitos reservados - socials: Redes - support: Suporte - terms: Termos de serviço - privacy: Política de privacidade - -social-links: - github: Github - linkedin: LinkedIn - -team-page: - title: Pessoas da comunidade - no-items: Nenhum item encontrado :( - add-member-title: Adicione um novo membro - role.engineer: Engenheiro de Software - role.mentor: Mentor - role.mentored: Mentorado - label.github: Usuário do Github - label.linkedin: Usuário do LinkedIn - label.role: Papel na comunidade - toast.success: Nós adicionamos {{username}} como membro da PodCodar! - submit: Enviar - -ask-us-page: - head: Nos pergunte - title: Nos pergunte qualquer coisa - text-placeholder: Adicione uma pergunta aqui... - send-button: Perguntar - checkbox-label: Mostrar perguntas respondidas - answer-button-label: Marcar como respondida - -transparency-portal: - title: Portal de Transparência - -not-found: - title: Oops, não encontramos sua página diff --git a/packages/locale/types.d.ts b/packages/locale/types.d.ts deleted file mode 100644 index 2e8eb9e5..00000000 --- a/packages/locale/types.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module "@packages/locale/pt.yml"; -declare module "@packages/locale/en.yml"; diff --git a/packages/repositories/db/index.ts b/packages/repositories/db/index.ts deleted file mode 100644 index 70f983a9..00000000 --- a/packages/repositories/db/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createClient } from "@libsql/client"; -import { config } from "dotenv"; -import { drizzle } from "drizzle-orm/libsql"; -import type { AppLoadContext } from "react-router"; -import { membersTable, testimonialsTable } from "./schema"; - -config({ path: ".env" }); // or .env.local - -export class Database { - private db: ReturnType; - - constructor( - url = process.env.TURSO_CONNECTION_URL ?? "", - authToken = process.env.TURSO_AUTH_TOKEN ?? "", - ) { - const client = createClient({ url, authToken }); - this.db = drizzle(client); - } - - get testimonials() { - return this.db.select().from(testimonialsTable); - } - - get members() { - return this.db.select().from(membersTable); - } -} - -export function getDatabase(context: AppLoadContext) { - return new Database( - context.cloudflare.env.TURSO_CONNECTION_URL, - context.cloudflare.env.TURSO_AUTH_TOKEN, - ); -} diff --git a/packages/repositories/db/schema.ts b/packages/repositories/db/schema.ts deleted file mode 100644 index a0e54f4e..00000000 --- a/packages/repositories/db/schema.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; - -export const testimonialsTable = sqliteTable("testimonials", { - id: integer("id").primaryKey(), - name: text("name").notNull(), - avatarUrl: text("avatarUrl").notNull(), - profileUrl: text("profileUrl").notNull(), - description: text("description").notNull(), -}); - -export type InsertTestimonial = typeof testimonialsTable.$inferInsert; -export type SelectTestimonial = typeof testimonialsTable.$inferSelect; - -export const membersTable = sqliteTable("members", { - id: integer("id").primaryKey(), - name: text("name").notNull(), - role: text("role").notNull(), - avatar: text("avatar").notNull(), - cover: text("cover").notNull(), - github: text("github").notNull(), - linkedin: text("linkedin").notNull(), -}); - -export type InsertMember = typeof membersTable.$inferInsert; -export type SelectMember = typeof membersTable.$inferSelect; diff --git a/packages/services/auth.server.ts b/packages/services/auth.server.ts deleted file mode 100644 index 08aa1519..00000000 --- a/packages/services/auth.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { MAX_COOKIE_AGE } from "@packages/contants"; -import { createCookie } from "react-router"; - -export const authCookie = createCookie("auth-cookie", { - path: "/", - maxAge: MAX_COOKIE_AGE, -}); - -export const refreshCookie = createCookie("refresh-cookie", { - path: "/", - maxAge: MAX_COOKIE_AGE, -}); - -export async function hasValidSession(request: Request) { - const cookieHeader = request.headers.get("Cookie"); - const [authToken, refreshToken] = await Promise.all([ - authCookie.parse(cookieHeader), - refreshCookie.parse(cookieHeader), - ]); - - return authToken && refreshToken; -} diff --git a/packages/services/auth.ts b/packages/services/auth.ts deleted file mode 100644 index 02a1a9f2..00000000 --- a/packages/services/auth.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GitHubAuth } from "@m3o/auth"; -import { ADMIN_ROUTES } from "@packages/contants"; -import { raise } from "@packages/utils/typescript"; -import type { AppLoadContext } from "react-router"; - -export function getAuth(context: AppLoadContext): GitHubAuth { - const auth: GitHubAuth = new GitHubAuth({ - scope: "read:user user:email", - client_id: - context.cloudflare.env.GITHUB_CLIENT_ID ?? - raise("GITHUB_CLIENT_ID not found"), - client_secret: - context.cloudflare.env.GITHUB_CLIENT_SECRET ?? - raise("GITHUB_CLIENT_SECRET not found"), - }); - - auth.setUrls(ADMIN_ROUTES); - - return auth; -} diff --git a/packages/services/logger.ts b/packages/services/logger.ts deleted file mode 100644 index f11fc315..00000000 --- a/packages/services/logger.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as log from "@std/log"; - -const level = process.env.LOG_LEVEL || "DEBUG"; - -log.setup({ - handlers: { - default: new log.ConsoleHandler(level as log.LevelName, { - useColors: false, - }), - }, -}); - -export const logger = log.getLogger(); diff --git a/packages/utils/classes.test.ts b/packages/utils/classes.test.ts deleted file mode 100644 index 9d3a40fb..00000000 --- a/packages/utils/classes.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { expect, test } from "bun:test"; -import { classes } from "./classes"; - -test("test_with_empty_arguments", () => { - const input: string[] = []; - - const expected = ""; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_valid_argument", () => { - const input: string[] = ["bg-red-500"]; - - const expected = "bg-red-500"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_multiple_valid_arguments", () => { - const input: string[] = ["bg-red-500", "text-white"]; - - const expected = "bg-red-500 text-white"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_dynamic_argument", () => { - const darkMode = Math.random() > 0.5 ? "dark" : "light"; - const input: string[] = ["bg-red-500", darkMode]; - - const expected = `bg-red-500 ${darkMode}`; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_non_string_argument", () => { - const input = ["bg-red-500", false as const]; - const expected = "bg-red-500"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_null_and_undefined_arguments", () => { - const input = ["bg-red-500", null, undefined]; - - const expected = "bg-red-500"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_empty_string_argument", () => { - const input: string[] = ["bg-red-500", ""]; - - const expected = "bg-red-500"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_mixed_arguments", () => { - const input = ["bg-red-500", "", null, undefined, "text-white"]; - const expected = "bg-red-500 text-white"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_only_invalid_arguments", () => { - const input = [null, undefined, false as const, ""]; - const expected = ""; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_duplicate_arguments", () => { - const input: string[] = ["bg-red-500", "bg-red-500"]; - - const expected = "bg-red-500"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_duplicated_classes_in_combined_strings", () => { - const input: string[] = ["bg-red-500 w-4", "bg-red-500 px-3"]; - - const expected = "bg-red-500 w-4 px-3"; - - expect(classes(...input)).toBe(expected); -}); - -test("test_with_conflicting_tokens", () => { - const input: string[] = ["bg-red-900 px-4", "bg-red-500 px-3"]; - - const expected = "bg-red-500 px-3"; - - expect(classes(...input)).toBe(expected); -}); diff --git a/packages/utils/classes.ts b/packages/utils/classes.ts deleted file mode 100644 index 9a5acf42..00000000 --- a/packages/utils/classes.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Optional } from "./types"; - -/** - * Generates a string of unique CSS classes from the provided list, - * filtering out non-string values and empty strings. - * - * Ensures that the output contains only valid, non-empty class names, - * and removes any duplicates. - * - * @param {...OptionalString[]} classList - A variable number of CSS class names. - * - * @return {string} A string of unique, valid CSS classes, separated by spaces. - * - * @example classes("bg-red-500", "text-white"); - * >>> "bg-red-500 text-white" - * - * @example classes("bg-red-500", "bg-red-500"); - * >>> "bg-red-500" - * - * @example classes("bg-red-500", "", null, undefined, "text-white", 42); - * >>> "bg-red-500 text-white" - * - * @example classes("bg-red-500 w-4", "bg-red-500 px-3"); - * >>> "bg-red-500 w-4 px-3" - */ -export function classes(...classList: Optional[]): string { - const tokenMap = new Map(); - - for (const className of classList) { - if (!className || className.trim().length === 0) continue; - - for (const token of className.split(" ")) { - const tokenKey = token.replaceAll(/\d*/g, ""); - tokenMap.set(tokenKey, token); - } - } - - const validClasses = Array.from(tokenMap.values()); - if (validClasses.length === 0) return ""; - - return validClasses.join(" "); -} diff --git a/packages/utils/react.tsx b/packages/utils/react.tsx deleted file mode 100644 index b35b3c54..00000000 --- a/packages/utils/react.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import react from "react"; - -export interface ChildrenProps { - readonly children: react.ReactNode; -} - -export function useEffectOnce(effect: react.EffectCallback) { - // eslint-disable-next-line react-hooks/exhaustive-deps - react.useEffect(effect, []); -} - -export type SafeContextResult = [ - () => T, - react.Provider, - react.Context, -]; - -/** - * A helper that creates a Context Consumer and Provider without having to declare a - * default value and checking nullable. - * - * @example - * export const [useAuth, AuthProvider] = createSafeContext('AuthCtx') - * - * @param displayName Context name displayed in React Component's Tree - * @returns A context consumer, the context provider and the context itself - */ -export function createCtx( - displayName: Readonly, -): SafeContextResult { - const Ctx = react.createContext(null); - Ctx.displayName = displayName; - - function useCtx() { - const value = react.useContext(Ctx); - if (value === null) { - throw new Error( - `Missing ${displayName} context provider upwards on this tree`, - ); - } - - return value; - } - - return [useCtx, Ctx.Provider, Ctx]; -} - -export function useIsClient() { - const [isClient, setIsClient] = react.useState(false); - - useEffectOnce(() => { - setIsClient(true); - }); - - return isClient && typeof window !== "undefined"; -} diff --git a/packages/utils/theme.test.ts b/packages/utils/theme.test.ts deleted file mode 100644 index 676428a3..00000000 --- a/packages/utils/theme.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect, test } from "bun:test"; -import * as themeModule from "./theme"; - -// mock document -const setDatasetTheme = (theme: string) => { - (globalThis.document as object) = { - documentElement: { - dataset: { - theme, - }, - }, - }; -}; - -// mock document -const setSystemTheme = (isDark: boolean) => { - (globalThis.window as object) = { - matchMedia: () => ({ - matches: isDark, - }), - }; -}; - -test("test_theme_module", () => { - const theme = themeModule.strToTheme("dark"); - expect(theme).toBe("dark"); -}); - -test("test_theme_module_with_invalid_theme", () => { - const theme = themeModule.strToTheme("invalid"); - expect(theme).toBe("system"); -}); - -test("test_get_selected_theme", () => { - setDatasetTheme("system"); - setSystemTheme(false); - - let theme = themeModule.getSelectedTheme(); - expect(theme).toBe("light"); - - setSystemTheme(true); - theme = themeModule.getSelectedTheme(); - expect(theme).toBe("dark"); - - setDatasetTheme("light"); - theme = themeModule.getSelectedTheme(); - expect(theme).toBe("light"); - - setDatasetTheme("dark"); - theme = themeModule.getSelectedTheme(); - expect(theme).toBe("dark"); -}); - -test("test_toggle_theme", () => { - let theme = themeModule.toggleTheme("light"); - expect(theme).toBe("dark"); - - theme = themeModule.toggleTheme("dark"); - expect(theme).toBe("light"); - - theme = themeModule.toggleTheme("system"); - expect(theme).toBe("light"); -}); diff --git a/packages/utils/theme.ts b/packages/utils/theme.ts deleted file mode 100644 index a1217c4c..00000000 --- a/packages/utils/theme.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const THEME_OPTIONS = ["light", "dark", "system"] as const; - -export type Theme = (typeof THEME_OPTIONS)[number]; - -export function strToTheme(str: string): Theme { - if (THEME_OPTIONS.includes(str as Theme)) { - return str as Theme; - } - - return "system"; -} - -export function getSelectedTheme() { - let selectedTheme = document.documentElement.dataset.theme; - if (!selectedTheme || selectedTheme === "system") { - selectedTheme = window.matchMedia("(prefers-color-scheme: dark)")?.matches - ? "dark" - : "light"; - } - - return strToTheme(selectedTheme); -} - -export function toggleTheme(selectedTheme: Theme) { - return selectedTheme === "light" ? "dark" : "light"; -} diff --git a/packages/utils/types.ts b/packages/utils/types.ts deleted file mode 100644 index cb27cead..00000000 --- a/packages/utils/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type Optional = T | false | undefined | null; diff --git a/packages/utils/typescript.test.ts b/packages/utils/typescript.test.ts deleted file mode 100644 index 481f5e18..00000000 --- a/packages/utils/typescript.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect, test } from "bun:test"; -import { raise } from "./typescript"; - -test("test_raise_an_error", () => { - const message = "This is an error message"; - - expect(() => raise(message)).toThrowError(message); -}); - -test("test_raise_as_fallback", () => { - const message = "This is an error message"; - - const retursnNull = () => null; - expect(() => retursnNull() ?? raise(message)).toThrowError(message); - - expect( - () => process.env.THIS_VAR_DOESNT_EXIST ?? raise(message), - ).toThrowError(message); -}); - -test("test_not_raising_if_valid_code", () => { - process.env.MY_TESTING_VAR = "test"; - - expect(() => process.env.MY_TESTING_VAR ?? raise("error")).not.toThrow(); -}); diff --git a/packages/utils/typescript.ts b/packages/utils/typescript.ts deleted file mode 100644 index 29f25241..00000000 --- a/packages/utils/typescript.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function raise(message: string): never { - throw new Error(message); -} diff --git a/playwright.config.ts b/playwright.config.ts index b7b79691..e4054c5b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,41 +1,45 @@ -import { defineConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from '@playwright/test'; + +const targetUrl = 'http://localhost:4321'; -/** - * See https://playwright.dev/docs/test-configuration. - */ export default defineConfig({ - testDir: "./tests", - /* Run tests in files in parallel */ + // Look for test files in the "tests" directory, relative to this configuration file. + testDir: 'e2e', + + // Run all tests in parallel. fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ + + // Fail the build on CI if you accidentally left test.only in the source code. forbidOnly: !!process.env.CI, - /* Retry on CI only */ + + // Retry on CI only. retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ + + // Opt out of parallel tests on CI. workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", - }, - /* Configure projects for major browsers */ - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, + // Reporter to use + reporter: 'html', - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, + use: { + // Base URL to use in actions like `await page.goto('/')`. + baseURL: targetUrl, + // Collect trace when retrying the failed test. + trace: 'on-first-retry', + }, + // Configure projects for major browsers. + projects: [ { - name: "webkit", - use: { ...devices["Desktop Safari"] }, + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, }, ], + + // Run your local dev server before starting the tests. + webServer: { + command: 'bun run preview', + url: targetUrl, + reuseExistingServer: !process.env.CI, + }, }); diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 6ba14df9..00000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index dfd2a7c6..00000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/images/covers/lines.png b/public/images/covers/lines.png deleted file mode 100644 index 8dcd08f7..00000000 Binary files a/public/images/covers/lines.png and /dev/null differ diff --git a/public/images/covers/main.png b/public/images/covers/main.png deleted file mode 100644 index b1bad14b..00000000 Binary files a/public/images/covers/main.png and /dev/null differ diff --git a/public/images/covers/party.png b/public/images/covers/party.png deleted file mode 100644 index 3434bdee..00000000 Binary files a/public/images/covers/party.png and /dev/null differ diff --git a/public/images/favicon.svg b/public/images/favicon.svg deleted file mode 100644 index 67eb270a..00000000 --- a/public/images/favicon.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/public/images/just-llama.png b/public/images/just-llama.png deleted file mode 100644 index de3cbeb6..00000000 Binary files a/public/images/just-llama.png and /dev/null differ diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 00000000..70cb589c --- /dev/null +++ b/public/images/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/og-512.png b/public/images/og-512.png deleted file mode 100644 index 67056376..00000000 Binary files a/public/images/og-512.png and /dev/null differ diff --git a/public/images/og.png b/public/images/og.png deleted file mode 100644 index 1e7ad652..00000000 Binary files a/public/images/og.png and /dev/null differ diff --git a/public/images/pattern.svg b/public/images/pattern.svg new file mode 100644 index 00000000..8b04aee9 --- /dev/null +++ b/public/images/pattern.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/pix-qrcode.png b/public/images/pix-qrcode.png deleted file mode 100644 index 16fcc6b7..00000000 Binary files a/public/images/pix-qrcode.png and /dev/null differ diff --git a/public/js/konami.js b/public/js/konami.js deleted file mode 100644 index e4195f23..00000000 --- a/public/js/konami.js +++ /dev/null @@ -1,25 +0,0 @@ -const konamiCode = [ - "ArrowUp", - "ArrowUp", - "ArrowDown", - "ArrowDown", - "ArrowLeft", - "ArrowRight", - "ArrowLeft", - "ArrowRight", - "a", - "b", -]; -let konamiCodePosition = 0; - -window.addEventListener("keydown", (e) => { - if (e.key === konamiCode[konamiCodePosition]) { - konamiCodePosition++; - if (konamiCodePosition === konamiCode.length) { - window.location.href = "/admin"; - konamiCodePosition = 0; - } - } else { - konamiCodePosition = 0; - } -}); diff --git a/public/logo-dark.png b/public/logo-dark.png deleted file mode 100644 index b24c7aee..00000000 Binary files a/public/logo-dark.png and /dev/null differ diff --git a/public/logo-light.png b/public/logo-light.png deleted file mode 100644 index 4490ae79..00000000 Binary files a/public/logo-light.png and /dev/null differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..26e06b5f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/react-router.config.ts b/react-router.config.ts deleted file mode 100644 index 6ff16f91..00000000 --- a/react-router.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Config } from "@react-router/dev/config"; - -export default { - // Config options... - // Server-side render by default, to enable SPA mode set this to `false` - ssr: true, -} satisfies Config; diff --git a/src/assets/blog-placeholder-1.jpg b/src/assets/blog-placeholder-1.jpg new file mode 100644 index 00000000..74d4009b Binary files /dev/null and b/src/assets/blog-placeholder-1.jpg differ diff --git a/src/assets/blog-placeholder-2.jpg b/src/assets/blog-placeholder-2.jpg new file mode 100644 index 00000000..c4214b0e Binary files /dev/null and b/src/assets/blog-placeholder-2.jpg differ diff --git a/src/assets/blog-placeholder-3.jpg b/src/assets/blog-placeholder-3.jpg new file mode 100644 index 00000000..fbe2ac0c Binary files /dev/null and b/src/assets/blog-placeholder-3.jpg differ diff --git a/src/assets/blog-placeholder-4.jpg b/src/assets/blog-placeholder-4.jpg new file mode 100644 index 00000000..f4fc88e2 Binary files /dev/null and b/src/assets/blog-placeholder-4.jpg differ diff --git a/src/assets/blog-placeholder-5.jpg b/src/assets/blog-placeholder-5.jpg new file mode 100644 index 00000000..c5646746 Binary files /dev/null and b/src/assets/blog-placeholder-5.jpg differ diff --git a/src/assets/blog-placeholder-about.jpg b/src/assets/blog-placeholder-about.jpg new file mode 100644 index 00000000..cf5f6853 Binary files /dev/null and b/src/assets/blog-placeholder-about.jpg differ diff --git a/src/assets/fonts/atkinson-bold.woff b/src/assets/fonts/atkinson-bold.woff new file mode 100644 index 00000000..e7f8977e Binary files /dev/null and b/src/assets/fonts/atkinson-bold.woff differ diff --git a/src/assets/fonts/atkinson-regular.woff b/src/assets/fonts/atkinson-regular.woff new file mode 100644 index 00000000..bbe09c58 Binary files /dev/null and b/src/assets/fonts/atkinson-regular.woff differ diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro new file mode 100644 index 00000000..d300ae6b --- /dev/null +++ b/src/components/BaseHead.astro @@ -0,0 +1,54 @@ +--- +// Global CSS is pulled in here so it ships with every layout that uses BaseHead. +import '@/styles/global.css'; +import { Font } from 'astro:assets'; +import type { ImageMetadata } from 'astro'; +import PageMetadata from '@/components/metadata/PageMetadata.astro'; +import { SITE_TITLE } from '@/consts'; + +interface Props { + title: string; + description: string; + image?: ImageMetadata; +} + +const { title, description, image } = Astro.props; + +/** Root-relative URLs that respect `base` (e.g. GitHub Pages project sites). */ +const withBase = (path: string) => { + const base = import.meta.env.BASE_URL.replace(/\/$/, ''); + return `${base}/${path.replace(/^\//, '')}`; +}; + +/** + * Absolute asset URL in production (nested routes like /blog/...). + * In dev, keep root-relative so assets load from the dev server. + */ +const assetHref = (path: string) => { + const rel = withBase(path); + if (import.meta.env.DEV || !Astro.site) return rel; + try { + return new URL(rel, Astro.site).href; + } catch { + return rel; + } +}; + +const logoIconHref = assetHref('images/logo.svg'); +--- + + + + + + + + + + + diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 00000000..34be4282 --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,38 @@ +--- +import { Icon } from 'astro-icon/components'; +import Logo from '@/components/Logo.astro'; +import { footerSocialIconify, footerSocialLinks } from '@/data/social-links'; +import { getLangFromUrl, useTranslations } from '@/i18n/utils'; + +const today = new Date(); +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); +--- + +
+
+ +
+

© {today.getFullYear()} PodCodar. {t('footer.copyright')}

+
+ { + footerSocialLinks.map((link) => ( + + {link.label} + + )) + } +
+
diff --git a/src/components/FormattedDate.astro b/src/components/FormattedDate.astro new file mode 100644 index 00000000..f3191caf --- /dev/null +++ b/src/components/FormattedDate.astro @@ -0,0 +1,17 @@ +--- +type Props = { + date: Date; +}; + +const { date } = Astro.props; +--- + + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 00000000..c52d2669 --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,57 @@ +--- +import { getRelativeLocaleUrl } from 'astro:i18n'; +import Logo from '@/components/Logo.astro'; +import { getLangFromUrl, useTranslations } from '@/i18n/utils'; + +const lang = getLangFromUrl(Astro.url); +const t = useTranslations(lang); +const pathname = Astro.url.pathname; + +const navigationLinks = [ + { href: getRelativeLocaleUrl(lang, '/'), label: t('nav.home') }, + { href: getRelativeLocaleUrl(lang, '/blog'), label: t('nav.blog') }, + { href: getRelativeLocaleUrl(lang, '/about'), label: t('nav.about') }, + { href: getRelativeLocaleUrl(lang, '/contact'), label: t('nav.contact') }, +]; + +const ctaLinks = { + secondary: { href: getRelativeLocaleUrl(lang, '/contributing'), label: t('nav.contributing') }, + primary: { href: getRelativeLocaleUrl(lang, '/join-us'), label: t('nav.join_us') }, +}; +--- + +
+ +
diff --git a/src/components/HeaderLink.astro b/src/components/HeaderLink.astro new file mode 100644 index 00000000..dcbc43c2 --- /dev/null +++ b/src/components/HeaderLink.astro @@ -0,0 +1,24 @@ +--- +import type { HTMLAttributes } from 'astro/types'; + +type Props = HTMLAttributes<'a'>; + +const { href, class: className, ...props } = Astro.props; +const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, ''); +const subpath = pathname.match(/[^/]+/g); +const isActive = href === pathname || href === `/${subpath?.[0] ?? ''}`; +--- + + + + diff --git a/src/components/Logo.astro b/src/components/Logo.astro new file mode 100644 index 00000000..bd09d80d --- /dev/null +++ b/src/components/Logo.astro @@ -0,0 +1,49 @@ +--- +import { SITE_TITLE } from '@/consts'; + +const sizeMap = { + sm: 'h-8 w-8 min-h-8 min-w-8', + md: 'h-10 w-10 min-h-10 min-w-10', + lg: 'h-14 w-14 min-h-14 min-w-14', + nav: 'h-9 w-9 min-h-9 min-w-9', + full: 'w-full', +} as const; + +const titleMap: Record = { + default: 'text-lg font-semibold text-base-content', + nav: 'text-lg font-bold text-base-content', +}; + +interface Props { + /** Logo mark size (`nav` = 36px + bold title, same as podcodar.fly.dev navbar) */ + size?: 'sm' | 'md' | 'lg' | 'nav' | 'full'; + /** Show site title next to the mark */ + showTitle?: boolean; + class?: string; +} + +const { size = 'md', showTitle = true, class: className } = Astro.props; + +const logoSrc = `${import.meta.env.BASE_URL}/images/logo.svg`; +const markClass = sizeMap[size] ?? sizeMap.md; +const titleClass = titleMap[size] ?? titleMap.default; +--- + + + PodCodar + {showTitle && {SITE_TITLE}} + diff --git a/src/components/marketing/ActivityGrid.astro b/src/components/marketing/ActivityGrid.astro new file mode 100644 index 00000000..3eafcbd8 --- /dev/null +++ b/src/components/marketing/ActivityGrid.astro @@ -0,0 +1,43 @@ +--- +import type { Activity } from '@/data/marketing'; + +interface Props { + items: readonly Activity[]; +} + +const { items } = Astro.props; + +/** Heroicons 24 outline: interview, career, group, terminal, calendar */ +const icons: Record = { + interview: + 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155', + career: + 'M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 00.75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 00-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0112 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 01-.673-.38m0 0A2.18 2.18 0 013 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 013.413-.387m7.5 0V5.25A2.25 2.25 0 0013.5 3h-3a2.25 2.25 0 00-2.25 2.25v.894m7.5 0a48.667 48.667 0 00-7.5 0M12 12.75h.008v.008H12v-.008Z', + groups: + 'M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0Zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0Zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0Z', + project: + 'm6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z', + cafe: 'M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5', +}; +--- + +
+ { + items.map((item) => ( +
+
+ +

{item.title}

+

{item.description}

+
+
+ )) + } +
diff --git a/src/components/marketing/Hero.astro b/src/components/marketing/Hero.astro new file mode 100644 index 00000000..910f9bc0 --- /dev/null +++ b/src/components/marketing/Hero.astro @@ -0,0 +1,44 @@ +--- +import Logo from '@/components/Logo.astro'; +import type { HeroContent } from '@/data/marketing'; + +interface Props { + content: HeroContent; +} + +const { content } = Astro.props; +--- + +
+ + +
+
+

{content.eyebrow}

+

+ {content.headline} +

+

+ {content.subhead} +

+ +
+ + +
+
diff --git a/src/components/marketing/HowToHelp.astro b/src/components/marketing/HowToHelp.astro new file mode 100644 index 00000000..35cca62b --- /dev/null +++ b/src/components/marketing/HowToHelp.astro @@ -0,0 +1,27 @@ +--- +import type { HelpPath } from '@/data/marketing'; + +interface Props { + items: readonly HelpPath[]; +} + +const { items } = Astro.props; +--- + +
+ { + items.map((item) => ( + + )) + } +
diff --git a/src/components/marketing/Section.astro b/src/components/marketing/Section.astro new file mode 100644 index 00000000..91ba37ce --- /dev/null +++ b/src/components/marketing/Section.astro @@ -0,0 +1,32 @@ +--- +interface Props { + id?: string; + eyebrow?: string; + title: string; + subtitle?: string; + variant?: 'default' | 'muted' | 'gradient'; + class?: string; +} + +const { id, eyebrow, title, subtitle, variant = 'default', class: className } = Astro.props; + +const bg = + variant === 'muted' + ? 'bg-base-200/80' + : variant === 'gradient' + ? 'bg-linear-to-b from-primary/10 via-base-100 to-base-100' + : 'bg-base-100'; +--- + +
+
+ { + eyebrow && ( +

{eyebrow}

+ ) + } +

{title}

+ {subtitle &&

{subtitle}

} + +
+
diff --git a/src/components/marketing/TestimonialGrid.astro b/src/components/marketing/TestimonialGrid.astro new file mode 100644 index 00000000..83081489 --- /dev/null +++ b/src/components/marketing/TestimonialGrid.astro @@ -0,0 +1,43 @@ +--- +import type { Testimonial } from '@/data/marketing'; + +interface Props { + items: readonly Testimonial[]; +} + +const { items } = Astro.props; +--- + +
+ { + items.map((t) => ( +
+
+

{t.quote}

+
+
+ + +
+ {t.name} + {t.role &&

{t.role}

} +
+
+
+
+ )) + } +
diff --git a/src/components/metadata/PageMetadata.astro b/src/components/metadata/PageMetadata.astro new file mode 100644 index 00000000..a985e6bb --- /dev/null +++ b/src/components/metadata/PageMetadata.astro @@ -0,0 +1,38 @@ +--- +/** + * Per-page SEO: canonical URL, document title, description, Open Graph, Twitter Card. + * Global charset, viewport, favicon, RSS, etc. stay in BaseHead. + */ +import type { ImageMetadata } from 'astro'; +import FallbackImage from '@/assets/blog-placeholder-1.jpg'; + +interface Props { + title: string; + description: string; + image?: ImageMetadata; +} + +const { title, description, image = FallbackImage } = Astro.props; + +const canonicalURL = new URL(Astro.url.pathname, Astro.site); +const canonicalHref = canonicalURL.href; +const ogImageHref = new URL(image.src, Astro.url).href; +--- + + + +{title} + + + + + + + + + + + + + + diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 00000000..01be460b --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,9 @@ +// Place any global data in this file. +// You can import this data from anywhere in your site by using the `import` keyword. + +export const SITE_TITLE = 'PodCodar'; +export const SITE_DESCRIPTION = + 'PodCodar — comunidade sem fins lucrativos que democratiza educação profissionalizante em tecnologia no Brasil. Mentoria, estudos em grupo e projetos reais.'; + +/** Pass to `` for full-width pages (landing, hero sections). */ +export const LAYOUT_MAIN_FULL_WIDTH = 'flex flex-1 flex-col'; diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 00000000..f7d68b76 --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,20 @@ +import { defineCollection } from 'astro:content'; +import { glob } from 'astro/loaders'; +import { z } from 'astro/zod'; + +const blog = defineCollection({ + // Load Markdown and MDX files in the `src/content/blog/` directory. + loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }), + // Type-check frontmatter using a schema + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string(), + // Transform string to Date object + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + heroImage: z.optional(image()), + }), +}); + +export const collections = { blog }; diff --git a/src/content/blog/first-post.md b/src/content/blog/first-post.md new file mode 100644 index 00000000..ba83b6d4 --- /dev/null +++ b/src/content/blog/first-post.md @@ -0,0 +1,16 @@ +--- +title: 'Primeira publicação' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 08 2022' +heroImage: '../../assets/blog-placeholder-3.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/blog/markdown-style-guide.md b/src/content/blog/markdown-style-guide.md new file mode 100644 index 00000000..31bde3b1 --- /dev/null +++ b/src/content/blog/markdown-style-guide.md @@ -0,0 +1,214 @@ +--- +title: 'Guia de estilo Markdown' +description: 'Exemplos básicos de sintaxe Markdown para conteúdo no Astro.' +pubDate: 'Jun 19 2024' +heroImage: '../../assets/blog-placeholder-1.jpg' +--- + +A seguir, exemplos de sintaxe Markdown úteis ao escrever conteúdo no Astro. + +## Títulos + +Os elementos HTML `

` a `

` representam seis níveis de título. `

` é o nível mais alto e `

` o mais baixo. + +# H1 + +## H2 + +### H3 + +#### H4 + +##### H5 + +###### H6 + +## Parágrafo + +Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat. + +Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat. + +## Imagens + +### Sintaxe + +```markdown +![Texto alternativo](./caminho/completo/ou/relativo/da/imagem) +``` + +### Resultado + +![blog placeholder](../../assets/blog-placeholder-about.jpg) + +## Citações + +O elemento `blockquote` representa conteúdo citado de outra fonte, opcionalmente com atribuição em `footer` ou `cite`, e alterações em linha como anotações e abreviações. + +### Citação sem atribuição + +#### Sintaxe + +```markdown +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **Observe** que você pode usar _sintaxe Markdown_ dentro da citação. +``` + +#### Resultado + +> Tiam, ad mint andaepu dandae nostion secatur sequo quae. +> **Observe** que você pode usar _sintaxe Markdown_ dentro da citação. + +### Citação com atribuição + +#### Sintaxe + +```markdown +> Não se comunique compartilhando memória; compartilhe memória se comunicando.
+> — Rob Pike[^1] +``` + +#### Resultado + +> Não se comunique compartilhando memória; compartilhe memória se comunicando.
+> — Rob Pike[^1] + +[^1]: Trecho da [palestra](https://www.youtube.com/watch?v=PAAkCSZUG1c) de Rob Pike no Gopherfest, 18 de novembro de 2015. + +## Tabelas + +### Sintaxe + +```markdown +| Itálico | Negrito | Código | +| --------- | -------- | ------ | +| _itálico_ | **negrito** | `code` | +``` + +### Resultado + +| Itálico | Negrito | Código | +| --------- | -------- | ------ | +| _itálico_ | **negrito** | `code` | + +## Blocos de código + +### Sintaxe + +Use três crases em uma linha, o trecho e feche com três crases. Para realce de sintaxe, coloque o nome do idioma após as primeiras três crases (por exemplo `html`, `javascript`, `css`, `markdown`, `typescript`, `txt`, `bash`). + +````markdown +```html + + + + + Documento HTML5 de exemplo + + +

Teste

+ + +``` +```` + +### Resultado + +```html + + + + + Documento HTML5 de exemplo + + +

Teste

+ + +``` + +## Tipos de lista + +### Lista ordenada + +#### Sintaxe + +```markdown +1. Primeiro item +2. Segundo item +3. Terceiro item +``` + +#### Resultado + +1. Primeiro item +2. Segundo item +3. Terceiro item + +### Lista não ordenada + +#### Sintaxe + +```markdown +- Item +- Outro item +- Mais um item +``` + +#### Resultado + +- Item +- Outro item +- Mais um item + +### Lista aninhada + +#### Sintaxe + +```markdown +- Frutas + - Maçã + - Laranja + - Banana +- Laticínios + - Leite + - Queijo +``` + +#### Resultado + +- Frutas + - Maçã + - Laranja + - Banana +- Laticínios + - Leite + - Queijo + +## Outros elementos — abbr, sub, sup, kbd, mark + +### Sintaxe + +```markdown +GIF é um formato de imagem raster. + +H2O + +Xn + Yn = Zn + +Pressione CTRL + ALT + Delete para encerrar a sessão. + +A maioria dos salamandros é noturna e caça insetos, minhocas e outros pequenos animais. +``` + +### Resultado + +GIF é um formato de imagem raster. + +H2O + +Xn + Yn = Zn + +Pressione CTRL + ALT + Delete para encerrar a sessão. + +A maioria dos salamandros é noturna e caça insetos, minhocas e outros pequenos animais. diff --git a/src/content/blog/second-post.md b/src/content/blog/second-post.md new file mode 100644 index 00000000..ef28db51 --- /dev/null +++ b/src/content/blog/second-post.md @@ -0,0 +1,16 @@ +--- +title: 'Segunda publicação' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 15 2022' +heroImage: '../../assets/blog-placeholder-4.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/blog/third-post.md b/src/content/blog/third-post.md new file mode 100644 index 00000000..a7e0bf27 --- /dev/null +++ b/src/content/blog/third-post.md @@ -0,0 +1,16 @@ +--- +title: 'Terceira publicação' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jul 22 2022' +heroImage: '../../assets/blog-placeholder-2.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet. + +Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi. + +Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim. + +Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi. + +Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna. diff --git a/src/content/blog/using-mdx.mdx b/src/content/blog/using-mdx.mdx new file mode 100644 index 00000000..8d06cc02 --- /dev/null +++ b/src/content/blog/using-mdx.mdx @@ -0,0 +1,33 @@ +--- +title: 'Usando MDX' +description: 'Lorem ipsum dolor sit amet' +pubDate: 'Jun 01 2024' +heroImage: '../../assets/blog-placeholder-5.jpg' +--- + +Este tema inclui a integração [@astrojs/mdx](https://docs.astro.build/pt-br/guides/integrations-guide/mdx/) já instalada e configurada no seu arquivo `astro.config.mjs`. Se preferir não usar MDX, desative removendo a integração da configuração. + +## Por que MDX? + +MDX é um Markdown estendido com JavaScript e JSX. Isso permite [misturar componentes de interface ao conteúdo Markdown](https://docs.astro.build/pt-br/guides/integrations-guide/mdx/#mdx-no-astro) — por exemplo gráficos interativos ou alertas. + +Se você já tem conteúdo em MDX, a integração deve facilitar a migração para o Astro. + +## Exemplo + + + +Veja como importar e usar um componente dentro do MDX. +Ao abrir esta página no navegador, o link clicável abaixo deve aparecer. + +import HeaderLink from '@/components/HeaderLink.astro'; + + + Componente embutido em MDX + + +## Mais links + +- [Documentação MDX](https://mdxjs.com/docs/what-is-mdx) +- [Astro: Markdown / MDX](https://docs.astro.build/pt-br/basics/astro-pages/#markdown-and-mdx-pages) +- **Obs.:** [diretivas de cliente](https://docs.astro.build/pt-br/reference/directives-reference/#diretivas-de-cliente) ainda são necessárias para componentes interativos. Caso contrário, tudo renderiza como HTML estático (sem JavaScript) por padrão. diff --git a/src/data/marketing.ts b/src/data/marketing.ts new file mode 100644 index 00000000..0fd3905f --- /dev/null +++ b/src/data/marketing.ts @@ -0,0 +1,207 @@ +/** User-facing copy for the landing and institutional pages (pt-BR strings). */ + +export const hero = { + eyebrow: 'PodCodar', + headline: 'Educação em tecnologia, feita em comunidade', + subhead: + 'Somos uma comunidade e organização sem fins lucrativos focada em transformar a vida de brasileiros por meio da educação profissionalizante em tecnologia — com mentoria, estudos em grupo e projetos reais.', + /** Short line in the hero side card */ + cardTagline: 'Democratizar o acesso ao conhecimento. Estudar junto. Crescer com propósito.', + primaryCta: { label: 'Faça parte', href: '/join-us' }, + secondaryCta: { label: 'Como posso ajudar?', href: '/contributing' }, +} as const; + +export type HeroContent = typeof hero; + +export const mission = { + title: 'Missão', + body: [ + 'A PodCodar existe para democratizar o acesso à educação profissionalizante nas áreas de tecnologia. Acreditamos que qualificação e acesso ao conhecimento digital são motores de mudança de vida — e que, no Brasil, essa educação ainda costuma ser elitizada, limitada e cara.', + 'Por isso guiamos e damos acesso a quem deseja se profissionalizar: em comunidade, com escuta e responsabilidade social.', + 'Nosso foco de ensino inclui, entre outras frentes: software (front-end e back-end), infraestrutura, inteligência artificial, dados (incluindo engenharia e ciência de dados) e design (UI e UX).', + ], +} as const; + +export type Activity = { + title: string; + description: string; + icon: 'interview' | 'career' | 'groups' | 'project' | 'cafe'; +}; + +export const activities: Activity[] = [ + { + title: 'Entrevistas simuladas', + description: + 'Pratique processos seletivos com apoio da comunidade e feedback para ganhar confiança antes da entrevista de verdade.', + icon: 'interview', + }, + { + title: 'Mentoria de carreira', + description: + 'Conversas e orientação para transição, currículo, portfólio e próximos passos — do primeiro estágio à troca de área.', + icon: 'career', + }, + { + title: 'Grupos de estudo', + description: + 'Turmas e canais no WhatsApp e no Discord para tirar dúvidas, compartilhar materiais e manter o ritmo de estudo coletivo.', + icon: 'groups', + }, + { + title: 'Assistência em projetos', + description: + 'Mentoria em projetos práticos — da ideia ao repositório — com apoio de quem já passou por desafios parecidos.', + icon: 'project', + }, + { + title: 'Café com Código', + description: + 'Encontros para trocar experiência, apresentar o que você está construindo e conhecer a comunidade com calma (e café).', + icon: 'cafe', + }, +] as const; + +export type Testimonial = { + id: number; + name: string; + role?: string; + avatarUrl: string; + profileUrl: string; + quote: string; +}; + +export const testimonials: Testimonial[] = [ + { + id: 1, + name: 'Giovanna Neves Damasceno', + avatarUrl: 'https://avatars.githubusercontent.com/u/18710340?v=4', + profileUrl: 'https://github.com/giovannand', + quote: + 'A PodCodar juntou minha trajetória em tecnologia com o desejo de trabalhar com pessoas e ajudá-las a se desenvolver. É um projeto com propósito claro — amo fazer parte.', + }, + { + id: 2, + name: 'Gilberto Ferreira Borges Júnior', + avatarUrl: 'https://avatars.githubusercontent.com/u/57193296?v=4', + profileUrl: 'https://github.com/borgesgfj', + quote: + 'Com o apoio da comunidade fiz a transição da pesquisa e do ensino em física para engenharia de software. Hoje contribuir de volta é tão gratificante quanto aprender aqui.', + }, + { + id: 3, + name: 'Filipe Barbosa', + avatarUrl: 'https://avatars.githubusercontent.com/u/65319425?v=4', + profileUrl: 'https://github.com/Filipe-barbosa', + quote: + 'Pensei que programar não era pra mim — entrei na comunidade e, com o tempo, a confiança veio. Hoje sigo construindo carreira com a rede ao lado.', + }, + { + id: 4, + name: 'Guilherme Barbosa', + avatarUrl: 'https://avatars.githubusercontent.com/u/73261443?v=4', + profileUrl: 'https://github.com/Guilherme-BS', + quote: + 'A PodCodar mudou minha perspectiva: novas conversas, novos aprendizados e um lugar onde me sinto em casa na tecnologia.', + }, +] as const; + +export type HelpPath = { + title: string; + description: string; + href: string; + cta: string; +}; + +export const howToHelp: HelpPath[] = [ + { + title: 'Doações', + description: + 'Apoio financeiro de pessoas físicas e patrocínios ajudam a manter a PodCodar sustentável e a ampliar impacto — plataforma de ensino, oficinas e equipe.', + href: '/contact', + cta: 'Falar sobre doação', + }, + { + title: 'Voluntariado', + description: + 'Tempo e habilidades: mentoria, facilitação de estudos e eventos, revisão de código, design e comunicação — há espaço para o seu jeito de contribuir.', + href: '/contributing', + cta: 'Ver voluntariado', + }, + { + title: 'Parcerias', + description: + 'Empresas e fundos podem apoiar diversidade e educação em tecnologia — inclusive parcerias para contratação de pessoas qualificadas pela comunidade.', + href: '/contact', + cta: 'Propor parceria', + }, +] as const; + +/** Core values (from the onboarding guide). */ +export const coreValues: { title: string; text: string }[] = [ + { + title: 'Inclusão', + text: 'Garantir que a educação tecnológica seja acessível a todos, independentemente de origem, renda ou localização. Somos um espaço seguro e acolhedor.', + }, + { + title: 'Colaboração', + text: 'Estudar junto é mais prazeroso e eficiente. Valorizamos o compartilhamento de conhecimento, discussões abertas e o apoio mútuo em projetos e estudos.', + }, + { + title: 'Qualidade de ensino', + text: 'Foco em mentoria e conteúdo que prepare de fato a próxima geração de profissionais digitais para o mercado de trabalho.', + }, +]; + +export const aboutCommunity = { + title: 'Cultura e organização', + lead: 'A comunidade se organiza para multiplicar impacto: núcleo pedagógico, guildas por área e iniciativas práticas dentro de cada uma.', + points: [ + 'Núcleo Pedagógico: centraliza visão, prioridades e alocação de pessoas e recursos para as iniciativas.', + 'Guildas: núcleos por área de interesse ou entrega (projetos, eventos, design e outras frentes).', + 'Iniciativas: programas concretos — mentorias, incubadora, Café com Código, meetups, workshops, Chá com Design e mais.', + ], +} as const; + +export type ProjectItem = { + name: string; + description: string; + href: string; +}; + +export const projects: ProjectItem[] = [ + { + name: 'Site e materiais PodCodar', + description: 'Este site e recursos da comunidade em evolução — contribuições são bem-vindas.', + href: 'https://github.com/podcodar/webapp', + }, + { + name: 'Organização no GitHub', + description: 'Repositórios abertos da PodCodar — issues e PRs são um ótimo primeiro passo.', + href: 'https://github.com/podcodar', + }, +] as const; + +export const eventsBlock = { + title: 'Eventos', + body: 'Café com Código, meetups e workshops são alguns dos formatos em que a comunidade se encontra ao vivo (muitas vezes no Google Meet). Novidades e convites circulam nos grupos e no Discord.', + externalLabel: 'Ver organização no GitHub', + externalHref: 'https://github.com/podcodar', +} as const; + +/** Communication channels (onboarding guide). */ +export const communicationChannels: { channel: string; description: string }[] = [ + { + channel: 'WhatsApp', + description: + 'Comunicação do dia a dia: avisos rápidos, interação social e coordenação com a turma.', + }, + { + channel: 'Discord', + description: + 'Grupos de estudo, canais técnicos, mentorias e discussões — é o “quartel-general” assíncrono da PodCodar.', + }, + { + channel: 'Google Meet', + description: 'Reuniões do núcleo pedagógico, workshops e encontros ao vivo com a comunidade.', + }, +]; diff --git a/src/data/og-default.ts b/src/data/og-default.ts new file mode 100644 index 00000000..52e7301d --- /dev/null +++ b/src/data/og-default.ts @@ -0,0 +1,7 @@ +/** + * Default Open Graph image for marketing pages. + * Replace with a 1200×630 asset when brand artwork is ready. + */ +import marketingOgImage from '@/assets/blog-placeholder-about.jpg'; + +export default marketingOgImage; diff --git a/src/data/social-links.ts b/src/data/social-links.ts new file mode 100644 index 00000000..3485ace3 --- /dev/null +++ b/src/data/social-links.ts @@ -0,0 +1,54 @@ +/** + * Footer / social presence. Update hrefs if a handle or invite URL changes. + */ +export type SocialNetwork = 'github' | 'linkedin' | 'instagram' | 'youtube' | 'x' | 'discord'; + +export type FooterSocialLink = { + href: string; + /** Screen reader label (pt-BR). */ + label: string; + network: SocialNetwork; +}; + +/** Iconify ids for `astro-icon` + `@iconify-json/simple-icons`. */ +export const footerSocialIconify: Record = { + github: 'simple-icons:github', + linkedin: 'simple-icons:linkedin', + instagram: 'simple-icons:instagram', + youtube: 'simple-icons:youtube', + x: 'simple-icons:x', + discord: 'simple-icons:discord', +}; + +export const footerSocialLinks: FooterSocialLink[] = [ + { + network: 'github', + href: 'https://github.com/podcodar', + label: 'PodCodar no GitHub', + }, + { + network: 'linkedin', + href: 'https://www.linkedin.com/company/podcodar/', + label: 'PodCodar no LinkedIn', + }, + { + network: 'instagram', + href: 'https://www.instagram.com/podcodar/', + label: 'PodCodar no Instagram', + }, + { + network: 'youtube', + href: 'https://www.youtube.com/@podcodar', + label: 'PodCodar no YouTube', + }, + { + network: 'x', + href: 'https://x.com/podcodar', + label: 'PodCodar no X', + }, + { + network: 'discord', + href: 'https://discord.com/invite/podcodar', + label: 'PodCodar no Discord', + }, +]; diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts new file mode 100644 index 00000000..0c114676 --- /dev/null +++ b/src/i18n/ui.ts @@ -0,0 +1,13 @@ +export const defaultLang = 'pt-br' as const; + +export const ui = { + 'pt-br': { + 'nav.home': 'Início', + 'nav.blog': 'Blog', + 'nav.about': 'Sobre', + 'nav.contact': 'Contato', + 'nav.contributing': 'Como posso ajudar?', + 'nav.join_us': 'Faça parte!', + 'footer.copyright': 'Todos os direitos reservados.', + }, +} as const; diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts new file mode 100644 index 00000000..1957bbbe --- /dev/null +++ b/src/i18n/utils.ts @@ -0,0 +1,16 @@ +import { defaultLang, ui } from '@/i18n/ui'; + +export type Lang = keyof typeof ui; + +/** + * Idioma do site (pt-BR). Rotas em inglês foram removidas. + */ +export function getLangFromUrl(_url: URL): Lang { + return defaultLang; +} + +export function useTranslations(lang: Lang) { + return function t(key: keyof (typeof ui)[typeof defaultLang]) { + return ui[lang][key]; + }; +} diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro new file mode 100644 index 00000000..9c5f849a --- /dev/null +++ b/src/layouts/BlogPost.astro @@ -0,0 +1,45 @@ +--- +import { Image } from 'astro:assets'; +import type { CollectionEntry } from 'astro:content'; +import FormattedDate from '@/components/FormattedDate.astro'; +import Layout from '@/layouts/Layout.astro'; + +type Props = CollectionEntry<'blog'>['data']; + +const { title, description, pubDate, updatedDate, heroImage } = Astro.props; +--- + + +
+
+ { + heroImage && ( + + ) + } +
+
+
+
+ + { + updatedDate && ( +
+ Última atualização em +
+ ) + } +
+

{title}

+
+
+ +
+
+
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro new file mode 100644 index 00000000..b986389f --- /dev/null +++ b/src/layouts/Layout.astro @@ -0,0 +1,50 @@ +--- +import type { ImageMetadata } from 'astro'; +import ClientRouter from 'astro/components/ClientRouter.astro'; +import BaseHead from '@/components/BaseHead.astro'; +import Footer from '@/components/Footer.astro'; +import Header from '@/components/Header.astro'; +import { getLangFromUrl } from '@/i18n/utils'; + +interface Props { + title: string; + description: string; + image?: ImageMetadata; + /** Tailwind classes for `
` (width, padding, etc.) */ + mainClass?: string; + /** Document `lang`; defaults to site locale (pt-BR). */ + lang?: string; +} + +const { + title, + description, + image, + mainClass = 'mx-auto w-full max-w-[720px] flex-1 px-4', + lang: langOverride, +} = Astro.props; + +const rawLang = langOverride ?? getLangFromUrl(Astro.url); +const documentLang = rawLang === 'pt-br' ? 'pt-BR' : rawLang; +--- + + + + + + + + + + Pular para o conteúdo + +
+
+ +
+