Skip to content

Commit

Permalink
feat: update post card design
Browse files Browse the repository at this point in the history
  • Loading branch information
CaliCastle committed Jun 14, 2023
1 parent 4c2d0e0 commit 0f4636c
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 81 deletions.
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.next
74 changes: 74 additions & 0 deletions app/(main)/blog/BlogPostCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Image from 'next/image'
import Link from 'next/link'

import {
CalendarIcon,
CursorClickIcon,
HourglassIcon,
ScriptIcon,
} from '~/assets'
import { prettifyNumber } from '~/lib/math'
import { type Post } from '~/sanity/schemas/post'

export function BlogPostCard({ post, views }: { post: Post; views: number }) {
const { title, slug, mainImage, publishedAt, categories, readingTime } = post

return (
<Link
href={`/blog/${slug}`}
prefetch={false}
className="group relative w-full rounded-3xl bg-transparent ring-2 ring-zinc-800/10 dark:ring-zinc-200/10"
style={
{
'--post-image-fg': mainImage.asset.dominant?.foreground,
'--post-image-bg': mainImage.asset.dominant?.background,
'--post-image': `url(${mainImage.asset.url}`,
} as React.CSSProperties
}
>
<div className="relative aspect-[240/135] w-full">
<Image
src={mainImage.asset.url}
alt=""
className="rounded-t-3xl object-cover"
placeholder="blur"
blurDataURL={mainImage.asset.lqip}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw"
/>
</div>
<span className="relative z-10 flex w-full flex-col gap-0.5 rounded-b-3xl bg-cover bg-bottom bg-no-repeat p-4 bg-blend-overlay [background-image:var(--post-image)] before:pointer-events-none before:absolute before:inset-0 before:z-10 before:select-none before:rounded-b-3xl before:bg-[--post-image-bg] before:opacity-70 before:transition-opacity after:pointer-events-none after:absolute after:inset-0 after:z-10 after:select-none after:rounded-b-3xl after:bg-gradient-to-b after:from-transparent after:to-[--post-image-bg] after:backdrop-blur after:transition-opacity group-hover:before:opacity-30 md:p-5">
<h2 className="z-20 text-base font-bold tracking-tight text-[--post-image-fg] mix-blend-overlay md:text-xl">
{title}
</h2>

<span className="relative z-20 flex items-center justify-between mix-blend-overlay">
<span className="inline-flex items-center space-x-3">
<span className="inline-flex items-center space-x-1 text-[12px] font-medium text-[--post-image-fg] opacity-80 transition-opacity group-hover:opacity-100 md:text-sm">
<CalendarIcon />
<span>
{Intl.DateTimeFormat('zh').format(new Date(publishedAt))}
</span>
</span>

<span className="inline-flex items-center space-x-1 text-[12px] font-medium text-[--post-image-fg] opacity-80 transition-opacity group-hover:opacity-100 md:text-sm">
<ScriptIcon />
<span>{categories.join(', ')}</span>
</span>
</span>
<span className="inline-flex items-center space-x-3 text-[12px] font-medium text-[--post-image-fg] opacity-80 transition-opacity group-hover:opacity-100 md:text-xs">
<span className="inline-flex items-center space-x-1">
<CursorClickIcon />
<span>{prettifyNumber(views, true)}</span>
</span>

<span className="inline-flex items-center space-x-1">
<HourglassIcon />
<span>{readingTime.toFixed(0)}分钟阅读</span>
</span>
</span>
</span>
</span>
</Link>
)
}
78 changes: 5 additions & 73 deletions app/(main)/blog/BlogPosts.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import Image from 'next/image'
import Link from 'next/link'

import {
CalendarIcon,
CursorClickIcon,
HourglassIcon,
ScriptIcon,
} from '~/assets'
import { kvKeys } from '~/config/kv'
import { env } from '~/env.mjs'
import { prettifyNumber } from '~/lib/math'
import { redis } from '~/lib/redis'
import { getLatestBlogPosts } from '~/sanity/queries'

import { BlogPostCard } from './BlogPostCard'

