Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prisma/schema): add posts and items data model #129

Merged
merged 14 commits into from
Feb 25, 2024
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
47 changes: 36 additions & 11 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ jobs:
cypress:
name: Cypress
runs-on: ubuntu-latest
env:
PORT: '8811'
steps:
- name: Cancel previous runs
uses: styfle/[email protected]
Expand All @@ -114,9 +116,18 @@ jobs:
node-version-file: package.json
cache: pnpm

- name: Cache cypress binary
uses: actions/cache@v4
with:
path: ~/.cache/Cypress
key: cypress-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}

- name: Download deps
run: pnpm install --frozen-lockfile

- name: Download cypress binary
run: pnpm cypress install

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

Expand All @@ -130,21 +141,23 @@ jobs:
- name: Build
run: pnpm build

- name: Cypress run
uses: cypress-io/github-action@v5
with:
start: pnpm start:mocks
wait-on: 'http://localhost:8811'
record: true
tag: ${{ github.event_name }}
# right now, we can't easily reset state between tests and thus we're
# only going to run a single smoke test (hitting k8s back-end) in CI.
spec: cypress/e2e/smoke.cy.ts
- name: Start frontend
id: startfrontend
run: |
pnpm start:mocks > frontend.log 2>&1 &
echo $! > frontend_pid.txt
env:
PORT: '8811'
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }}
SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}

- name: Wait for frontend
run: pnpm wait-on --timeout 60000 http://localhost:${{ env.PORT }}

- name: Run cypress tests
run: pnpm cypress run --record --tag ${{ github.event_name }} --spec cypress/e2e/smoke.cy.ts
env:
# testing keys (e.g. cypress) and other environment variables.
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# override the github commit message for better Cypress Cloud mesages.
Expand All @@ -154,6 +167,18 @@ jobs:
# @see https://docs.cypress.io/guides/continuous-integration/github-actions#Pull-requests-commit-message-is-merge-SHA-into-SHA
COMMIT_INFO_SHA: ${{ github.event.pull_request.head.sha }}

- name: Kill frontend
if: ${{ always() && steps.startfrontend.outcome == 'success' }}
run: |
frontend_pid=$(cat frontend_pid.txt)
kill $frontend_pid

- name: Display frontend logs
if: always()
run: |
cat frontend.log


build:
name: Build
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions IDEAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,8 @@ I can optionally sync Instagram and upload the fit pics as a post.

An app that does the style "what are my colors?" thing.
There was a TikTok I saw earlier of a model who traveled to Korea to get a style consultation (i.e. what her colors are, what the best makeup is for her, etc).

## 9

