Skip to content

Commit

Permalink
feat: add projects page
Browse files Browse the repository at this point in the history
  • Loading branch information
CaliCastle committed May 31, 2023
1 parent 0e17c5c commit c694352
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 11 deletions.
93 changes: 93 additions & 0 deletions app/(main)/projects/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client'

import {
AnimatePresence,
useMotionTemplate,
useMotionValue,
} from 'framer-motion'
import { motion } from 'framer-motion'
import Image from 'next/image'
import React from 'react'

import { ExternalLinkIcon } from '~/assets'
import { Card } from '~/components/ui/Card'
import { urlForImage } from '~/sanity/lib/image'
import { type Project } from '~/sanity/schemas/project'

export function ProjectCard({ project }: { project: Project }) {
const { _id, url, icon, name, description } = project

const mouseX = useMotionValue(0)
const mouseY = useMotionValue(0)
const radius = useMotionValue(0)
const handleMouseMove = React.useCallback(
({ clientX, clientY, currentTarget }: React.MouseEvent) => {
const bounds = currentTarget.getBoundingClientRect()
mouseX.set(clientX - bounds.left)
mouseY.set(clientY - bounds.top)
radius.set(Math.sqrt(bounds.width ** 2 + bounds.height ** 2) / 2)
console.log({
mouseX: mouseX.get(),
mouseY: mouseY.get(),
radius: radius.get(),
})
},
[mouseX, mouseY, radius]
)
const maskBackground = useMotionTemplate`radial-gradient(circle ${radius}px at ${mouseX}px ${mouseY}px, black 40%, transparent)`
const [isHovering, setIsHovering] = React.useState(false)

return (
<Card
as="li"
key={_id}
onMouseEnter={() => setIsHovering(true)}
onMouseMove={handleMouseMove}
onMouseLeave={() => setIsHovering(false)}
>
<div className="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-md shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0">
<Image
src={urlForImage(icon)?.size(100, 100).auto('format').url()}
alt=""
width={36}
height={36}
className="h-9 w-9 rounded-full"
unoptimized
/>
</div>
<h2 className="mt-6 text-base font-bold text-zinc-800 dark:text-zinc-100">
<Card.Link href={url}>{name}</Card.Link>
</h2>
<Card.Description>{description}</Card.Description>
<p className="pointer-events-none relative z-40 mt-6 flex items-center text-sm font-medium text-zinc-400 transition group-hover:-translate-y-0.5 group-hover:text-lime-600 dark:text-zinc-200 dark:group-hover:text-lime-400">
<span className="mr-2">{new URL(url).host}</span>
<ExternalLinkIcon className="h-4 w-4 flex-none" />
</p>

<AnimatePresence>
{isHovering && (
<motion.footer
className="pointer-events-none absolute -inset-x-4 -inset-y-6 z-30 select-none px-4 py-6 sm:-inset-x-6 sm:rounded-2xl sm:px-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
style={{
WebkitMaskImage: maskBackground,
}}
exit={{ opacity: 0 }}
>
<div className="absolute inset-x-px inset-y-px rounded-2xl border border-dashed border-zinc-900/30 dark:border-zinc-100/20" />
<div className="flex h-12 w-12 items-center justify-center rounded-full border border-dashed border-zinc-900/20 bg-white dark:border-zinc-100/20 dark:bg-zinc-800">
<div className="h-9 w-9 rounded-full border border-dashed border-zinc-900/40 dark:border-zinc-100/60 dark:bg-zinc-900/20" />
</div>
<h2 className="mt-6 text-base font-bold text-zinc-50 [text-shadow:rgb(0,0,0)_-0.5px_0.5px_0px,rgb(0,0,0)_0.5px_0.5px_0px,rgb(0,0,0)_0.5px_-0.5px_0px,rgb(0,0,0)_-0.5px_-0.5px_0px] dark:text-zinc-900 dark:[text-shadow:rgb(255,255,255)_-0.5px_0.5px_0px,rgb(255,255,255)_0.5px_0.5px_0px,rgb(255,255,255)_0.5px_-0.5px_0px,rgb(255,255,255)_-0.5px_-0.5px_0px]">
{name}
</h2>
<p className="mt-2 text-sm text-zinc-600 opacity-50 dark:text-zinc-400">
{description}
</p>
</motion.footer>
)}
</AnimatePresence>
</Card>
)
}
17 changes: 17 additions & 0 deletions app/(main)/projects/Projects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ProjectCard } from '~/app/(main)/projects/ProjectCard'
import { getSettings } from '~/sanity/queries'

