Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ jobs:
- name: 📥 Download deps
uses: bahmutov/npm-install@v1

- name: 🎭 Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-

- name: 🎭 Install Playwright browsers
run: npm run test:e2e:install

- name: 🏄 Copy test env vars
run: cp .env.example .env

Expand Down Expand Up @@ -100,6 +111,14 @@ jobs:
- name: 📥 Download deps
uses: bahmutov/npm-install@v1

- name: 🎭 Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-

- name: 📥 Install Playwright Browsers
run: npm run test:e2e:install

Expand Down
2 changes: 1 addition & 1 deletion app/components/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type ReactElement, useEffect } from 'react'
import * as Sentry from '@sentry/react-router'
import { type ReactElement, useEffect } from 'react'
import {
type ErrorResponse,
isRouteErrorResponse,
Expand Down
2 changes: 1 addition & 1 deletion app/components/progress-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useNavigation } from 'react-router'
import { useEffect, useRef, useState } from 'react'
import { useNavigation } from 'react-router'
import { useSpinDelay } from 'spin-delay'
import { cn } from '#app/utils/misc.tsx'
import { Icon } from './ui/icon.tsx'
Expand Down
2 changes: 1 addition & 1 deletion app/components/search-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Form, useSearchParams, useSubmit } from 'react-router'
import { useId } from 'react'
import { Form, useSearchParams, useSubmit } from 'react-router'
import { useDebounce, useIsPending } from '#app/utils/misc.tsx'
import { Icon } from './ui/icon.tsx'
import { Input } from './ui/input.tsx'
Expand Down
2 changes: 1 addition & 1 deletion app/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Slot } from '@radix-ui/react-slot'
import { Link, type LinkProps } from 'react-router'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { Link, type LinkProps } from 'react-router'

import { cn } from '#app/utils/misc.tsx'

Expand Down
85 changes: 85 additions & 0 deletions app/components/user-profile.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { type ComponentProps, type ReactElement } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { createMemoryRouter, RouterProvider } from 'react-router'
import { afterEach, expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { UserProfileView } from './user-profile.tsx'

let root: Root | null = null
let container: HTMLDivElement | null = null

const render = (ui: ReactElement) => {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
root.render(ui)
}

const renderProfile = (props: ComponentProps<typeof UserProfileView>) => {
const router = createMemoryRouter(
[
{
path: '/',
element: <UserProfileView {...props} />,
},
],
{ initialEntries: ['/'] },
)

render(<RouterProvider router={router} />)
}

afterEach(() => {
root?.unmount()
root = null
container?.remove()
container = null
})

test('The user profile when not logged in as self', async () => {
const user = {
id: 'user_1',
username: 'harry',
name: 'Harry Example',
}

renderProfile({
user,
userJoinedDisplay: 'Jan 1, 2024',
isLoggedInUser: false,
})

await expect
.element(page.getByRole('heading', { level: 1, name: user.name }))
.toBeVisible()
await expect
.element(page.getByRole('link', { name: `${user.name}'s recipients` }))
.toBeVisible()
})

test('The user profile when logged in as self', async () => {
const user = {
id: 'user_2',
username: 'logan',
name: 'Logan Example',
}

renderProfile({
user,
userJoinedDisplay: 'Jan 1, 2024',
isLoggedInUser: true,
})

await expect
.element(page.getByRole('heading', { level: 1, name: user.name }))
.toBeVisible()
await expect
.element(page.getByRole('button', { name: /logout/i }))
.toBeVisible()
await expect
.element(page.getByRole('link', { name: /my recipients/i }))
.toBeVisible()
await expect
.element(page.getByRole('link', { name: /edit profile/i }))
.toBeVisible()
})
89 changes: 89 additions & 0 deletions app/components/user-profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Form, Link, useLoaderData } from 'react-router'
import { Spacer } from '#app/components/spacer.tsx'
import { Button } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.tsx'
import { useOptionalUser } from '#app/utils/user.ts'

export type UserProfileUser = {
id: string
name: string | null
username: string
}

export type UserProfileLoaderData = {
user: UserProfileUser
userJoinedDisplay: string
}

export type UserProfileViewProps = {
user: UserProfileUser
userJoinedDisplay: string
isLoggedInUser: boolean
}

export function UserProfileView({
user,
userJoinedDisplay,
isLoggedInUser,
}: UserProfileViewProps) {
const userDisplayName = user.name ?? user.username

return (
<div className="container mt-36 mb-48 flex flex-col items-center justify-center">
<Spacer size="4xs" />
<div className="flex flex-col items-center">
<div className="flex flex-wrap items-center justify-center gap-4">
<h1 className="text-h2 text-center">{userDisplayName}</h1>
</div>
<p className="text-muted-foreground mt-2 text-center">
Joined {userJoinedDisplay}
</p>
{isLoggedInUser ? (
<Form action="/logout" method="POST" className="mt-3">
<Button type="submit" variant="link" size="pill">
<Icon name="exit" className="scale-125 max-md:scale-150">
Logout
</Icon>
</Button>
</Form>
) : null}
<div className="mt-10 flex gap-4">
{isLoggedInUser ? (
<>
<Button asChild>
<Link to="/recipients" prefetch="intent">
My recipients
</Link>
</Button>
<Button asChild>
<Link to="/settings/profile" prefetch="intent">
Edit profile
</Link>
</Button>
</>
) : (
<Button asChild>
<Link to="/recipients" prefetch="intent">
{userDisplayName}'s recipients
</Link>
</Button>
)}
</div>
</div>
</div>
)
}

