Skip to content

feat: Partial migration to Next.js App Router and dependency updates #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

lint-staged
54 changes: 54 additions & 0 deletions app/components/navigation-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client'

import { useEffect, useRef } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
import NProgress from 'nprogress'
import { pageView as gTagPageView } from '../../lib/gtag' // Ensure path is correct

export function NavigationEvents() {
const pathname = usePathname()
const searchParams = useSearchParams()
const nprogressTimeoutRef = useRef(null)
const previousPathRef = useRef(null) // Initialize with null or current path

useEffect(() => {
const currentUrl = `${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ''}`

// Initialize previousPathRef with the first path encountered
if (previousPathRef.current === null) {
previousPathRef.current = currentUrl
// Potentially call gTagPageView for the initial load if not handled elsewhere
// gTagPageView(currentUrl); // Uncomment if initial page view isn't logged by GA script itself
return
}

// Only proceed if the path has actually changed.
if (currentUrl === previousPathRef.current) {
return
}
previousPathRef.current = currentUrl

// Clear existing timeout if any
if (nprogressTimeoutRef.current) {
clearTimeout(nprogressTimeoutRef.current)
}

// Ensure NProgress is reset/stopped before starting a new timeout
NProgress.done()

// Start NProgress after a delay
nprogressTimeoutRef.current = setTimeout(() => {
NProgress.start()
}, 250) // Original was 500ms, using 250ms as a test

// Log page view for the new URL
gTagPageView(currentUrl)

return () => {
clearTimeout(nprogressTimeoutRef.current)
NProgress.done()
}
}, [pathname, searchParams])

return null
}
47 changes: 47 additions & 0 deletions app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client' // Mark as a Client Component

import { useState, Suspense } from 'react' // Import Suspense
import '../styles/global.css'
import { SearchContext } from '../lib/search-context'
import { GA_TRACKING_ID } from '../lib/gtag'
import { NavigationEvents } from './components/navigation-events' // Import NavigationEvents

// Removed metadata export as it's not allowed in client components.
// Title will be handled directly in <head> for now.

export default function RootLayout({ children }) {
const [search, setSearch] = useState('')

return (
<html lang="en">
<head>
<title>Hyper</title> {/* Static title for now */}
{/* Google Analytics Scripts */}
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_TRACKING_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_TRACKING_ID}');
`,
}}
/>
</head>
<body>
<SearchContext.Provider value={{ search, setSearch }}>
<Suspense fallback={null}>
{' '}
{/* Wrap NavigationEvents in Suspense */}
<NavigationEvents />
</Suspense>
<main>{children}</main>
</SearchContext.Provider>
</body>
</html>
)
}
36 changes: 15 additions & 21 deletions pages/index.js → app/page.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client' // Mark as Client Component

import Page from 'components/page'
import Footer from 'components/footer'
import DownloadButton from 'components/download-button'
@@ -15,10 +17,10 @@ function Path({ os, path }) {
os === 'mac'
? '~/Library/Application Support/Hyper/'
: os === 'windows'
? '$Env:AppData/Hyper/'
: os === 'linux'
? '~/.config/Hyper/'
: ''
? '$Env:AppData/Hyper/'
: os === 'linux'
? '~/.config/Hyper/'
: ''
}${path}`}
</code>
)
@@ -84,21 +86,12 @@ const installationTableData = [
},
]

export async function getStaticProps() {
const res = await fetch(
'https://api.github.com/repos/vercel/hyper/releases/latest'
)
const latestRelease = await res.json()

return {
props: {
latestRelease,
},
revalidate: 60 * 60 * 24,
}
}
// Removed getStaticProps - data fetching will be handled differently in App Router

export default function HomePage({ latestRelease }) {
export default function HomePage({
latestRelease = { tag_name: 'Loading...' },
}) {
// Provide a default for latestRelease
const os = useOs()

return (
@@ -129,7 +122,8 @@ export default function HomePage({ latestRelease }) {
<h2 className={installationStyles.title} id="installation">
<a href="#installation">Installation</a>
</h2>
<span>latest version: {latestRelease.tag_name}</span>
{/* Conditionally render or show placeholder if latestRelease is not fully loaded */}
<span>latest version: {latestRelease?.tag_name || 'N/A'}</span>
<div className="table">
<table className={installationStyles.table}>
<tbody>
@@ -162,15 +156,15 @@ export default function HomePage({ latestRelease }) {
width={16}
className={installationStyles.icon}
/>
{latestRelease.tag_name}
{latestRelease?.tag_name || 'N/A'}
</a>
) : (
'N/A'
)}
</td>
))}
</tr>
)
),
)}
</tbody>
</table>
58 changes: 26 additions & 32 deletions components/blog/with-post.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { MDXProvider } from '@mdx-js/react'
import NextLink from 'next/link'
import styles from './with-post.module.css'
import Page from 'components/page'
import Author from './author'

const Video = ({ src, caption, oversize }) => (
export const Video = ({ src, caption, oversize }) => (
<figure>
<video
src={src}
@@ -18,13 +17,14 @@ const Video = ({ src, caption, oversize }) => (
</figure>
)

const Image = ({ src, caption, oversize, ...props }) => (
export const Image = ({ src, caption, oversize, ...props }) => (
<figure>
<img src={src} className={oversize ? styles.oversize : null} {...props} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
)

// Keep Link for now, though it won't be auto-applied to <a> via MDXProvider
const Link = ({ href, children }) => {
const IS_INTERNAL = /^\/(?!\/)/.test(href)

@@ -33,7 +33,7 @@ const Link = ({ href, children }) => {
<NextLink href={href} className={styles.link}>
{children}
</NextLink>
);
)

return (
<a
@@ -47,33 +47,27 @@ const Link = ({ href, children }) => {
)
}

const components = {
Image,
Video,
a: Link,
}

export default (meta) => ({ children }) => (
<Page
title={meta?.metaTitle}
description={meta?.metaDescription}
image={meta?.metaImage}
>
<div className={styles.root}>
<div className={styles.header}>
<h1>{meta.title}</h1>
{meta.authors && (
<div className={styles.authors}>
{meta.authors.map((author, i) => (
<Author key={i} {...author} />
))}
</div>
)}
</div>
export default (meta) =>
({ children }) => (
<Page
title={meta?.metaTitle}
description={meta?.metaDescription}
image={meta?.metaImage}
>
<div className={styles.root}>
<div className={styles.header}>
<h1>{meta.title}</h1>
{meta.authors && (
<div className={styles.authors}>
{meta.authors.map((author, i) => (
<Author key={i} {...author} />
))}
</div>
)}
</div>

<MDXProvider components={components}>
{/* MDXProvider removed, content is rendered directly */}
<div className={styles.post}>{children}</div>
</MDXProvider>
</div>
</Page>
)
</div>
</Page>
)
Loading
Oops, something went wrong.