Skip to content

Commit 13d455b

Browse files
committed
feat: add application shell
1 parent 523d91f commit 13d455b

File tree

14 files changed

+433
-95
lines changed

14 files changed

+433
-95
lines changed

apps/portal/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
root: true,
3-
extends: ['tih', 'next/core-web-vitals'],
3+
extends: ['tih'],
44
parserOptions: {
55
tsconfigRootDir: __dirname,
66
project: ['./tsconfig.json'],

apps/portal/next.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ function defineNextConfig(config) {
1313
}
1414

1515
export default defineNextConfig({
16+
experimental: {
17+
newNextLinkBehavior: true,
18+
},
1619
reactStrictMode: true,
1720
swcMinify: true,
1821
});

apps/portal/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
"prisma": "prisma"
1313
},
1414
"dependencies": {
15+
"@headlessui/react": "^1.7.2",
16+
"@heroicons/react": "^2.0.11",
1517
"@next-auth/prisma-adapter": "^1.0.4",
1618
"@prisma/client": "^4.4.0",
1719
"@tih/ui": "*",
1820
"@trpc/client": "^9.27.2",
1921
"@trpc/next": "^9.27.2",
2022
"@trpc/react": "^9.27.2",
2123
"@trpc/server": "^9.27.2",
24+
"clsx": "^1.2.1",
2225
"next": "12.3.1",
2326
"next-auth": "~4.10.3",
2427
"react": "18.2.0",
@@ -28,6 +31,10 @@
2831
"zod": "^3.18.0"
2932
},
3033
"devDependencies": {
34+
"@tailwindcss/aspect-ratio": "^0.4.2",
35+
"@tailwindcss/forms": "^0.5.3",
36+
"@tailwindcss/line-clamp": "^0.4.2",
37+
"@tailwindcss/typography": "^0.5.7",
3138
"@tih/tsconfig": "*",
3239
"@types/node": "18.0.0",
3340
"@types/react": "18.0.21",
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import clsx from 'clsx';
2+
import Link from 'next/link';
3+
import { signIn, signOut, useSession } from 'next-auth/react';
4+
import type { ReactNode } from 'react';
5+
import { Fragment, useState } from 'react';
6+
import { Dialog, Menu, Transition } from '@headlessui/react';
7+
import {
8+
Bars3BottomLeftIcon,
9+
BriefcaseIcon,
10+
CurrencyDollarIcon,
11+
DocumentTextIcon,
12+
HomeIcon,
13+
XMarkIcon,
14+
} from '@heroicons/react/24/outline';
15+
16+
const sidebarNavigation = [
17+
{ current: false, href: '/', icon: HomeIcon, name: 'Home' },
18+
{ current: false, href: '/resumes', icon: DocumentTextIcon, name: 'Resumes' },
19+
{
20+
current: false,
21+
href: '/questions',
22+
icon: BriefcaseIcon,
23+
name: 'Questions',
24+
},
25+
{ current: false, href: '/offers', icon: CurrencyDollarIcon, name: 'Offers' },
26+
];
27+
28+
type Props = Readonly<{
29+
children: ReactNode;
30+
}>;
31+
32+
function ProfileJewel() {
33+
const { data: session, status } = useSession();
34+
const isSessionLoading = status === 'loading';
35+
36+
if (isSessionLoading) {
37+
return null;
38+
}
39+
40+
if (session == null) {
41+
return (
42+
<a
43+
href="/api/auth/signin"
44+
onClick={(event) => {
45+
event.preventDefault();
46+
signIn();
47+
}}>
48+
Sign in
49+
</a>
50+
);
51+
}
52+
53+
const userNavigation = [
54+
{ href: '/profile', name: 'Profile' },
55+
{
56+
href: '/api/auth/signout',
57+
name: 'Sign out',
58+
onClick: (event: MouseEvent) => {
59+
event.preventDefault();
60+
signOut();
61+
},
62+
},
63+
];
64+
65+
return (
66+
<Menu as="div" className="relative flex-shrink-0">
67+
<div>
68+
<Menu.Button className="flex rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
69+
<span className="sr-only">Open user menu</span>
70+
{session?.user?.image == null ? (
71+
<span>Render some icon</span>
72+
) : (
73+
<img
74+
alt={session?.user?.email ?? session?.user?.name ?? ''}
75+
className="h-8 w-8 rounded-full"
76+
src={session?.user.image}
77+
/>
78+
)}
79+
</Menu.Button>
80+
</div>
81+
<Transition
82+
as={Fragment}
83+
enter="transition ease-out duration-100"
84+
enterFrom="transform opacity-0 scale-95"
85+
enterTo="transform opacity-100 scale-100"
86+
leave="transition ease-in duration-75"
87+
leaveFrom="transform opacity-100 scale-100"
88+
leaveTo="transform opacity-0 scale-95">
89+
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
90+
{userNavigation.map((item) => (
91+
<Menu.Item key={item.name}>
92+
{({ active }) => (
93+
<Link
94+
className={clsx(
95+
active ? 'bg-gray-100' : '',
96+
'block px-4 py-2 text-sm text-gray-700',
97+
)}
98+
href={item.href}
99+
onClick={item.onClick}>
100+
{item.name}
101+
</Link>
102+
)}
103+
</Menu.Item>
104+
))}
105+
</Menu.Items>
106+
</Transition>
107+
</Menu>
108+
);
109+
}
110+
111+
export default function AppShell({ children }: Props) {
112+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
113+
114+
return (
115+
<div className="flex min-h-screen h-full">
116+
{/* Narrow sidebar */}
117+
<div className="hidden w-28 overflow-y-auto bg-indigo-700 md:block">
118+
<div className="flex w-full flex-col items-center py-6">
119+
<div className="flex flex-shrink-0 items-center">
120+
<img
121+
alt="Your Company"
122+
className="h-8 w-auto"
123+
src="https://tailwindui.com/img/logos/mark.svg?color=white"
124+
/>
125+
</div>
126+
<div className="mt-6 w-full flex-1 space-y-1 px-2">
127+
{sidebarNavigation.map((item) => (
128+
<Link
129+
key={item.name}
130+
aria-current={item.current ? 'page' : undefined}
131+
className={clsx(
132+
item.current
133+
? 'bg-indigo-800 text-white'
134+
: 'text-indigo-100 hover:bg-indigo-800 hover:text-white',
135+
'group w-full p-3 rounded-md flex flex-col items-center text-xs font-medium',
136+
)}
137+
href={item.href}>
138+
<item.icon
139+
aria-hidden="true"
140+
className={clsx(
141+
item.current
142+
? 'text-white'
143+
: 'text-indigo-300 group-hover:text-white',
144+
'h-6 w-6',
145+
)}
146+
/>
147+
<span className="mt-2">{item.name}</span>
148+
</Link>
149+
))}
150+
</div>
151+
</div>
152+
</div>
153+
154+
{/* Mobile menu */}
155+
<Transition.Root as={Fragment} show={mobileMenuOpen}>
156+
<Dialog
157+
as="div"
158+
className="relative z-20 md:hidden"
159+
onClose={setMobileMenuOpen}>
160+
<Transition.Child
161+
as={Fragment}
162+
enter="transition-opacity ease-linear duration-300"
163+
enterFrom="opacity-0"
164+
enterTo="opacity-100"
165+
leave="transition-opacity ease-linear duration-300"
166+
leaveFrom="opacity-100"
167+
leaveTo="opacity-0">
168+
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
169+
</Transition.Child>
170+
171+
<div className="fixed inset-0 z-40 flex">
172+
<Transition.Child
173+
as={Fragment}
174+
enter="transition ease-in-out duration-300 transform"
175+
enterFrom="-translate-x-full"
176+
enterTo="translate-x-0"
177+
leave="transition ease-in-out duration-300 transform"
178+
leaveFrom="translate-x-0"
179+
leaveTo="-translate-x-full">
180+
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-indigo-700 pt-5 pb-4">
181+
<Transition.Child
182+
as={Fragment}
183+
enter="ease-in-out duration-300"
184+
enterFrom="opacity-0"
185+
enterTo="opacity-100"
186+
leave="ease-in-out duration-300"
187+
leaveFrom="opacity-100"
188+
leaveTo="opacity-0">
189+
<div className="absolute top-1 right-0 -mr-14 p-1">
190+
<button
191+
className="flex h-12 w-12 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-white"
192+
type="button"
193+
onClick={() => setMobileMenuOpen(false)}>
194+
<XMarkIcon
195+
aria-hidden="true"
196+
className="h-6 w-6 text-white"
197+
/>
198+
<span className="sr-only">Close sidebar</span>
199+
</button>
200+
</div>
201+
</Transition.Child>
202+
<div className="flex flex-shrink-0 items-center px-4">
203+
<img
204+
alt="Your Company"
205+
className="h-8 w-auto"
206+
src="https://tailwindui.com/img/logos/mark.svg?color=white"
207+
/>
208+
</div>
209+
<div className="mt-5 h-0 flex-1 overflow-y-auto px-2">
210+
<nav className="flex h-full flex-col">
211+
<div className="space-y-1">
212+
{sidebarNavigation.map((item) => (
213+
<a
214+
key={item.name}
215+
aria-current={item.current ? 'page' : undefined}
216+
className={clsx(
217+
item.current
218+
? 'bg-indigo-800 text-white'
219+
: 'text-indigo-100 hover:bg-indigo-800 hover:text-white',
220+
'group py-2 px-3 rounded-md flex items-center text-sm font-medium',
221+
)}
222+
href={item.href}>
223+
<item.icon
224+
aria-hidden="true"
225+
className={clsx(
226+
item.current
227+
? 'text-white'
228+
: 'text-indigo-300 group-hover:text-white',
229+
'mr-3 h-6 w-6',
230+
)}
231+
/>
232+
<span>{item.name}</span>
233+
</a>
234+
))}
235+
</div>
236+
</nav>
237+
</div>
238+
</Dialog.Panel>
239+
</Transition.Child>
240+
<div aria-hidden="true" className="w-14 flex-shrink-0">
241+
{/* Dummy element to force sidebar to shrink to fit close icon */}
242+
</div>
243+
</div>
244+
</Dialog>
245+
</Transition.Root>
246+
247+
{/* Content area */}
248+
<div className="flex flex-1 flex-col overflow-hidden">
249+
<header className="w-full">
250+
<div className="relative z-10 flex h-16 flex-shrink-0 border-b border-gray-200 bg-white shadow-sm">
251+
<button
252+
className="border-r border-gray-200 px-4 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 md:hidden"
253+
type="button"
254+
onClick={() => setMobileMenuOpen(true)}>
255+
<span className="sr-only">Open sidebar</span>
256+
<Bars3BottomLeftIcon aria-hidden="true" className="h-6 w-6" />
257+
</button>
258+
<div className="flex flex-1 justify-between px-4 sm:px-6">
259+
<div className="flex flex-1 items-center">Some menu items</div>
260+
<div className="ml-2 flex items-center space-x-4 sm:ml-6 sm:space-x-6">
261+
<ProfileJewel />
262+
</div>
263+
</div>
264+
</div>
265+
</header>
266+
267+
{/* Main content */}
268+
<div className="flex flex-1 items-stretch overflow-hidden">
269+
{children}
270+
</div>
271+
</div>
272+
</div>
273+
);
274+
}

apps/portal/src/pages/_app.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
66
import { loggerLink } from '@trpc/client/links/loggerLink';
77
import { withTRPC } from '@trpc/next';
88

9+
import AppShell from '~/components/global/AppShell';
10+
911
import type { AppRouter } from '~/server/router';
1012

1113
import '~/styles/globals.css';
@@ -16,7 +18,9 @@ const MyApp: AppType<{ session: Session | null }> = ({
1618
}) => {
1719
return (
1820
<SessionProvider session={session}>
19-
<Component {...pageProps} />
21+
<AppShell>
22+
<Component {...pageProps} />
23+
</AppShell>
2024
</SessionProvider>
2125
);
2226
};

apps/portal/src/pages/_document.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Head, Html, Main, NextScript } from 'next/document';
2+
3+
export default function Document() {
4+
return (
5+
<Html className="h-full bg-gray-50">
6+
<Head />
7+
<body className="h-full overflow-hidden">
8+
<Main />
9+
<NextScript />
10+
</body>
11+
</Html>
12+
);
13+
}

apps/portal/src/pages/example.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { trpc } from '~/utils/trpc';
2+
3+
export default function HomePage() {
4+
const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
5+
const getAll = trpc.useQuery(['example.getAll']);
6+
7+
return (
8+
<>
9+
<main className="flex-1 overflow-y-auto">
10+
{/* Primary column */}
11+
<section
12+
aria-labelledby="primary-heading"
13+
className="flex h-full min-w-0 flex-1 flex-col lg:order-last">
14+
<h1 className="sr-only" id="primary-heading">
15+
Photos
16+
</h1>
17+
<div className="pt-6 text-2xl text-blue-500 flex justify-center items-center w-full">
18+
{hello.data ? <p>{hello.data.greeting}</p> : <p>Loading..</p>}
19+
</div>
20+
<pre className="w-1/2">{JSON.stringify(getAll.data, null, 2)}</pre>
21+
</section>
22+
</main>
23+
24+
{/* Secondary column (hidden on smaller screens) */}
25+
<aside className="hidden w-96 overflow-y-auto border-l border-gray-200 bg-white lg:block">
26+
{/* Your content */}
27+
</aside>
28+
</>
29+
);
30+
}

0 commit comments

Comments
 (0)