An app to organize mood boards.
You can import posts from Instagram and TikTok.
4 changes: 2 additions & 2 deletions app/components/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ export function Dialog({
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Trigger />
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className='fixed inset-0 z-40 bg-white/50 dark:bg-gray-900/50' />
<DialogPrimitive.Overlay className='fixed inset-0 z-40 bg-white/75 dark:bg-gray-900/75' />
<DialogPrimitive.Content
className={cn(
'center fixed z-50 overflow-auto rounded-lg border border-gray-200/50 bg-white shadow-2xl focus:outline-none dark:border-gray-800/50 dark:bg-gray-900 max-h-[calc(100vh-3rem)]',
'center fixed z-50 overflow-auto rounded border border-gray-200/50 bg-white shadow-2xl focus:outline-none dark:border-gray-800/50 dark:bg-gray-950 max-h-[calc(100vh-3rem)] max-w-[calc(100vw-3rem)]',
className,
)}
onOpenAutoFocus={onOpenAutoFocus}
Expand Down
28 changes: 28 additions & 0 deletions app/components/image-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Link, type LinkProps } from '@remix-run/react'

import { Image } from 'components/image'

import { cn } from 'utils/cn'

export function ImageItem({
image,
className,
...etc
}: { image?: string } & LinkProps) {
return (
<li>
<Link
prefetch='intent'
className={cn(
'aspect-person bg-gray-100 dark:bg-gray-900 block',
className,
)}
{...etc}
>
{image && (
<Image className='object-cover w-full h-full' src={image} alt='' />
)}
</Link>
</li>
)
}
6 changes: 4 additions & 2 deletions app/components/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { buttonVariants } from 'components/ui/button'

import { cn } from 'utils/cn'
import { slug } from 'utils/general'

export function Layout({ className, ...etc }: Partial<PanelGroupProps>) {
return (
Expand Down Expand Up @@ -60,11 +61,12 @@ export function LayoutDivider() {
}

export function LayoutSection({
id,
id: initialId,
header,
children,
className,
}: PropsWithChildren<{ id: string; header: string; className?: string }>) {
}: PropsWithChildren<{ id?: string; header: string; className?: string }>) {
const id = initialId ?? slug(header)
return (
<section
className={cn(
Expand Down
20 changes: 2 additions & 18 deletions app/components/set-item.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
import { Link } from '@remix-run/react'
import { ImageItem } from 'components/image-item'

import { type SetItem as SetItemT } from 'utils/set'

export function SetItem({ item }: { item: SetItemT }) {
return (
<li>
<Link
to={item.url}
prefetch='intent'
className='aspect-person bg-gray-100 dark:bg-gray-900 block'
>
{item.images.length > 0 && (
<img
className='object-cover w-full h-full'
src={item.images[0].url}
alt=''
/>
)}
</Link>
</li>
)
return <ImageItem to={item.url} image={item.images[0]?.url} />
}
2 changes: 2 additions & 0 deletions app/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type PrismaProductFilter =
| Prisma.BrandListRelationFilter
| Prisma.UserListRelationFilter
| Prisma.LookListRelationFilter
| Prisma.ItemListRelationFilter
| Prisma.PostListRelationFilter
type PrismaShowFilter =
| Prisma.IntFilter<'Show'>
| Prisma.IntNullableFilter<'Show'>
Expand Down
2 changes: 0 additions & 2 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ export const handle: Handle = {
breadcrumb: () => ({ to: '/', children: 'DOLCE' }),
}

export const config = { runtime: 'edge' }

export const links: LinksFunction = () => [
{
rel: 'preload',
Expand Down
176 changes: 176 additions & 0 deletions app/routes/_header.$username.posts.$postId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { useLoaderData, useNavigate } from '@remix-run/react'
import { type DataFunctionArgs } from '@vercel/remix'
import {
MoreHorizontal,
Bookmark,
Send,
Heart,
MessageCircle,
} from 'lucide-react'
import { type FC } from 'react'

import { Avatar } from 'components/avatar'
import { Dialog } from 'components/dialog'
import { Image } from 'components/image'
import { ImageItem } from 'components/image-item'
import { LayoutSection } from 'components/layout'
import { TimeAgo } from 'components/time-ago'
import { Badge } from 'components/ui/badge'
import { Button } from 'components/ui/button'

import { prisma } from 'db.server'

export async function loader({ params }: DataFunctionArgs) {
const postId = Number(params.postId)
if (Number.isNaN(postId)) throw new Response('Not Found', { status: 404 })
const post = await prisma.post.findUnique({
where: { id: postId },
include: {
items: { include: { colors: true, styles: true } },
products: {
include: { variants: { include: { images: { take: 1 } }, take: 1 } },
},
variants: { include: { product: true, images: { take: 1 } } },
looks: { include: { images: { take: 1 } } },
author: true,
images: true,
},
})
if (post == null) throw new Response('Not Found', { status: 404 })
return post
}

export default function PostPage() {
const post = useLoaderData<typeof loader>()
const nav = useNavigate()
return (
<Dialog
open
onOpenChange={() => nav('..', { preventScrollReset: true })}
className='flex w-max'
>
<a
href={post.url}
target='_blank'
rel='noopener noreferrer'
aria-label='Open original post'
className='aspect-square bg-gray-100 dark:bg-gray-900 grow shrink min-h-[450px] max-w-[800px] max-h-[800px]'
>
<Image
src={post.images[0]?.url}
alt=''
className='w-full h-full object-cover'
/>
</a>
<div className='grow shrink-[2] min-w-[405px] max-w-[500px] flex flex-col'>
<header className='flex items-center gap-2 p-4 justify-between border-b flex-none border-gray-200 dark:border-gray-800'>
<div className='flex items-center gap-2'>
<Avatar src={post.author} />
<div>
<h1 className='text-sm font-medium'>{post.author.username}</h1>
<p className='text-2xs text-gray-500'>{post.author.name}</p>
</div>
</div>
<Button size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
</Button>
</header>
<div className='h-0 grow overflow-y-auto'>
<Items />
<LayoutSection header='Products'>
<ol className='grid grid-cols-2 gap-1'>
{post.products.map((product) => (
<ImageItem
key={product.id}
className='aspect-product'
to={`/products/${product.slug}/variants/${product.variants[0]?.id}`}
image={product.variants[0]?.images[0]?.url}
/>
))}
{post.variants.map((variant) => (
<ImageItem
key={variant.id}
className='aspect-product'
to={`/products/${variant.product.slug}/${variant.id}`}
image={variant.images[0]?.url}
/>
))}
</ol>
</LayoutSection>
<LayoutSection header='Looks'>
<ol className='grid grid-cols-2 gap-1'>
{post.looks.map((look) => (
<ImageItem
key={look.id}
className='aspect-person'
to={`/shows/${look.showId}`}
image={look.images[0]?.url}
/>
))}
</ol>
</LayoutSection>
</div>
<div className='flex-none p-2 border-t border-gray-200 dark:border-gray-800'>
<div className='flex items-center gap-2 justify-between'>
<div className='flex items-center'>
<IconButtonLink label='Like' url={post.url} icon={Heart} />
<IconButtonLink
label='Comment'
url={post.url}
icon={MessageCircle}
/>
<IconButtonLink label='Share' url={post.url} icon={Send} />
</div>
<IconButtonLink label='Save' url={post.url} icon={Bookmark} />
</div>
<TimeAgo
datetime={post.createdAt}
className='text-3xs text-gray-500 uppercase p-2'
/>
</div>
</div>
</Dialog>
)
}

function Items() {
const post = useLoaderData<typeof loader>()
return (
<LayoutSection header='Items'>
<ol>
{post.items.map((item) => (
<li key={item.id}>
{item.colors.map((color) => (
<Badge>{color.name}</Badge>
))}
{item.styles.map((style) => (
<Badge>{style.name}</Badge>
))}
</li>
))}
</ol>
</LayoutSection>
)
}

function IconButtonLink({
url,
icon: Icon,
label,
}: {
url: string
icon: FC<{ className?: string }>
label: string
}) {
return (
<a
href={url}
aria-label={label}
target='_blank'
rel='noopener noreferrer'
className='hover:opacity-50 transition-colors p-2'
>
<Icon className='w-6 h-6' />
</a>
)
}
Loading
Loading