Skip to content

Commit

Permalink
add next 14
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed Nov 21, 2024
1 parent 3501430 commit c19f93a
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 142 deletions.
48 changes: 48 additions & 0 deletions next-14/src/app/SanityLive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use client'

import type {LiveEventMessage, LiveEventRestart, LiveEventWelcome} from '@sanity/client'
import {CorsOriginError} from '@sanity/client'
import {useRouter} from 'next/navigation'
import {useEffect} from 'react'
import {useEffectEvent} from 'use-effect-event'
import {client} from '../sanity/client'
import {expireTags} from './actions'

export function SanityLive() {
const router = useRouter()

const handleLiveEvent = useEffectEvent(
(event: LiveEventMessage | LiveEventRestart | LiveEventWelcome) => {
if (event.type === 'welcome') {
console.info('Sanity is live with automatic revalidation of published content')
} else if (event.type === 'message') {
expireTags(event.tags)
} else if (event.type === 'restart') {
router.refresh()
}
},
)
useEffect(() => {
const subscription = client.live.events().subscribe({
next: (event) => {
if (event.type === 'message' || event.type === 'restart' || event.type === 'welcome') {
handleLiveEvent(event)
}
},
error: (error: unknown) => {
if (error instanceof CorsOriginError) {
console.warn(
`Sanity Live is unable to connect to the Sanity API as the current origin - ${window.origin} - is not in the list of allowed CORS origins for this Sanity Project.`,
error.addOriginUrl && `Add it here:`,
error.addOriginUrl?.toString(),
)
} else {
console.error(error)
}
},
})
return () => subscription.unsubscribe()
}, [handleLiveEvent])

return null
}
24 changes: 24 additions & 0 deletions next-14/src/app/ThemeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import type {SyncTag} from '@sanity/client'
import {useTransition} from 'react'
import {randomColorTheme} from './actions'

export function ThemeButton({tags}: {tags: SyncTag[]}) {
const [pending, startTransition] = useTransition()
return (
<button
disabled={pending}
onClick={() =>
startTransition(async () => {
await randomColorTheme(tags)
// Wait 2 seconds to stagger requests a little bit
await new Promise((resolve) => setTimeout(resolve, 2_000))
})
}
className={`bg-theme-button text-theme-button focus:ring-theme focus:ring-offset-theme rounded-md px-4 py-2 text-sm font-semibold transition ease-in-out focus:outline-none focus:ring-2 focus:ring-opacity-50 focus:ring-offset-2 focus:duration-0 disabled:cursor-not-allowed disabled:opacity-50 ${pending ? 'animate-pulse cursor-wait duration-150' : 'duration-1000'} `}
>
{pending ? 'Generating...' : 'Random Color Theme'}
</button>
)
}
45 changes: 45 additions & 0 deletions next-14/src/app/TimeSince.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client'

import {useLayoutEffect, useState} from 'react'

export function TimeSince({label, since}: {label: string; since: string}) {
const [from, setFrom] = useState<null | Date>(null)
const [now, setNow] = useState<null | Date>(null)
useLayoutEffect(() => {
setFrom(new Date(since))
const interval = setInterval(() => setNow(new Date()), 1000)
return () => clearInterval(interval)
}, [since])

let timeSince = '…'
if (from && now) {
timeSince = formatTimeSince(from, now)
}

return (
<div className="bg-theme-button text-theme-button absolute left-2 top-2 block rounded text-xs transition duration-1000 ease-in-out">
<span className="inline-block py-1 pl-2 pr-0.5">{label}:</span>
<span className="bg-theme text-theme mr-0.5 inline-block rounded-r-sm px-1 py-0.5 tabular-nums transition duration-1000 ease-in-out">
fetched {timeSince}
</span>
</div>
)
}

const rtf = new Intl.RelativeTimeFormat('en', {style: 'short'})
export function formatTimeSince(from: Date, to: Date): string {
const seconds = Math.floor((from.getTime() - to.getTime()) / 1000)
if (seconds > -60) {
return rtf.format(Math.min(seconds, -1), 'second')
}
const minutes = Math.ceil(seconds / 60)
if (minutes > -60) {
return rtf.format(minutes, 'minute')
}
const hours = Math.ceil(minutes / 60)
if (hours > -24) {
return rtf.format(hours, 'hour')
}
const days = Math.ceil(hours / 24)
return rtf.format(days, 'day')
}
21 changes: 21 additions & 0 deletions next-14/src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use server'

import type {SyncTag} from '@sanity/client'
import {revalidateTag} from 'next/cache'

export async function expireTags(tags: SyncTag[]) {
for (const tag of tags) {
revalidateTag(tag)
}
console.log(`<SanityLive /> expired tags: ${tags.join(', ')}`)
}