export async function BlogPosts({ limit = 5 }) {
const posts = await getLatestBlogPosts({ limit, forDisplay: true })
const postIdKeys = posts.map(({ _id }) => kvKeys.postViews(_id))
Expand All @@ -26,69 +18,9 @@ export async function BlogPosts({ limit = 5 }) {

return (
<>
{posts.map(
(
{ slug, title, mainImage, publishedAt, categories, readingTime },
idx
) => (
<Link
key={idx}
href={`/blog/${slug}`}
prefetch={false}
className="group relative w-full rounded-3xl bg-transparent ring-2 ring-zinc-800/10 dark:ring-zinc-200/10"
style={
{
'--post-image-fg': mainImage.asset.dominant?.foreground,
'--post-image-bg': mainImage.asset.dominant?.background,
} as React.CSSProperties
}
>
<div className="relative aspect-[240/135] w-full">
<Image
src={mainImage.asset.url}
alt=""
className="rounded-t-3xl object-cover"
placeholder="blur"
blurDataURL={mainImage.asset.lqip}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw"
/>
</div>
<span className="relative z-10 flex w-full flex-col gap-0.5 p-4 after:pointer-events-none after:absolute after:inset-0 after:select-none after:rounded-b-3xl after:bg-[--post-image-bg] after:from-transparent after:to-[--post-image-bg] after:backdrop-blur-lg after:transition-opacity md:p-5">
<h2 className="z-20 text-base font-bold tracking-tight text-[--post-image-fg] md:text-xl">
{title}
</h2>

<span className="relative z-20 flex items-center justify-between">
<span className="inline-flex items-center space-x-3">
<span className="inline-flex items-center space-x-1 text-[12px] font-medium text-[--post-image-fg] opacity-50 transition-opacity group-hover:opacity-100 md:text-sm">
<CalendarIcon />
<span>
{Intl.DateTimeFormat('zh').format(new Date(publishedAt))}
</span>
</span>

<span className="inline-flex items-center space-x-1 text-[12px] font-medium text-[--post-image-fg] opacity-50 transition-opacity group-hover:opacity-100 md:text-sm">
<ScriptIcon />
<span>{categories.join(', ')}</span>
</span>
</span>
<span className="inline-flex items-center space-x-3 text-[12px] font-medium text-[--post-image-fg] opacity-50 transition-opacity group-hover:opacity-100 md:text-xs">
<span className="inline-flex items-center space-x-1">
<CursorClickIcon />
<span>{prettifyNumber(views[idx] ?? 0, true)}</span>
</span>

<span className="inline-flex items-center space-x-1">
<HourglassIcon />
<span>{readingTime.toFixed(0)}分钟阅读</span>
</span>
</span>
</span>
</span>
</Link>
)
)}
{posts.map((post, idx) => (
<BlogPostCard post={post} views={views[idx] ?? 0} key={post._id} />
))}
</>
)
}
17 changes: 11 additions & 6 deletions components/links/RichLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import React from 'react'

import { ExternalLinkIcon } from '~/assets'

const hostsThatNeedInvertedFavicons = ['github.com']

type RichLinkProps = LinkProps &
React.ComponentPropsWithoutRef<'a'> & {
children: React.ReactNode
Expand All @@ -15,12 +17,10 @@ type RichLinkProps = LinkProps &
}
export const RichLink = React.forwardRef<HTMLAnchorElement, RichLinkProps>(
({ children, href, favicon = true, className, ...props }, ref) => {
const hrefHost = new URL(href).host
const faviconUrl = React.useMemo(
() =>
href.startsWith('http')
? `/api/favicon?url=${new URL(href).host}`
: null,
[href]
() => (href.startsWith('http') ? `/api/favicon?url=${hrefHost}` : null),
[hrefHost]
)

// if it's a relative link, use a fallback Link
Expand All @@ -45,7 +45,12 @@ export const RichLink = React.forwardRef<HTMLAnchorElement, RichLinkProps>(
{...props}
>
{favicon && faviconUrl && (
<span className="mr-px inline-flex translate-y-0.5">
<span
className={clsxm(
'mr-px inline-flex translate-y-0.5',
hostsThatNeedInvertedFavicons.includes(hrefHost) && 'dark:invert'
)}
>
<Image
src={faviconUrl}
alt=""
Expand Down
9 changes: 9 additions & 0 deletions db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const subscribers = mysqlTable('subscribers', {
updatedAt: timestamp('updated_at').defaultNow(),
})

export const newsletters = mysqlTable('newsletters', {
id: serial('id').primaryKey(),
subject: varchar('subject', { length: 200 }),
body: text('body'),
sentAt: datetime('sent_at'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
})

export const comments = mysqlTable(
'comments',
{
Expand Down
32 changes: 32 additions & 0 deletions emails/NewslettersTemplate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react'
import ReactMarkdown from 'react-markdown'

import { Heading, Section } from './_components'
import { Layout } from './Layout'

const NewslettersTemplate = (props: {
subject?: string | null
body?: string | null
}) => {
const {
subject = '测试主题',
body = `# 你好,世界
- 你好
- 世界
`,
} = props

return (
<Layout previewText={subject ?? ''}>
<Heading>{subject}</Heading>

{body && (
<Section className="px-2 text-[14px] leading-[16px] text-zinc-700">
<ReactMarkdown>{body}</ReactMarkdown>
</Section>
)}
</Layout>
)
}

export default NewslettersTemplate
8 changes: 6 additions & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { get } from '@vercel/edge-config'

/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},

images: {
domains: ['cdn.sanity.io'],
},
Expand All @@ -33,9 +37,9 @@ const nextConfig = {
{
source: '/rss.xml',
destination: '/feed.xml',
}
},
]
}
},
}

export default nextConfig

0 comments on commit 0f4636c

Please sign in to comment.