diff --git a/.github/workflows/alt-text-bot.yml b/.github/workflows/alt-text-bot.yml index 3fe35936..edba0194 100644 --- a/.github/workflows/alt-text-bot.yml +++ b/.github/workflows/alt-text-bot.yml @@ -1,4 +1,5 @@ -name: Accessibility-alt-text-bot +name: Accessibility Alt Text Bot + on: issues: types: [opened, edited] @@ -11,6 +12,10 @@ on: discussion_comment: types: [created, edited] +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.event.discussion.number || github.ref }} + cancel-in-progress: true + permissions: issues: write pull-requests: write @@ -18,8 +23,8 @@ permissions: jobs: accessibility_alt_text_bot: - name: Check alt text is set on issue or pull requests + name: Check alt text on issues and pull requests runs-on: ubuntu-latest steps: - - name: Get action 'github/accessibility-alt-text-bot' - uses: github/accessibility-alt-text-bot@v1.4.0 # Set to latest + - name: Run Alt Text Bot + uses: github/accessibility-alt-text-bot@v1.4.0 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 2e8b9cf3..4bbbcdd2 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -4,49 +4,87 @@ on: push: branches: - develop - pull_request: + pull_request_target: + types: [opened, synchronize, reopened] branches: - develop +# Cancel old builds on new commit for same workflow + branch/PR +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + initial-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + + - name: Install dependencies + run: npm ci + e2e: + needs: initial-checks runs-on: ubuntu-latest + environment: production env: DATABASE_URL: "postgresql://postgres:secret@localhost:5432/postgres" NEXTAUTH_URL: http://localhost:3000/api/auth GITHUB_ID: ${{ secrets.E2E_GITHUB_ID }} GITHUB_SECRET: ${{ secrets.E2E_GITHUB_SECRET }} - NEXTAUTH_SECRET: "please_keep_this_secret" + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' + + - name: Cache Playwright browsers + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-playwright- - name: Run docker-compose - uses: hoverkraft-tech/compose-action@v2.0.1 + uses: isbang/compose-action@v1.5.1 with: compose-file: "./docker-compose.yml" + down-flags: "--volumes" services: | db - name: Wait for DB to be ready run: | - until nc -z localhost 5432; do - echo "Waiting for database connection..." - sleep 5 - done + timeout 60s bash -c 'until nc -z localhost 5432; do echo "Waiting for database connection..."; sleep 2; done' + shell: bash - name: Install dependencies - run: npm install + run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps + if: steps.playwright-cache.outputs.cache-hit != 'true' - name: Seed database run: | @@ -54,11 +92,18 @@ jobs: npm run db:seed - name: Run Playwright tests - run: npx playwright test --reporter=html + id: playwright-tests + run: npx playwright test + continue-on-error: true - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() with: name: playwright-report path: playwright-report/ - retention-days: 30 \ No newline at end of file + retention-days: 30 + + - name: Check test results + if: steps.playwright-tests.outcome == 'failure' + run: exit 1 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index e404107c..0d52c26c 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -6,13 +6,20 @@ on: pull_request_target: types: [opened] +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: welcome-new-contributor: runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write steps: - name: 'Greet the contributor' uses: garg3133/welcome-new-contributors@v1.2 with: token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Hello @contributor_name, thanks for opening your first issue! your contribution is valuable to us. The maintainers will review this issue and provide feedback as soon as possible.' - pr-message: 'Hello @contributor_name, thanks for opening your first Pull Request. The maintainers will review this Pull Request and provide feedback as soon as possible. Keep up the great work!' + issue-message: 'Hello @{{ contributor }}, thanks for opening your first issue! Your contribution is valuable to us. The maintainers will review this issue and provide feedback as soon as possible.' + pr-message: 'Hello @{{ contributor }}, thanks for opening your first Pull Request. The maintainers will review this Pull Request and provide feedback as soon as possible. Keep up the great work!' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index f339ff8f..84df7d17 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,23 +1,39 @@ -name: Check the pull request +name: Code Quality Checks on: pull_request: types: [opened, synchronize, reopened, edited] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: - check-pull-request: - name: Check code styling and run tests + lint-and-format: + name: Run ESLint and Prettier runs-on: ubuntu-latest steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Use the correct Node.js version + + - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: "20.17.0" + node-version: 'lts/*' + cache: 'npm' + - name: Install dependencies run: npm ci + - name: Run ESLint run: npm run lint + - name: Run Prettier run: npm run prettier + + - name: Check for uncommitted changes + run: | + git diff --exit-code || \ + (echo "Detected uncommitted changes after build. See status below:" && \ + git diff && \ + exit 1) diff --git a/app/(app)/settings/_client.tsx b/app/(app)/settings/_client.tsx index eb3dd37b..04d02cce 100644 --- a/app/(app)/settings/_client.tsx +++ b/app/(app)/settings/_client.tsx @@ -12,8 +12,8 @@ import { saveSettingsSchema } from "@/schema/profile"; import { uploadFile } from "@/utils/s3helpers"; import type { user } from "@/server/db/schema"; import { Button } from "@/components/ui-components/button"; -import { CheckCheck, Loader2 } from "lucide-react"; -import { Heading } from "@/components/ui-components/heading"; +import { Loader2 } from "lucide-react"; +import { Subheading, Heading } from "@/components/ui-components/heading"; import { Avatar } from "@/components/ui-components/avatar"; import { Input } from "@/components/ui-components/input"; import { @@ -23,10 +23,8 @@ import { } from "@/components/ui-components/fieldset"; import { Textarea } from "@/components/ui-components/textarea"; import { Switch } from "@/components/ui-components/switch"; - -function classNames(...classes: string[]) { - return classes.filter(Boolean).join(" "); -} +import { Divider } from "@/components/ui-components/divider"; +import { Text } from "@/components/ui-components/text"; type User = Pick< typeof user.$inferSelect, @@ -51,9 +49,8 @@ const Settings = ({ profile }: { profile: User }) => { const { register, handleSubmit, - watch, reset, - formState: { errors }, + formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(saveSettingsSchema), defaultValues: { @@ -70,14 +67,14 @@ const Settings = ({ profile }: { profile: User }) => { const [sendForVerification, setSendForVerification] = useState(false); const [loading, setLoading] = useState(false); const [emailError, setEmailError] = useState(""); + const [cooldown, setCooldown] = useState(0); const [profilePhoto, setProfilePhoto] = useState({ status: "idle", url: profile.image, }); - const { mutate, isError, isSuccess, isLoading } = - api.profile.edit.useMutation(); + const { mutate, isError, isSuccess } = api.profile.edit.useMutation(); const { mutate: getUploadUrl } = api.profile.getUploadUrl.useMutation(); const { mutate: updateUserPhotoUrl } = api.profile.updateProfilePhotoUrl.useMutation(); @@ -85,6 +82,8 @@ const Settings = ({ profile }: { profile: User }) => { const fileInputRef = useRef(null); + const isValidEmail = (email: string) => /\S+@\S+\.\S+/.test(email); + useEffect(() => { if (isSuccess) { toast.success("Saved"); @@ -94,6 +93,13 @@ const Settings = ({ profile }: { profile: User }) => { } }, [isError, isSuccess]); + useEffect(() => { + if (cooldown > 0) { + const timer = setTimeout(() => setCooldown(cooldown - 1), 1000); + return () => clearTimeout(timer); + } + }, [cooldown]); + const onSubmit: SubmitHandler = (values) => { mutate({ ...values, newsletter: weeklyNewsletter, emailNotifications }); }; @@ -148,6 +154,15 @@ const Settings = ({ profile }: { profile: User }) => { }; const handleNewEmailUpdate = async () => { + if (cooldown > 0) { + return; + } + + if (!isValidEmail(newEmail)) { + setEmailError("Please enter a valid email address"); + return; + } + setLoading(true); await updateEmail( { newEmail }, @@ -163,404 +178,270 @@ const Settings = ({ profile }: { profile: User }) => { setLoading(false); toast.success("Verification link sent to your email."); setSendForVerification(true); + setCooldown(120); // Set a 2 minute cooldown + setEmailError(""); // Clear any existing error }, }, ); }; return ( -
-
-
-
- {/* Profile section */} -
-
-
- Profile Information -
- - {/* Photo upload */} - -
- - {/* Container for Label and Subheading */} -
- -
- This will be displayed on your public profile -
-
- -
-
- -
- -
- - - - -
- JPG, GIF or PNG. 1MB max. -
-
-
- - {/* Input field */} -
-
- -
- - {/* Container for Label and Subheading */} -
- -
- This will be displayed on your public profile -
-
- - {/* Input field */} -
- - - {/* Error message */} - {errors?.name && ( - - {errors.name.message} - - )} -
-
-
- - {/* User name */} -
- - {/* Container for Label and Subheading */} -
- -
- This will be how you share your profile -
-
- - {/* Input field */} -
- - - {/* Error message */} - {errors?.username && ( - - {errors.username.message} - - )} -
-
-
- - {/* Short Bio */} -
- - {/* Container for Label and Subheading */} -
- -
- This will be displayed on your public profile. Maximum - 200 characters. -
-
- - {/* Text Area Field */} -
-