Skip to content

Commit 4139ded

Browse files
committed
Add search.
1 parent 119ebeb commit 4139ded

File tree

11 files changed

+804
-499
lines changed

11 files changed

+804
-499
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2022 Vercel, Inc.
3+
Copyright (c) 2023 Vercel, Inc.
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal
File renamed without changes.

app/head.tsx

Lines changed: 0 additions & 13 deletions
This file was deleted.

app/layout.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import './globals.css';
22
import '@tremor/react/dist/esm/tremor.css';
33

4-
import Navbar from './navbar';
4+
import Nav from './nav';
55
import AnalyticsWrapper from './analytics';
6-
import { unstable_getServerSession } from 'next-auth/next';
76
import Toast from './toast';
7+
import { Suspense } from 'react';
8+
9+
export const metadata = {
10+
title: 'Next.js 13 + PlanetScale + NextAuth + Tailwind CSS',
11+
description:
12+
'A user admin dashboard configured with Next.js, PlanetScale, NextAuth, Tailwind CSS, TypeScript, ESLint, and Prettier.'
13+
};
814

915
export default async function RootLayout({
1016
children
1117
}: {
1218
children: React.ReactNode;
1319
}) {
14-
const session = await unstable_getServerSession();
1520
return (
1621
<html lang="en" className="h-full bg-gray-50">
1722
<body className="h-full">
18-
<Navbar user={session?.user} />
23+
<Suspense fallback="...">
24+
{/* @ts-expect-error Server Component */}
25+
<Nav />
26+
</Suspense>
1927
{children}
2028
<AnalyticsWrapper />
2129
<Toast />

app/loading.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ export default async function Loading() {
55
<main className="p-4 md:p-10 mx-auto max-w-7xl">
66
<Title>Users</Title>
77
<Text>
8-
A list of users retrieved from an external API and automatically cached
9-
static.
8+
A list of users retrieved from a MySQL database (PlanetScale).
109
</Text>
1110
<div className="tremor-base tr-relative tr-w-full tr-mx-auto tr-text-left tr-ring-1 tr-mt-6 tr-max-w-none tr-bg-white tr-shadow tr-border-blue-400 tr-ring-gray-200 tr-pl-6 tr-pr-6 tr-pt-6 tr-pb-6 tr-rounded-lg h-[360px]" />
1211
</main>

app/nav.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Navbar from './navbar';
2+
import { getServerSession } from 'next-auth/next';
3+
4+
export default async function Nav() {
5+
const session = await getServerSession();
6+
return <Navbar user={session?.user} />;
7+
}

app/page.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
import { Card, Title, Text } from '@tremor/react';
22
import { queryBuilder } from '../lib/planetscale';
3+
import Search from './search';
34
import UsersTable from './table';
45

5-
export const dynamic = 'force-dynamic',
6-
runtime = 'experimental-edge',
7-
preferredRegion = 'home';
6+
export const dynamic = 'force-dynamic';
87

9-
export default async function IndexPage() {
8+
export default async function IndexPage({
9+
searchParams
10+
}: {
11+
searchParams: { q: string };
12+
}) {
13+
const search = searchParams.q ?? '';
1014
const users = await queryBuilder
1115
.selectFrom('users')
1216
.select(['id', 'name', 'username', 'email'])
17+
.where('name', 'like', `%${search}%`)
1318
.execute();
1419

1520
return (
1621
<main className="p-4 md:p-10 mx-auto max-w-7xl">
1722
<Title>Users</Title>
1823
<Text>
19-
A list of users retrieved from an external API and automatically cached
20-
static.
24+
A list of users retrieved from a MySQL database (PlanetScale).
2125
</Text>
26+
<Search />
2227
<Card marginTop="mt-6">
2328
{/* @ts-expect-error Server Component */}
2429
<UsersTable users={users} />

app/search.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use client';
2+
3+
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
4+
import { usePathname, useRouter } from 'next/navigation';
5+
import { useState, useTransition } from 'react';
6+
7+
export default function Search() {
8+
const [focused, setFocused] = useState(false);
9+
const { replace } = useRouter();
10+
const pathname = usePathname();
11+
12+
const [isPending, startTransition] = useTransition();
13+
14+
function handleSearch(term: string) {
15+
const params = new URLSearchParams(window.location.search);
16+
if (term) {
17+
params.set('q', term);
18+
} else {
19+
params.delete('q');
20+
}
21+
22+
startTransition(() => {
23+
replace(`${pathname}?${params.toString()}`);
24+
});
25+
}
26+
27+
return (
28+
<div className="relative mt-5 max-w-md">
29+
<label htmlFor="search" className="sr-only">
30+
Search
31+
</label>
32+
<div className="relative mt-1 rounded-md shadow-sm">
33+
<div
34+
className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
35+
aria-hidden="true"
36+
>
37+
<MagnifyingGlassIcon
38+
className="mr-3 h-4 w-4 text-gray-400"
39+
aria-hidden="true"
40+
/>
41+
</div>
42+
<input
43+
type="text"
44+
name="search"
45+
id="search"
46+
className="h-10 block w-full rounded-md border border-gray-200 pl-9 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
47+
placeholder="Search by name..."
48+
spellCheck={false}
49+
onFocus={() => setFocused(true)}
50+
onBlur={() => setFocused(false)}
51+
onChange={(e) => handleSearch(e.target.value)}
52+
/>
53+
</div>
54+
55+
{true && (
56+
<div className="absolute right-0 top-0 bottom-0 flex items-center justify-center">
57+
<svg
58+
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
59+
xmlns="http://www.w3.org/2000/svg"
60+
fill="none"
61+
viewBox="0 0 24 24"
62+
>
63+
<circle
64+
className="opacity-25"
65+
cx="12"
66+
cy="12"
67+
r="10"
68+
stroke="currentColor"
69+
stroke-width="4"
70+
/>
71+
<path
72+
className="opacity-75"
73+
fill="currentColor"
74+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
75+
/>
76+
</svg>
77+
</div>
78+
)}
79+
</div>
80+
);
81+
}

package.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@
88
"lint": "next lint"
99
},
1010
"dependencies": {
11-
"@headlessui/react": "^1.7.4",
12-
"@heroicons/react": "^2.0.13",
13-
"@planetscale/database": "^1.4.0",
14-
"@tremor/react": "^1.1.5",
15-
"@types/js-cookie": "^3.0.2",
16-
"@types/node": "18.11.9",
17-
"@types/react": "18.0.25",
18-
"@types/react-dom": "18.0.9",
19-
"@vercel/analytics": "^0.1.5",
11+
"@headlessui/react": "^1.7.11",
12+
"@heroicons/react": "^2.0.16",
13+
"@planetscale/database": "^1.5.0",
14+
"@tremor/react": "^1.8.0",
15+
"@types/js-cookie": "^3.0.3",
16+
"@types/node": "18.14.0",
17+
"@types/react": "18.0.28",
18+
"@types/react-dom": "18.0.11",
19+
"@vercel/analytics": "^0.1.10",
2020
"autoprefixer": "^10.4.13",
21-
"eslint": "8.28.0",
22-
"eslint-config-next": "13.0.5",
21+
"eslint": "8.34.0",
22+
"eslint-config-next": "13.1.6",
2323
"js-cookie": "^3.0.1",
24-
"kysely": "^0.22.0",
25-
"kysely-planetscale": "^1.1.0",
26-
"next": "13.0.5",
27-
"next-auth": "^4.17.0",
28-
"postcss": "^8.4.19",
29-
"prettier": "^2.8.0",
24+
"kysely": "^0.23.4",
25+
"kysely-planetscale": "^1.3.0",
26+
"next": "13.1.7-canary.21",
27+
"next-auth": "^4.19.2",
28+
"postcss": "^8.4.21",
29+
"prettier": "^2.8.4",
3030
"prop-types": "^15.8.1",
3131
"react": "18.2.0",
3232
"react-dom": "18.2.0",
3333
"server-only": "^0.0.1",
34-
"tailwindcss": "^3.2.4",
35-
"typescript": "4.9.3"
34+
"tailwindcss": "^3.2.7",
35+
"typescript": "4.9.5"
3636
},
3737
"prettier": {
3838
"arrowParens": "always",

0 commit comments

Comments
 (0)