Skip to content

Commit 547ac6d

Browse files
committed
add moments ui
1 parent 338d9dd commit 547ac6d

File tree

16 files changed

+251
-104
lines changed

16 files changed

+251
-104
lines changed

messages/en.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
{
22
"PostPage": {
33
"join_the_discussion_on_github": "Join the discussion on GitHub"
4+
},
5+
"Moments": {
6+
"mood": {
7+
"happy": "happy",
8+
"calm": "calm",
9+
"thoughtful": "thoughtful",
10+
"tired": "tired",
11+
"sad": "sad",
12+
"energized": "energized",
13+
"content": "content",
14+
"neutral": "neutral"
15+
}
416
}
517
}

messages/zh.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
{
22
"PostPage": {
33
"join_the_discussion_on_github": "在 GitHub 参与讨论"
4+
},
5+
"Moments": {
6+
"mood": {
7+
"happy": "开心",
8+
"calm": "平静",
9+
"thoughtful": "思考",
10+
"tired": "疲惫",
11+
"sad": "低落",
12+
"energized": "充实",
13+
"content": "满足",
14+
"neutral": "无感"
15+
}
416
}
517
}
Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,106 @@
11
'use client'
2-
import { useState } from 'react'
2+
import { useState, useMemo } from 'react'
33
import { Media } from '@/components/Media'
4-
import { RenderImageContext, RenderImageProps, RowsPhotoAlbum } from 'react-photo-album'
5-
import 'react-photo-album/rows.css'
64

75
import ImageLightbox from './ImageLightbox'
86

7+
import type { Media as MediaType } from '@/payload-types'
8+
99
interface ImageGridProps {
1010
images: {
1111
id: string
12-
image: {
13-
url: string
14-
width: number
15-
height: number
16-
alt?: string
17-
}
12+
image: MediaType
1813
}[]
1914
}
2015

