diff --git a/.github/workflows/neon_workflow.yml b/.github/workflows/neon_workflow.yml new file mode 100644 index 0000000..a481137 --- /dev/null +++ b/.github/workflows/neon_workflow.yml @@ -0,0 +1,141 @@ +name: Create/Delete Branch for Pull Request + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - closed + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + setup: + name: Setup + outputs: + branch: ${{ steps.branch_name.outputs.current_branch }} + runs-on: ubuntu-latest + steps: + - name: Get branch name + id: branch_name + uses: tj-actions/branch-names@v8 + + jest: + name: Jest Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Run Jest tests + run: npm test + + create_and_test_neon_branch: + name: Create Neon Branch and Run E2E Tests + needs: setup + if: | + github.event_name == 'pull_request' && ( + github.event.action == 'synchronize' + || github.event.action == 'opened' + || github.event.action == 'reopened') + runs-on: ubuntu-latest + env: + NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} + NEXTAUTH_URL: http://localhost:3001 # URL of main page for testing after authoriztion redirect + steps: + - name: Get branch expiration date as an env variable (2 weeks from now) + id: get_expiration_date + run: echo "EXPIRES_AT=$(date -u --date '+14 days' +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_ENV" + - name: Create Neon Branch + id: create_neon_branch + uses: neondatabase/create-branch-action@v6 + with: + project_id: ${{ vars.NEON_PROJECT_ID }} + branch_name: preview/pr-${{ github.event.number }}-${{ needs.setup.outputs.branch }} + api_key: ${{ secrets.NEON_API_KEY }} + expires_at: ${{ env.EXPIRES_AT }} + - name: Set DATABASE_URL from Neon branch + run: echo "DATABASE_URL=${{ steps.create_neon_branch.outputs.db_url }}" >> $GITHUB_ENV + - name: Debug - Print Neon outputs + run: | + echo "db_url: ${{ steps.create_neon_branch.outputs.db_url }}" + echo "db_url_with_pooler: ${{ steps.create_neon_branch.outputs.db_url_with_pooler }}" + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Debug - Check DATABASE_URL + env: + DATABASE_URL: ${{ steps.create_neon_branch.outputs.db_url }} + run: | + echo "DATABASE_URL is set: ${DATABASE_URL:+yes}" + echo "DATABASE_URL length: ${#DATABASE_URL}" + - name: Run Playwright E2E tests + env: + POSTGRES_URL: ${{ steps.create_neon_branch.outputs.db_url }} + run: npm run test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report-neon-branch + path: playwright-report/ + retention-days: 30 + +# The step above creates a new Neon branch. +# You may want to do something with the new branch, such as run migrations, run tests +# on it, or send the connection details to a hosting platform environment. +# The branch DATABASE_URL is available to you via: +# "${{ steps.create_neon_branch.outputs.db_url_with_pooler }}". +# It's important you don't log the DATABASE_URL as output as it contains a username and +# password for your database. +# For example, you can uncomment the lines below to run a database migration command: +# - name: Run Migrations +# run: npm run db:migrate +# env: +# # to use pooled connection +# DATABASE_URL: "${{ steps.create_neon_branch.outputs.db_url_with_pooler }}" +# # OR to use unpooled connection +# # DATABASE_URL: "${{ steps.create_neon_branch.outputs.db_url }}" + +# Following the step above, which runs database migrations, you may want to check +# for schema changes in your database. We recommend using the following action to +# post a comment to your pull request with the schema diff. For this action to work, +# you also need to give permissions to the workflow job to be able to post comments +# and read your repository contents. Add the following permissions to the workflow job: +# +# permissions: +# contents: read +# pull-requests: write +# +# You can also check out https://github.com/neondatabase/schema-diff-action for more +# information on how to use the schema diff action. +# You can uncomment the lines below to enable the schema diff action. +# - name: Post Schema Diff Comment to PR +# uses: neondatabase/schema-diff-action@v1 +# with: +# project_id: ${{ vars.NEON_PROJECT_ID }} +# compare_branch: preview/pr-${{ github.event.number }}-${{ needs.setup.outputs.branch }} +# api_key: ${{ secrets.NEON_API_KEY }} + + delete_neon_branch: + name: Delete Neon Branch + needs: setup + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Delete Neon Branch + uses: neondatabase/delete-branch-action@v3 + with: + project_id: ${{ vars.NEON_PROJECT_ID }} + branch: preview/pr-${{ github.event.number }}-${{ needs.setup.outputs.branch }} + api_key: ${{ secrets.NEON_API_KEY }} + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index d6504d9..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Test Suite -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -env: - NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }} -jobs: - jest: - name: Jest Unit Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm ci - - name: Run Jest tests - run: npm test - - playwright: - name: Playwright E2E Tests - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npm run test:e2e - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/app/api/auth/[...nextauth]/route.js b/app/api/auth/[...nextauth]/route.js index 3a6887e..9abc3c2 100644 --- a/app/api/auth/[...nextauth]/route.js +++ b/app/api/auth/[...nextauth]/route.js @@ -1,20 +1,79 @@ import NextAuth from "next-auth" import GoogleProvider from "next-auth/providers/google" +import CredentialsProvider from "next-auth/providers/credentials" import NeonAdapter from "@auth/neon-adapter" import { Pool } from "@neondatabase/serverless" const pool = new Pool({ connectionString: process.env.DATABASE_URL }) +const adapter = NeonAdapter(pool) + +// Build providers list +const providers = [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), +] + +// Add test credentials provider only in non-production environments +if (process.env.NODE_ENV !== 'production') { + providers.push( + CredentialsProvider({ + id: 'test-credentials', + name: 'Test Login', + credentials: { + email: { label: 'Email', type: 'email' }, + name: { label: 'Name', type: 'text' }, + }, + async authorize(credentials) { + if (!credentials?.email) { + return null + } + + const email = credentials.email + const name = credentials.name || 'Test User' + + // Check if user already exists + let user = await adapter.getUserByEmail(email) + + if (!user) { + // Create user in database (like OAuth would do) + user = await adapter.createUser({ + email, + name, + emailVerified: new Date(), + image: null, + }) + } + + return user + }, + }) + ) +} export const authOptions = { - providers: [ - GoogleProvider({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - }), - ], - adapter: NeonAdapter(pool), - + providers, + adapter, + // Use JWT for credentials provider (development), database for production OAuth + session: { + strategy: process.env.NODE_ENV !== 'production' ? 'jwt' : 'database', + }, + callbacks: { + // Include user id in the session (for both JWT and database sessions) + async session({ session, token, user }) { + // JWT strategy (credentials provider in development) + if (token?.sub) { + session.user.id = token.sub + } + // Database strategy (OAuth in production) + else if (user?.id) { + session.user.id = user.id + } + return session + }, + }, } const handler = NextAuth(authOptions) diff --git a/app/api/submit-score/route.js b/app/api/submit-score/route.js new file mode 100644 index 0000000..9a6fab1 --- /dev/null +++ b/app/api/submit-score/route.js @@ -0,0 +1,40 @@ +import { submitDailyScore, updateStreak } from 'app/lib/db/db'; +import { NextResponse } from 'next/server'; +import { revalidatePath } from 'next/cache'; + +export async function POST(request) { + try { + console.log(request); + const { milliseconds, date } = await request.json(); + console.log('Received score submission in POST:', milliseconds); + const submissionResult = await submitDailyScore(milliseconds, date); + console.log('Submission result:', submissionResult); + // if no new row is returned then a conflict occurred + const dailyAlreadyHasSubmission = submissionResult === null; + // submission is made only if one new row is inserted + const submissionSuccess = submissionResult !== null; + // Update the streak (completed if milliseconds is not null) + const completed = milliseconds !== null; + console.log('Submission success:', submissionSuccess); + const newStreak = submissionSuccess ? await updateStreak(completed, date) : null; + + const streakUpdateSuccess = newStreak !== null; + + if (streakUpdateSuccess) { + revalidatePath('/', 'layout'); + } + + return NextResponse.json({ + success: submissionSuccess && streakUpdateSuccess, + submission: submissionResult, + newStreak: streakUpdateSuccess ? newStreak : null, + dailyAlreadyHasSubmission: dailyAlreadyHasSubmission + }); + } catch (error) { + console.error('Error submitting score:', error); + return NextResponse.json( + { success: false, error: error.message }, + { status: error.message === 'User not authenticated' ? 401 : 500 } + ); + } +} diff --git a/app/layout.js b/app/layout.js index 8c3e654..080c9ed 100644 --- a/app/layout.js +++ b/app/layout.js @@ -5,10 +5,57 @@ import UserMenu from 'app/ui/user-menu'; import HelpButton from 'app/ui/help-button'; import { DailyTimer } from 'app/ui/timer'; import DatePicker from 'app/ui/date-picker'; -import { mahjongFeltPurple, mahjongTileFace } from 'app/ui/styles'; +import { mahjongTileFace } from 'app/ui/styles'; +import { getStreakInfo } from 'app/lib/db/db'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { streakIsCurrent } from 'app/lib/utils'; -const styleClass = { +const appBarStyle = { backgroundColor: mahjongTileFace, + boxShadow: 3, + borderBottom: '4px solid #a855f7', +} + +async function StreakBanner() { + const streakInfo = await getStreakInfo(); + // TODO also retrieve their daily score and color fires grey if they failed today + console.log(streakInfo); + + if (streakInfo?.lastDate && streakIsCurrent(streakInfo.lastDate)) { + const fireCount = Math.min(streakInfo.streak, 3); + const fires = new Array(fireCount).fill('🔥'); + + return ( +