diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..adade3e
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,23 @@
+import Link from 'next/link'
+import Spinner from '../components/Spinner'
+import Wordmark from '../components/Wordmark'
+
+export const metadata = {
+ title: 'FilePizza - 404: Slice Not Found',
+ description: 'Oops! This slice of FilePizza seems to be missing.',
+}
+
+export default function NotFound(): JSX.Element {
+ return (
+
+
+
+
+ 404: Looks like this slice of FilePizza is missing!
+
+
+ Serve up a fresh slice »
+
+
+ )
+}
diff --git a/src/components/InputLabel.tsx b/src/components/InputLabel.tsx
new file mode 100644
index 0000000..1f383a6
--- /dev/null
+++ b/src/components/InputLabel.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+export default function InputLabel({
+ children,
+}: {
+ children: React.ReactNode
+}): JSX.Element {
+ return (
+
+ )
+}
diff --git a/src/hooks/useUploaderChannelRenewal.ts b/src/hooks/useUploaderChannelRenewal.ts
new file mode 100644
index 0000000..e1aefc8
--- /dev/null
+++ b/src/hooks/useUploaderChannelRenewal.ts
@@ -0,0 +1,40 @@
+import { useEffect } from 'react'
+import { useMutation } from '@tanstack/react-query'
+
+export function useUploaderChannelRenewal(
+ shortSlug: string | undefined,
+ renewInterval = 5000,
+): void {
+ const mutation = useMutation({
+ mutationFn: async () => {
+ const response = await fetch('/api/renew', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ slug: shortSlug }),
+ })
+ if (!response.ok) {
+ throw new Error('Network response was not ok')
+ }
+ return response.json()
+ },
+ })
+
+ useEffect(() => {
+ if (!shortSlug) return
+
+ let timeout: NodeJS.Timeout | null = null
+
+ const run = (): void => {
+ timeout = setTimeout(() => {
+ mutation.mutate()
+ run()
+ }, renewInterval)
+ }
+
+ run()
+
+ return () => {
+ if (timeout) clearTimeout(timeout)
+ }
+ }, [shortSlug, mutation, renewInterval])
+}