21-
function NextJsImage({ alt = '' }: RenderImageProps, { photo, width, height }: RenderImageContext) {
22-
return (
23-
<div
24-
style={{
25-
width: '100%',
26-
position: 'relative',
27-
aspectRatio: `${width} / ${height}`,
28-
}}
29-
>
30-
<Media fill src={photo} alt={alt} pictureClassName="mt-0 mb-0" />
31-
</div>
32-
)
16+
type SlideType = {
17+
key: string
18+
src: string
19+
width?: number
20+
height?: number
21+
alt?: string
22+
type?: 'image' | 'video' | undefined
23+
autoPlay?: boolean
24+
sources?: {
25+
src: string
26+
type: string
27+
}[]
3328
}
3429

30+
const IMAGE_BASE_HEIGHT = 200
31+
3532
export default function ImageGrid({ images }: ImageGridProps) {
3633
const [index, setIndex] = useState(-1)
3734

38-
const photos = images
39-
.filter((i) => i.image)
40-
.map((item) => ({
35+
const { photos, sliders } = useMemo(() => {
36+
const validItems = images.filter((i) => i.image && i.image.url)
37+
38+
const processedPhotos = validItems.map((item) => ({
4139
...item.image,
4240
key: item.id,
43-
src: item.image.url,
44-
width: item.image.width,
45-
height: item.image.height,
41+
aspectRatio: (item.image.width ?? 100) / (item.image.height ?? 100),
4642
}))
4743

44+
const processedSliders: SlideType[] = processedPhotos.map((photo) => {
45+
const slide: SlideType = {
46+
key: photo.key,
47+
src: photo.url || '',
48+
width: photo.width ?? undefined,
49+
height: photo.height ?? undefined,
50+
alt: photo.alt || '',
51+
type: 'image',
52+
}
53+
if (photo.mimeType?.startsWith('video/')) {
54+
return {
55+
...slide,
56+
type: 'video',
57+
autoPlay: true,
58+
sources: [
59+
{
60+
src: photo.url || '',
61+
type: photo.mimeType,
62+
},
63+
],
64+
}
65+
}
66+
return slide
67+
})
68+
69+
return { photos: processedPhotos, sliders: processedSliders }
70+
}, [images])
71+
4872
return (
4973
<>
50-
<RowsPhotoAlbum
51-
photos={photos}
52-
render={{
53-
image: NextJsImage,
54-
}}
55-
targetRowHeight={200}
56-
spacing={(width) => (width < 300 ? 5 : 10)}
57-
onClick={({ index: current }) => setIndex(current)}
58-
/>
59-
<ImageLightbox index={index} open={index >= 0} close={() => setIndex(-1)} slides={photos} />
74+
<div className="flex flex-wrap gap-2">
75+
{photos.map((photo, photoIndex) => {
76+
const aspectRatio = photo.aspectRatio
77+
return (
78+
<div
79+
key={photo.key}
80+
className="relative overflow-hidden rounded-lg group cursor-pointer h-[160px] md:h-[200px]"
81+
style={{
82+
aspectRatio,
83+
flexGrow: aspectRatio,
84+
flexShrink: 0,
85+
flexBasis: `${IMAGE_BASE_HEIGHT * photo.aspectRatio}px`,
86+
// 限制最大宽度,防止单张图片过宽
87+
maxWidth: `${IMAGE_BASE_HEIGHT * aspectRatio * 1.2}px`,
88+
}}
89+
onClick={() => {
90+
setIndex(photoIndex)
91+
}}
92+
>
93+
<Media
94+
resource={photo}
95+
alt={photo.alt || ''}
96+
pictureClassName="mt-0 mb-0"
97+
className="absolute inset-0 w-full h-full object-cover transition-transform duration-300 group-hover:scale-105 flex justify-center items-center"
98+
/>
99+
</div>
100+
)
101+
})}
102+
</div>
103+
<ImageLightbox index={index} open={index >= 0} close={() => setIndex(-1)} slides={sliders} />
60104
</>
61105
)
62106
}

src/app/(frontend)/[locale]/moments/ImageLightbox.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
'use client'
2-
import { RenderImageContext, RenderImageProps, RowsPhotoAlbum } from 'react-photo-album'
32
import {
43
Lightbox,
54
isImageFitCover,
@@ -8,33 +7,38 @@ import {
87
useLightboxState,
98
RenderSlideProps,
109
SlideImage,
10+
SlideVideo,
1111
} from 'yet-another-react-lightbox'
12+
import { Video, Thumbnails, Counter, Captions } from 'yet-another-react-lightbox/plugins'
1213
import 'yet-another-react-lightbox/styles.css'
14+
import 'yet-another-react-lightbox/plugins/thumbnails.css'
15+
import 'yet-another-react-lightbox/plugins/counter.css'
16+
import 'yet-another-react-lightbox/plugins/captions.css'
1317

1418
import { Media } from '@/components/Media'
1519

16-
interface Slide extends SlideImage {
17-
key: string
20+
type CustomSlide = (SlideImage | SlideVideo) & {
21+
key?: string
1822
src: string
1923
width?: number
2024
height?: number
2125
alt?: string
2226
}
2327

2428
interface ImageLightboxProps {
25-
slides: Slide[]
29+
slides: CustomSlide[]
2630
index: number
2731
open: boolean
2832
close: () => void
2933
}
3034

3135
function isNextJsImage(
32-
slide: SlideImage,
33-
): slide is Required<Pick<SlideImage, 'width' | 'height'>> & SlideImage {
36+
slide: CustomSlide,
37+
): slide is Required<Pick<CustomSlide, 'width' | 'height'>> & CustomSlide {
3438
return isImageSlide(slide) && typeof slide.width === 'number' && typeof slide.height === 'number'
3539
}
3640

37-
function NextJsImage({ slide, offset, rect }: RenderSlideProps<SlideImage>) {
41+
function NextJsImage({ slide, offset, rect }: RenderSlideProps<CustomSlide>) {
3842
const {
3943
on: { click },
4044
carousel: { imageFit },
@@ -67,10 +71,19 @@ export default function ImageLightbox({ slides, index, open, close }: ImageLight
6771
return (
6872
<Lightbox
6973
index={index}
74+
plugins={[Video, Thumbnails, Counter, Captions]}
7075
slides={slides}
7176
open={open}
7277
close={close}
7378
render={{ slide: NextJsImage }}
79+
controller={{
80+
closeOnBackdropClick: true,
81+
}}
82+
thumbnails={{
83+
border: 2,
84+
borderRadius: 2,
85+
padding: 0,
86+
}}
7487
/>
7588
)
7689
}

src/app/(frontend)/[locale]/moments/MomentCard.tsx

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,35 @@ import ImageGrid from './ImageGrid'
22
import RichText from '@/components/RichText'
33
import { useLocale } from 'next-intl'
44
import { LocalTime } from '../../components/LocalTime'
5-
5+
import Mood from './Mood'
66
import { Locale } from '@/i18n/types'
7+
78
interface MomentCardProps {
89
moment: any
910
}
1011

11-
const MOOD_MAP: Record<string, { label: string; emoji: string }> = {
12-
happy: { label: '开心', emoji: '😊' },
13-
calm: { label: '平静', emoji: '😌' },
14-
thoughtful: { label: '思考', emoji: '🤔' },
15-
tired: { label: '疲惫', emoji: '😴' },
16-
sad: { label: '低落', emoji: '😢' },
17-
energized: { label: '充实', emoji: '🔥' },
18-
content: { label: '满足', emoji: '😎' },
19-
neutral: { label: '无感', emoji: '😐' },
20-
}
21-
2212
export default function MomentCard({ moment }: MomentCardProps) {
23-
const mood = moment.mood ? MOOD_MAP[moment.mood] : null
2413
const locale: Locale = useLocale() as Locale
2514

2615
return (
27-
<article className="space-y-4">
28-
{/* 时间 + 心情 */}
29-
<div className="flex items-center gap-2 text-sm text-gray-500">
30-
<time>
31-
<LocalTime date={moment.publishedAt} locale={locale} />
32-
</time>
33-
{mood && (
34-
<span title={mood.label} className="flex items-center gap-1">
35-
<span>{mood.emoji}</span>
36-
</span>
37-
)}
38-
</div>
39-
16+
<article className="flex flex-col md:flex-row gap-4 md:gap-16">
4017
{/* 图片 */}
41-
{moment.images?.length > 0 && <ImageGrid images={moment.images} />}
18+
<div className="md:basis-3/5 shrink-0">
19+
<ImageGrid images={moment.images} />
20+
</div>
4221

43-
{/* 内容 */}
44-
{moment.content && (
45-
<div className="prose prose-sm max-w-none">
46-
<RichText className="max-w-[48rem] mx-auto" data={moment.content} enableGutter={false} />
22+
{/* 时间 + 心情 */}
23+
<div className="md:basis-2/5 space-y-3 group ">
24+
<div className="flex items-center gap-2 text-sm text-gray-500">
25+
<time>
26+
<LocalTime date={moment.publishedAt} locale={locale} />
27+
</time>
28+
{moment.mood && <Mood mood={moment.mood} />}
4729
</div>
48-
)}
30+
31+
{/* 内容 */}
32+
{moment.content && <RichText data={moment.content} enableGutter={false} />}
33+
</div>
4934
</article>
5035
)
5136
}

src/app/(frontend)/[locale]/moments/MomentsFeed.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import React from 'react'
34
import MomentCard from './MomentCard'
45

56
interface MomentsFeedProps {
@@ -13,8 +14,11 @@ export default function MomentsFeed({ moments }: MomentsFeedProps) {
1314

1415
return (
1516
<div className="space-y-12">
16-
{moments.map((moment) => (
17-
<MomentCard key={moment.id} moment={moment} />
17+
{moments.map((moment, index) => (
18+
<React.Fragment key={moment.id}>
19+
{index > 0 && <div className="h-px w-full bg-gray-200 dark:bg-gray-700" />}
20+
<MomentCard moment={moment} />
21+
</React.Fragment>
1822
))}
1923
</div>
2024
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'use client'
2+
import React, { useState } from 'react'
3+
import { useTranslations } from 'next-intl'
4+
5+
type MoodKey = keyof typeof MOOD_MAP
6+
interface MoodProps {
7+
mood: MoodKey
8+
}
9+
10+
const MOOD_MAP: Record<string, string> = {
11+
happy: '😊',
12+
calm: '😌',
13+
thoughtful: '🤔',
14+
tired: '😴',
15+
sad: '😢',
16+
energized: '🔥',
17+
content: '😎',
18+
neutral: '😐',
19+
}
20+
const MOOD_MAP_KEY_LIST = Object.keys(MOOD_MAP)
21+
22+
const Mood: React.FC<MoodProps> = ({ mood }) => {
23+
const t = useTranslations('Moments')
24+
25+
const [currentMood, setCurrentMood] = useState(mood)
26+
27+
const handleClickMood = () => {
28+
// const toChange = Math.random() > 0.7
29+
// if (toChange) {
30+
const randomIndex = Math.floor(Math.random() * MOOD_MAP_KEY_LIST.length)
31+
setCurrentMood(MOOD_MAP_KEY_LIST[randomIndex])
32+
// }
33+
}
34+
35+
return (
36+
<span
37+
className="flex items-center gap-1 transition-transform duration-300 hover:scale-150 cursor-pointer select-none"
38+
onClick={handleClickMood}
39+
title={t(`mood.${currentMood}`)}
40+
>
41+
{MOOD_MAP[currentMood]}
42+
</span>
43+
)
44+
}
45+
46+
export default Mood

0 commit comments

Comments
 (0)