Skip to content

Commit

Permalink
add cta to gallery
Browse files Browse the repository at this point in the history
  • Loading branch information
viet nguyen committed Nov 30, 2023
1 parent 6709cd1 commit b653bfc
Show file tree
Hide file tree
Showing 16 changed files with 158 additions and 69 deletions.
20 changes: 20 additions & 0 deletions src/app/api/updateAreaPage/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'
import { validate } from 'uuid'

export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'

/**
* Endpoint: /api/updateAreaPage
*/
export async function GET (request: NextRequest): Promise<any> {
const uuid = request.nextUrl.searchParams.get('uuid') as string
if (uuid == null || !validate(uuid)) {
return NextResponse.json({ message: 'Missing uuid in query string' })
} else {
revalidatePath(`/area/${uuid}`, 'page')
revalidatePath(`/editArea/${uuid}`, 'layout')
return NextResponse.json({ message: 'OK' }, { status: 200 })
}
}
21 changes: 11 additions & 10 deletions src/app/area/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MapPinLine, Lightbulb, ArrowRight } from '@phosphor-icons/react/dist/ss
import 'mapbox-gl/dist/mapbox-gl.css'
import Markdown from 'react-markdown'

import PhotoMontage from '@/components/media/PhotoMontage'
import PhotoMontage, { UploadPhotoCTA } from '@/components/media/PhotoMontage'
import { getArea } from '@/js/graphql/getArea'
import { StickyHeaderContainer } from '@/app/components/ui/StickyHeaderContainer'
import { GluttenFreeCrumbs } from '@/components/ui/BreadCrumbs'
Expand Down Expand Up @@ -56,7 +56,12 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom

return (
<AreaPageContainer
photoGallery={<PhotoMontage isHero photoList={photoList} />}
photoGallery={
photoList.length === 0
? <UploadPhotoCTA />
: <PhotoMontage isHero photoList={photoList} />
}
pageActions={<AreaPageActions areaName={areaName} uuid={uuid} />}
breadcrumbs={
<StickyHeaderContainer>
<GluttenFreeCrumbs pathTokens={pathTokens} ancestors={ancestors} />
Expand Down Expand Up @@ -94,8 +99,6 @@ export default async function Page ({ params }: PageWithCatchAllUuidProps): Prom
</a>
<ArticleLastUpdate {...authorMetadata} />
</div>

<AreaPageActions uuid={uuid} areaName={areaName} />
</div>

<div className='area-climb-page-summary-right'>
Expand Down Expand Up @@ -136,12 +139,10 @@ const parseUuidAsFirstParam = ({ params }: PageWithCatchAllUuidProps): string =>
const EditDescriptionCTA: React.FC<{ uuid: string }> = ({ uuid }) => (
<div role='alert' className='alert'>
<Lightbulb size={24} />
<div className='text-sm'>No description available. Be the first to contribute!
<div className='mt-2'>
<Link href={`/editArea/${uuid}/general#description`} className='btn btn-sm btn-outline'>
Add description <ArrowRight size={16} />
</Link>
</div>
<div className='text-sm'>No information available. Be the first to&nbsp;
<Link href={`/editArea/${uuid}/general#description`} className='link-dotted inline-flex items-center gap-1'>
add a description <ArrowRight size={16} />
</Link>
</div>
</div>
)
7 changes: 6 additions & 1 deletion src/app/components/AreaPageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { UploadPhotoButton } from '@/components/NewPost'
* Main action bar for area page
*/
export const AreaPageActions: React.FC<{ uuid: string, areaName: string } > = ({ uuid, areaName }) => (
<ul className='mt-6 flex items-center justify-between'>
<ul className='flex items-center justify-between gap-2'>
<Link href={`/editArea/${uuid}`} className='btn btn-solid btn-accent'>
<PencilSimple size={20} weight='duotone' /> Edit
</Link>
Expand All @@ -20,3 +20,8 @@ export const AreaPageActions: React.FC<{ uuid: string, areaName: string } > = ({
<ShareAreaLinkButton uuid={uuid} areaName={areaName} />
</ul>
)

/**
* Skeleton. Height = actual component's button height.
*/
export const AreaPageActionsSkeleton: React.FC = () => (<div className='w-80 bg-base-200 h-9 rounded-btn' />)
2 changes: 1 addition & 1 deletion src/app/components/ProfileNavButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function ProfileNavButton ({ isMobile = true }: ProfileNavButtonP
<div className='block relative'>
<DropdownMenu>
<DropdownTrigger asChild>
<button className='btn btn-primary gap-2 no-animation'>
<button className='btn btn-accent gap-2 no-animation'>
<UserCircleIcon className='w-6 h-6 rounded-full' />
Profile
</button>
Expand Down
7 changes: 6 additions & 1 deletion src/app/components/ui/AreaPageContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { GallerySkeleton } from '@/components/media/PhotoMontage'
import React from 'react'
import { AreaPageActionsSkeleton } from '../AreaPageActions'

/**
* Area page containter. Show loading skeleton if no params are provided.
*/
export const AreaPageContainer: React.FC<{
photoGallery?: React.ReactNode
pageActions?: React.ReactNode
breadcrumbs?: React.ReactNode
map?: React.ReactNode
children?: React.ReactNode
}> = ({ photoGallery, breadcrumbs, map, children }) => {
}> = ({ photoGallery, pageActions, breadcrumbs, map, children }) => {
return (
<article>
<div className='p-4 mx-auto max-w-5xl xl:max-w-7xl'>
{photoGallery == null ? <GallerySkeleton /> : photoGallery}
<div className='flex justify-end py-4 border-b'>
{pageActions == null ? <AreaPageActionsSkeleton /> : pageActions}
</div>
{breadcrumbs == null ? <BreadCrumbsSkeleton /> : breadcrumbs}
{children == null ? <ContentSkeleton /> : children}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/ui/StickyHeaderContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const StickyHeaderContainer: React.FC<{ children: ReactNode }> = ({ child
const atTop = intersection?.isIntersecting ?? false

return (
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100 -mx-6 px-6', atTop ? 'border-b border-base-300/60 bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100', atTop ? 'border-b border-base-300/60 bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
{children}
</div>
)
Expand Down
2 changes: 1 addition & 1 deletion src/app/editArea/[slug]/SidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const SidebarNav: React.FC<{ slug: string, canAddAreas: boolean, canAddCl
/**
* Disable menu item's hover/click when own page is showing
*/
const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'bg-base-300/60 pointer-events-none' : ''
const classForActivePage = (myPath: string): string => activePath.endsWith(myPath) ? 'font-semibold pointer-events-none' : ''
return (
<nav className='px-6'>
<div className='sticky top-0'>
Expand Down
9 changes: 7 additions & 2 deletions src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ A slightly deemphasized dotted underline for a tag in order to not competing wit
.dialog-default {
@apply top-0 left-0 h-screen md:dialog-center fixed z-50 w-screen max-w-screen-md bg-base-100 lg:drop-shadow-lg overflow-y-auto h-fit max-h-screen lg:max-h-[95vh];
}
.dialog-title {
@apply py-4 lg:py-5 left-0 top-0 w-full text-center fixed bg-base-100 bg-opacity-80 backdrop-blur-sm z-50 leading-none;

.dialog-wide {
@apply top-0 left-0 fixed z-50 w-full h-full w-screen h-screen bg-base-100 overflow-y-auto overscroll-contain;
}

.dialog-close-button {
Expand All @@ -115,4 +116,8 @@ A slightly deemphasized dotted underline for a tag in order to not competing wit
.dialog-form-default {
@apply mt-16 lg:mt-20 px-2 lg:px-4 mb-6 !important;
}

.dialog-title {
@apply py-4 lg:py-5 left-0 top-0 w-full text-center fixed bg-base-100 bg-opacity-80 backdrop-blur-sm z-50 leading-none;
}
}
19 changes: 19 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'
import { useEffect } from 'react'
import { useSearchParams, redirect } from 'next/navigation'
import { signIn, useSession } from 'next-auth/react'

export default function Page (): any {
const { status } = useSession()
const searchParams = useSearchParams()
useEffect(() => {
if (status === 'authenticated') {
const url = searchParams.get('callbackUrl') ?? '/'
redirect(url)
}
if (status === 'unauthenticated') {
void signIn('auth0')
}
}, [status])
return <div className='h-screen w-screen'><div className='m-6'>Loading...</div></div>
}
8 changes: 7 additions & 1 deletion src/components/NewPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export default function NewPost ({ isMobile = true, className = '' }: ProfileNav

export const UploadPhotoButton: React.FC = () => (
<UploadPhotoTrigger>
<button className='btn btn-outline btn-accent'><Camera size={20} /> Photo</button>
<div className='btn'><Camera size={20} /> Photo</div>
</UploadPhotoTrigger>
)

export const UploadPhotoTextOnlyButton: React.FC = () => (
<UploadPhotoTrigger>
<div className='btn btn-outline btn-primary'>Add photo</div>
</UploadPhotoTrigger>
)
5 changes: 0 additions & 5 deletions src/components/UploadPhotoTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,8 @@ export default function UploadPhotoTrigger ({ className = '', onUploaded, childr
*/
const sessionRef = useRef<any>()
sessionRef.current = data?.user
console.log('#before useUploader')
const { getRootProps, getInputProps, openFileDialog } = usePhotoUploader()
console.log('#before useStore')

const uploading = useUserGalleryStore(store => store.uploading)

console.log('#isUploading ', uploading)
return (
<div
className={clx(className, uploading ? 'pointer-events-none' : '')} {...getRootProps()} onClick={(e) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/crag/StickyHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const StickyHeader = ({ isClimbPage = false, ancestors, pathTokens, formA
const atTop = intersection?.isIntersecting ?? false

return (
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100 -mx-6 px-6', atTop ? 'border-b bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
<div ref={intersectionRef} className={clx('sticky top-0 z-40 py-2 lg:min-h-[4rem] block lg:flex lg:items-center lg:justify-between bg-base-100', atTop ? 'border-b bottom-shadow backdrop-blur-sm bg-opacity-90' : '')}>
<GluttenFreeCrumbs ancestors={ancestors} pathTokens={pathTokens} />
<div className='hidden lg:block'>
{/* only visible at lg or wider */}
Expand Down
65 changes: 43 additions & 22 deletions src/components/media/PhotoMontage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import { useState, useEffect, MouseEventHandler } from 'react'
import Image from 'next/image'
import clx from 'classnames'
import { SquaresFour } from '@phosphor-icons/react/dist/ssr'

import PhotoFooter from './PhotoFooter'
import { MediaWithTags } from '../../js/types'
import useResponsive from '../../js/hooks/useResponsive'
import { DefaultLoader, MobileLoader } from '../../js/sirv/util'
import PhotoGalleryModal from './PhotoGalleryModal'
import { userMediaStore } from '../../js/stores/media'
import { UploadPhotoTextOnlyButton } from '../NewPost'

export interface PhotoMontageProps {
photoList: MediaWithTags[]
Expand Down Expand Up @@ -91,36 +93,47 @@ const PhotoMontage = ({ photoList: initialList, isHero = false, showSkeleton = f
</div>
)
})}
{shuffledList.length === 1 &&
<div className='w-full h-full bg-base-200 rounded-xl relative'>
<div className='absolute bottom-8 right-8'><UploadPhotoTextOnlyButton /></div>
</div>}
</div>
)
}

const first = shuffledList[0]
const theRest = shuffledList.slice(1, 5)
return (
<div
className='grid grid-cols-4 grid-flow-row-dense gap-1 rounded-xl overflow-hidden h-80 hover:cursor-pointer fadeinEffect'
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{showPhotoGalleryModal ? photoGalleryModal : undefined}
<div className='block relative col-start-1 col-span-2 row-span-2 col-end-3'>
<ResponsiveImage mediaUrl={first.mediaUrl} isHero={isHero} onClick={() => setShowPhotoGalleryModal(!showPhotoGalleryModal)} />
<PhotoFooter mediaWithTags={first} hover={hover} />
<div className='relative'>
<div
className='grid grid-cols-4 grid-flow-row-dense gap-1 rounded-xl overflow-hidden h-80 hover:cursor-pointer fadeinEffect'
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
{showPhotoGalleryModal ? photoGalleryModal : undefined}
<div className='block relative col-start-1 col-span-2 row-span-2 col-end-3'>
<ResponsiveImage mediaUrl={first.mediaUrl} isHero={isHero} onClick={() => setShowPhotoGalleryModal(!showPhotoGalleryModal)} />
<PhotoFooter mediaWithTags={first} hover={hover} />
</div>
{theRest.map((media) => {
const { mediaUrl } = media
return (
<div
key={mediaUrl}
className='block relative'
>
<ResponsiveImage mediaUrl={mediaUrl} isHero={isHero} onClick={() => setShowPhotoGalleryModal(!showPhotoGalleryModal)} />
<PhotoFooter mediaWithTags={media} hover={hover} />
</div>
)
})}
</div>
{theRest.map((media, i) => {
const { mediaUrl } = media
return (
<div
key={mediaUrl}
className='block relative'
>
<ResponsiveImage mediaUrl={mediaUrl} isHero={isHero} onClick={() => setShowPhotoGalleryModal(!showPhotoGalleryModal)} />
<PhotoFooter mediaWithTags={media} hover={hover} />
</div>
)
})}

{shuffledList.length > 5 &&
<div className='absolute right-8 top-[70%] drop-shadow-md'>
<button className='btn btn-sm btn-outline bg-base-200/60' onClick={() => setShowPhotoGalleryModal(true)}>
<SquaresFour size={16} />See all {shuffledList.length} photos
</button>
</div>}
</div>
)
}
Expand All @@ -147,6 +160,14 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({ mediaUrl, isHero = tr
alt=''
/>)

export const UploadPhotoCTA: React.FC = () => {
return (
<div className='bg-base-300/20 h-40 rounded-box relative'>
<div className='absolute bottom-3 right-3'><UploadPhotoTextOnlyButton /></div>
</div>
)
}

export const GallerySkeleton: React.FC = () => (
<div className='grid grid-cols-4 grid-flow-row-dense gap-1 rounded-xl overflow-hidden h-80 bg-base-200 lg:bg-transparent'>
<div className='hidden lg:block relative col-start-1 col-span-2 row-span-2 col-end-3 bg-base-200 h-80' />
Expand Down
47 changes: 31 additions & 16 deletions src/components/media/__tests__/PhotoMontage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
import { render, screen } from '@testing-library/react'
import PhotoMontage from '../PhotoMontage'
import type PhotoMontageType from '../PhotoMontage'
import { mediaList } from './data'

test('PhotoMontage can render 1 photo', async () => {
render(<PhotoMontage photoList={mediaList.slice(0, 1)} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(1)
expect(elements[0].src).toContain(mediaList[0].mediaUrl)
})
let PhotoMontage: typeof PhotoMontageType

test('PhotoMontage always renders 2 photos when provided with a list of 2 to 4', async () => {
render(<PhotoMontage photoList={mediaList.slice(0, 3)} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(2) // should be 2
})
jest.mock('../../UploadPhotoTrigger', () => ({
__esModule: true,
default: () => <div />
}))

describe('PhotoMontage tests', () => {
beforeAll(async () => {
// why async import? see https://github.com/facebook/jest/issues/10025#issuecomment-716789840
const module = await import('../PhotoMontage')
PhotoMontage = module.default
})

test('PhotoMontage can render 1 photo', async () => {
render(<PhotoMontage photoList={mediaList.slice(0, 1)} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(1)
expect(elements[0].src).toContain(mediaList[0].mediaUrl)
})

test('PhotoMontage always renders 2 photos when provided with a list of 2 to 4', async () => {
render(<PhotoMontage photoList={mediaList.slice(0, 3)} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(2) // should be 2
})

test('PhotoMontage always renders 5 photos when provided with a list > 5', async () => {
render(<PhotoMontage photoList={mediaList} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(5) // should be 5
test('PhotoMontage always renders 5 photos when provided with a list > 5', async () => {
render(<PhotoMontage photoList={mediaList} isHero />)
const elements: HTMLImageElement[] = await screen.findAllByRole('img')
expect(elements.length).toBe(5) // should be 5
})
})
Loading

0 comments on commit b653bfc

Please sign in to comment.