export async function Projects() {
const { projects } = await getSettings()

return (
<ul
role="list"
className="grid grid-cols-1 gap-x-12 gap-y-16 sm:grid-cols-2 lg:grid-cols-3"
>
{projects.map((project) => (
<ProjectCard project={project} key={project._id} />
))}
</ul>
)
}
Binary file added app/(main)/projects/opengraph-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 34 additions & 2 deletions app/(main)/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import { type Metadata } from 'next'

import { Projects } from '~/app/(main)/projects/Projects'
import { Container } from '~/components/ui/Container'

const title = '我的项目'
const description =
'多年来,我一直在做各种各样的小项目,这里就是我筛选出来我觉得还不错的项目合集,也是我在技术领域中尝试和探索的最好见证。'
export const metadata = {
title,
description,
openGraph: {
title,
description,
},
twitter: {
title,
description,
card: 'summary_large_image',
},
} satisfies Metadata

export default function ProjectsPage() {
return (
<Container>
<h1 className="mt-10">给我点时间开发一下...</h1>
<Container className="mt-16 sm:mt-32">
<header className="max-w-2xl">
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
我过去的项目冒险之旅。
</h1>
<p className="mt-6 text-base text-zinc-600 dark:text-zinc-400">
多年来,我一直在做各种各样的小项目,有<b>开源</b>的,有<b>实验</b>
的,也有 <b>just for fun </b>
的,下面就是我筛选出来我觉得还不错的项目合集,也是我在技术领域中尝试和探索的最好见证。
</p>
</header>
<div className="mt-16 sm:mt-20">
<Projects />
</div>
</Container>
)
}
Expand Down
Binary file added app/(main)/projects/twitter-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 14 additions & 3 deletions components/ui/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ export function Card({
as: Component = 'div',
className,
children,
...props
}: {
as?: keyof JSX.IntrinsicElements
className?: string
children: React.ReactNode
}) {
} & React.ComponentPropsWithoutRef<keyof JSX.IntrinsicElements>) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<Component
className={clsxm(className, 'group relative flex flex-col items-start')}
{...props}
>
{children}
</Component>
Expand All @@ -40,7 +44,7 @@ Card.Link = function CardLink({
}: LinkProps & { children: React.ReactNode }) {
return (
<>
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-200/30 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-700/20 sm:-inset-x-6 sm:rounded-2xl" />
<Link {...props}>
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
<span className="relative z-10">{children}</span>
Expand All @@ -67,11 +71,18 @@ Card.Title = function CardTitle({

Card.Description = function CardDescription({
children,
className,
}: {
children: React.ReactNode
className?: string
}) {
return (
<p className="relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
<p
className={clsxm(
'relative z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400',
className
)}
>
{children}
</p>
)
Expand Down
4 changes: 0 additions & 4 deletions env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const server = z.object({
UPSTASH_REDIS_REST_URL: z.string().min(1),
UPSTASH_REDIS_REST_TOKEN: z.string().min(1),
LINK_PREVIEW_API_BASE_URL: z.string().min(1),
SUPABASE_URL: z.string().url(),
SUPABASE_KEY: z.string().min(1),
})

const client = z.object({
Expand All @@ -36,8 +34,6 @@ const processEnv = {
NEXT_PUBLIC_SANITY_DATASET: process.env.NEXT_PUBLIC_SANITY_DATASET,
NEXT_PUBLIC_SANITY_USE_CDN: process.env.NEXT_PUBLIC_SANITY_USE_CDN == 'true',
LINK_PREVIEW_API_BASE_URL: process.env.LINK_PREVIEW_API_BASE_URL,
SUPABASE_URL: process.env.SUPABASE_URL,
SUPABASE_KEY: process.env.SUPABASE_KEY,
}

// Don't touch the part below
Expand Down
10 changes: 9 additions & 1 deletion sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import { visionTool } from '@sanity/vision'
import { defineConfig } from 'sanity'
import { deskTool } from 'sanity/desk'

import { settingsPlugin, settingsStructure } from '~/sanity/plugins/settings'

// Go to https://www.sanity.io/docs/api-versioning to learn how API versioning works
import { apiVersion, dataset, projectId } from './sanity/env'
import { schema } from './sanity/schema'
import settingsType from './sanity/schemas/settings'

export default defineConfig({
basePath: '/studio',
Expand All @@ -17,9 +20,14 @@ export default defineConfig({
// Add and edit the content schema in the './sanity/schema' folder
schema,
plugins: [
deskTool(),
deskTool({
structure: settingsStructure(settingsType),
}),
// Vision is a tool that lets you query your content with GROQ in the studio
// https://www.sanity.io/docs/the-vision-plugin
visionTool({ defaultApiVersion: apiVersion }),
settingsPlugin({
type: settingsType.name,
}),
],
})
55 changes: 55 additions & 0 deletions sanity/plugins/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { definePlugin, type DocumentDefinition } from 'sanity'
import { type StructureResolver } from 'sanity/desk'

export const settingsPlugin = definePlugin<{ type: string }>(({ type }) => {
return {
name: 'settings',
document: {
// Hide 'Settings' from new document options
newDocumentOptions: (prev, { creationContext }) => {
if (creationContext.type === 'global') {
return prev.filter((templateItem) => templateItem.templateId !== type)
}

return prev
},
// Removes the "duplicate" action on the "settings" singleton
actions: (prev, { schemaType }) => {
if (schemaType === type) {
return prev.filter(({ action }) => action !== 'duplicate')
}

return prev
},
},
}
})

// The StructureResolver is how we're changing the DeskTool
// structure to linking to a single "Settings" document
export const settingsStructure = (
typeDef: DocumentDefinition
): StructureResolver => {
return (S) => {
// The `Settings` root list item
const settingsListItem = // A singleton not using `documentListItem`, eg no built-in preview
S.listItem()
.title(typeDef.title ?? 'Settings')
.icon(typeDef.icon)
.child(
S.editor()
.id(typeDef.name)
.schemaType(typeDef.name)
.documentId(typeDef.name)
)

// The default root list items (except custom ones)
const defaultListItems = S.documentTypeListItems().filter(
(listItem) => listItem.getId() !== typeDef.name
)

return S.list()
.title('Content')
.items([settingsListItem, S.divider(), ...defaultListItems])
}
}
15 changes: 15 additions & 0 deletions sanity/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { groq } from 'next-sanity'
import { getDate } from '~/lib/date'
import { clientFetch } from '~/sanity/lib/client'
import { type Post } from '~/sanity/schemas/post'
import { type Project } from '~/sanity/schemas/project'

export const getAllLatestBlogPostSlugsQuery = () =>
groq`
Expand Down Expand Up @@ -66,3 +67,17 @@ export const getBlogPostQuery = (slug: string) =>
}`
export const getBlogPost = (slug: string) =>
clientFetch<Post | undefined>(getBlogPostQuery(slug))

export const getSettingsQuery = () =>
groq`
*[_type == "settings"][0] {
"projects": projects[]->{
_id,
name,
url,
description,
icon
}
}`
export const getSettings = () =>
clientFetch<{ projects: Project[] }>(getSettingsQuery())
4 changes: 3 additions & 1 deletion sanity/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { readingTimeType } from '~/sanity/schemas/types/readingTime'
import blockContent from './schemas/blockContent'
import category from './schemas/category'
import post from './schemas/post'
import project from './schemas/project'
import settings from './schemas/settings'

export const schema: { types: SchemaTypeDefinition[] } = {
types: [readingTimeType, post, category, blockContent],
types: [readingTimeType, post, category, blockContent, project, settings],
}
46 changes: 46 additions & 0 deletions sanity/schemas/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { defineField, defineType } from 'sanity'
import { z } from 'zod'

export const Project = z.object({
_id: z.string(),
name: z.string(),
url: z.string().url(),
description: z.string(),
icon: z.object({
_ref: z.string(),
asset: z.any(),
}),
})
export type Project = z.infer<typeof Project>

export default defineType({
name: 'project',
title: 'Project',
type: 'document',
fields: [
defineField({
name: 'name',
title: 'Name',
type: 'string',
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'url',
title: 'URL',
type: 'url',
}),
defineField({
name: 'description',
title: 'Description',
type: 'text',
}),
defineField({
name: 'icon',
title: 'Icon',
type: 'image',
options: {
hotspot: true,
},
}),
],
})
Loading

0 comments on commit c694352

Please sign in to comment.