export default function UserProfile() {
const data = useLoaderData<UserProfileLoaderData>()
const loggedInUser = useOptionalUser()
const isLoggedInUser = data.user.id === loggedInUser?.id

return (
<UserProfileView
user={data.user}
userJoinedDisplay={data.userJoinedDisplay}
isLoggedInUser={isLoggedInUser}
/>
)
}
8 changes: 4 additions & 4 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { PassThrough } from 'stream'
import { createReadableStreamFromReadable } from '@react-router/node'
import * as Sentry from '@sentry/react-router'
import chalk from 'chalk'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import {
type ActionFunctionArgs,
type HandleDocumentRequestFunction,
type LoaderFunctionArgs,
ServerRouter,
} from 'react-router'
import * as Sentry from '@sentry/react-router'
import chalk from 'chalk'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import { getSessionRenewal, sessionKey } from './utils/auth.server.ts'
import { init as initCron } from './utils/cron.server.ts'
import { getEnv, init as initEnv } from './utils/env.server.ts'
Expand Down
8 changes: 6 additions & 2 deletions app/routes/_app+/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { useRef } from 'react'
import {
Form,
Link,
Outlet,
data as json,
type HeadersFunction,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
useSubmit,
} from 'react-router'
import { Form, Link, Outlet, useLoaderData, useSubmit } from 'react-router'
import { useRef } from 'react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.js'
import { Button } from '#app/components/ui/button.tsx'
import {
Expand Down
7 changes: 4 additions & 3 deletions app/routes/_app+/admin+/source.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import {
type LoaderFunctionArgs,
data as json,
type ActionFunctionArgs,
data as json,
type LoaderFunctionArgs,
useFetcher,
useLoaderData,
} from 'react-router'
import { useFetcher, useLoaderData } from 'react-router'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.js'
import { Field } from '#app/components/forms.js'
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_app+/recipients+/$recipientId.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
data as json,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
} from 'react-router'
import { useLoaderData } from 'react-router'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { requireUserId } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
Expand Down
6 changes: 4 additions & 2 deletions app/routes/_app+/recipients+/$recipientId.past.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { invariantResponse } from '@epic-web/invariant'
import {
type MetaFunction,
Link,
data as json,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
useSearchParams,
} from 'react-router'
import { Link, useLoaderData, useSearchParams } from 'react-router'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { SearchBar } from '#app/components/search-bar.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand Down
5 changes: 3 additions & 2 deletions app/routes/_app+/recipients+/$recipientId.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { invariantResponse } from '@epic-web/invariant'
import { type LoaderFunctionArgs, type MetaFunction } from 'react-router'
import { useEffect, useRef } from 'react'
import {
Link,
Outlet,
data as json,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
useMatches,
} from 'react-router'
import { useEffect, useRef } from 'react'
import { GeneralErrorBoundary } from '#app/components/error-boundary.js'
import { ButtonLink } from '#app/components/ui/button.tsx'
import { Icon } from '#app/components/ui/icon.js'
Expand Down
2 changes: 1 addition & 1 deletion app/routes/_app+/recipients+/__editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
useForm,
} from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { Form, useActionData, useFetcher } from 'react-router'
import { useState } from 'react'
import { Form, useActionData, useFetcher } from 'react-router'
import { z } from 'zod'
import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx'
import { ErrorList, Field, SelectField } from '#app/components/forms.tsx'
Expand Down
4 changes: 2 additions & 2 deletions app/routes/_app+/recipients+/new.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import {
type MetaFunction,
data as json,
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
} from 'react-router'
import { useLoaderData } from 'react-router'
import { requireUserId } from '#app/utils/auth.server.ts'
import { RecipientEditor } from './__editor.tsx'

Expand Down
5 changes: 4 additions & 1 deletion app/routes/_app+/settings.profile+/change-number.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { getFormProps, getInputProps, useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import {
Form,
Link,
data as json,
redirect,
type ActionFunctionArgs,
type LoaderFunctionArgs,
useActionData,
useLoaderData,
} from 'react-router'
import { Form, Link, useActionData, useLoaderData } from 'react-router'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { Button } from '#app/components/ui/button.tsx'
Expand Down
6 changes: 4 additions & 2 deletions app/routes/_app+/settings.profile+/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { invariantResponse } from '@epic-web/invariant'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import {
Link,
data as json,
type LoaderFunctionArgs,
type ActionFunctionArgs,
type LoaderFunctionArgs,
useFetcher,
useLoaderData,
} from 'react-router'
import { Link, useFetcher, useLoaderData } from 'react-router'
import { z } from 'zod'
import { ErrorList, Field } from '#app/components/forms.tsx'
import { ButtonLink } from '#app/components/ui/button.tsx'
Expand Down
Loading
Loading