export async function randomColorTheme(tags: SyncTag[]) {
const res = await fetch('https://lcapi-examples-api.sanity.dev/api/random-color-theme', {
method: 'PUT',
})
for (const tag of tags) {
revalidateTag(tag)
}
return res.json()
}
28 changes: 8 additions & 20 deletions next-14/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,14 @@
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
@property --theme-text {
syntax: '<color>';
inherits: true;
initial-value: #000;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

@layer utilities {
.text-balance {
text-wrap: balance;
}
@property --theme-background {
syntax: '<color>';
inherits: true;
initial-value: #fff;
}
56 changes: 36 additions & 20 deletions next-14/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
import type {Metadata} from 'next'
import localFont from 'next/font/local'
import './globals.css'
import {sanityFetch} from '@/sanity/fetch'
import {defineQuery} from 'groq'
import {Suspense} from 'react'
import {SanityLive} from './SanityLive'
import {ThemeButton} from './ThemeButton'
import {TimeSince} from './TimeSince'

const geistSans = localFont({
src: './fonts/GeistVF.woff',
variable: '--font-geist-sans',
weight: '100 900',
})
const geistMono = localFont({
src: './fonts/GeistMonoVF.woff',
variable: '--font-geist-mono',
weight: '100 900',
})
const THEME_QUERY = defineQuery(
`*[_id == "theme"][0]{background,text,"fetchedAt": dateTime(now())}`,
)

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}

export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
const {data, tags} = await sanityFetch({query: THEME_QUERY})

return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
<html
lang="en"
className="bg-theme text-theme transition-colors duration-1000 ease-in-out"
style={{
['--theme-background' as string]: data?.background,
['--theme-text' as string]: data?.text,
}}
>
<body>
<div className="relative flex min-h-dvh flex-col items-center justify-evenly overflow-auto">
{data?.fetchedAt && (
<Suspense>
<TimeSince label="layout.tsx" since={data.fetchedAt} />
</Suspense>
)}
{children}
<Suspense>
<ThemeButton tags={tags!} />
</Suspense>
</div>
<Suspense>
<SanityLive />
</Suspense>
</body>
</html>
)
}
126 changes: 30 additions & 96 deletions next-14/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,35 @@
import Image from 'next/image'
import {sanityFetch} from '@/sanity/fetch'
import './globals.css'
import {defineQuery} from 'groq'
import type {Metadata} from 'next'
import {Suspense} from 'react'
import {TimeSince} from './TimeSince'

export default function Home() {
return (
<div className="grid min-h-screen grid-rows-[20px_1fr_20px] items-center justify-items-center gap-16 p-8 pb-20 font-[family-name:var(--font-geist-sans)] sm:p-20">
<main className="row-start-2 flex flex-col items-center gap-8 sm:items-start">
<Image
className="dark:invert"
src="https://nextjs.org/icons/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-center font-[family-name:var(--font-geist-mono)] text-sm sm:text-left">
<li className="mb-2">
Get started by editing{' '}
<code className="rounded bg-black/[.05] px-1 py-0.5 font-semibold dark:bg-white/[.06]">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
const DEMO_QUERY = defineQuery(
`*[_type == "demo" && slug.current == $slug][0]{title,"fetchedAt": dateTime(now())}`,
)
const slug = 'next-14'

export async function generateMetadata(): Promise<Metadata> {
const {data} = await sanityFetch({query: DEMO_QUERY, params: {slug}})
return {
title: data?.title || 'Next 14',
}
}

<div className="flex flex-col items-center gap-4 sm:flex-row">
<a
className="bg-foreground text-background flex h-10 items-center justify-center gap-2 rounded-full border border-solid border-transparent px-4 text-sm transition-colors hover:bg-[#383838] sm:h-12 sm:px-5 sm:text-base dark:hover:bg-[#ccc]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="https://nextjs.org/icons/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="flex h-10 items-center justify-center rounded-full border border-solid border-black/[.08] px-4 text-sm transition-colors hover:border-transparent hover:bg-[#f2f2f2] sm:h-12 sm:min-w-44 sm:px-5 sm:text-base dark:border-white/[.145] dark:hover:bg-[#1a1a1a]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex flex-wrap items-center justify-center gap-6">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="https://nextjs.org/icons/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
export default async function Home() {
const {data} = await sanityFetch({query: DEMO_QUERY, params: {slug}})

return (
<div className="ring-theme relative mx-2 rounded-lg px-2 pb-1 pt-8 ring-1">
<h1 className="min-w-64 text-balance text-4xl font-bold leading-tight tracking-tighter md:text-6xl lg:pr-8 lg:text-8xl">
{data?.title || 'Next 14'}
</h1>
{data?.fetchedAt && (
<Suspense>
<TimeSince label="page.tsx" since={data.fetchedAt} />
</Suspense>
)}
</div>
)
}
8 changes: 8 additions & 0 deletions next-14/src/sanity/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {createClient} from '@sanity/client'

export const client = createClient({
projectId: 'hiomol4a',
dataset: 'lcapi',
apiVersion: '2024-09-22',
useCdn: false,
})
Loading

0 comments on commit c19f93a

Please sign in to comment.