diff --git a/apps/portal/.eslintrc.js b/apps/portal/.eslintrc.js
index 2fbeeeb0..07b7f196 100644
--- a/apps/portal/.eslintrc.js
+++ b/apps/portal/.eslintrc.js
@@ -1,6 +1,6 @@
module.exports = {
root: true,
- extends: ['tih', 'next/core-web-vitals'],
+ extends: ['tih'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs
index e3404510..d4194e08 100644
--- a/apps/portal/next.config.mjs
+++ b/apps/portal/next.config.mjs
@@ -13,6 +13,9 @@ function defineNextConfig(config) {
}
export default defineNextConfig({
+ experimental: {
+ newNextLinkBehavior: true,
+ },
reactStrictMode: true,
swcMinify: true,
});
diff --git a/apps/portal/package.json b/apps/portal/package.json
index bb94ff82..ee605821 100644
--- a/apps/portal/package.json
+++ b/apps/portal/package.json
@@ -12,6 +12,8 @@
"prisma": "prisma"
},
"dependencies": {
+ "@headlessui/react": "^1.7.2",
+ "@heroicons/react": "^2.0.11",
"@next-auth/prisma-adapter": "^1.0.4",
"@prisma/client": "^4.4.0",
"@tih/ui": "*",
@@ -19,6 +21,7 @@
"@trpc/next": "^9.27.2",
"@trpc/react": "^9.27.2",
"@trpc/server": "^9.27.2",
+ "clsx": "^1.2.1",
"next": "12.3.1",
"next-auth": "~4.10.3",
"react": "18.2.0",
@@ -28,6 +31,10 @@
"zod": "^3.18.0"
},
"devDependencies": {
+ "@tailwindcss/aspect-ratio": "^0.4.2",
+ "@tailwindcss/forms": "^0.5.3",
+ "@tailwindcss/line-clamp": "^0.4.2",
+ "@tailwindcss/typography": "^0.5.7",
"@tih/tsconfig": "*",
"@types/node": "18.0.0",
"@types/react": "18.0.21",
diff --git a/apps/portal/src/components/global/AppShell.tsx b/apps/portal/src/components/global/AppShell.tsx
new file mode 100644
index 00000000..b218ad31
--- /dev/null
+++ b/apps/portal/src/components/global/AppShell.tsx
@@ -0,0 +1,274 @@
+import clsx from 'clsx';
+import Link from 'next/link';
+import { signIn, signOut, useSession } from 'next-auth/react';
+import type { ReactNode } from 'react';
+import { Fragment, useState } from 'react';
+import { Dialog, Menu, Transition } from '@headlessui/react';
+import {
+ Bars3BottomLeftIcon,
+ BriefcaseIcon,
+ CurrencyDollarIcon,
+ DocumentTextIcon,
+ HomeIcon,
+ XMarkIcon,
+} from '@heroicons/react/24/outline';
+
+const sidebarNavigation = [
+ { current: false, href: '/', icon: HomeIcon, name: 'Home' },
+ { current: false, href: '/resumes', icon: DocumentTextIcon, name: 'Resumes' },
+ {
+ current: false,
+ href: '/questions',
+ icon: BriefcaseIcon,
+ name: 'Questions',
+ },
+ { current: false, href: '/offers', icon: CurrencyDollarIcon, name: 'Offers' },
+];
+
+type Props = Readonly<{
+ children: ReactNode;
+}>;
+
+function ProfileJewel() {
+ const { data: session, status } = useSession();
+ const isSessionLoading = status === 'loading';
+
+ if (isSessionLoading) {
+ return null;
+ }
+
+ if (session == null) {
+ return (
+ {
+ event.preventDefault();
+ signIn();
+ }}>
+ Sign in
+
+ );
+ }
+
+ const userNavigation = [
+ { href: '/profile', name: 'Profile' },
+ {
+ href: '/api/auth/signout',
+ name: 'Sign out',
+ onClick: (event: MouseEvent) => {
+ event.preventDefault();
+ signOut();
+ },
+ },
+ ];
+
+ return (
+
+ );
+}
+
+export default function AppShell({ children }: Props) {
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+
+ return (
+
+ {/* Narrow sidebar */}
+
+
+
+
+
+
+ {sidebarNavigation.map((item) => (
+
+
+ {item.name}
+
+ ))}
+
+
+
+
+ {/* Mobile menu */}
+
+
+
+
+ {/* Content area */}
+
+
+
+
+
+
+
+
+ {/* Main content */}
+
+ {children}
+
+
+
+ );
+}
diff --git a/apps/portal/src/env/client.mjs b/apps/portal/src/env/client.mjs
index 0dc78118..c311ae62 100644
--- a/apps/portal/src/env/client.mjs
+++ b/apps/portal/src/env/client.mjs
@@ -9,8 +9,9 @@ export const formatErrors = (
) =>
Object.entries(errors)
.map(([name, value]) => {
- if (value && '_errors' in value)
+ if (value && '_errors' in value) {
return `${name}: ${value._errors.join(', ')}\n`;
+ }
})
.filter(Boolean);
@@ -25,7 +26,7 @@ if (_clientEnv.success === false) {
/**
* Validate that client-side environment variables are exposed to the client.
*/
-for (let key of Object.keys(_clientEnv.data)) {
+for (const key of Object.keys(_clientEnv.data)) {
if (!key.startsWith('NEXT_PUBLIC_')) {
console.warn(
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
diff --git a/apps/portal/src/pages/_app.tsx b/apps/portal/src/pages/_app.tsx
index ff51f282..5de4ac99 100644
--- a/apps/portal/src/pages/_app.tsx
+++ b/apps/portal/src/pages/_app.tsx
@@ -1,11 +1,14 @@
import type { AppType } from 'next/app';
import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react';
+import React from 'react';
import superjson from 'superjson';
import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next';
+import AppShell from '~/components/global/AppShell';
+
import type { AppRouter } from '~/server/router';
import '~/styles/globals.css';
@@ -16,7 +19,9 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => {
return (
-
+
+
+
);
};
diff --git a/apps/portal/src/pages/_document.tsx b/apps/portal/src/pages/_document.tsx
new file mode 100644
index 00000000..2054857e
--- /dev/null
+++ b/apps/portal/src/pages/_document.tsx
@@ -0,0 +1,13 @@
+import { Head, Html, Main, NextScript } from 'next/document';
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/pages/api/auth/[...nextauth].ts b/apps/portal/src/pages/api/auth/[...nextauth].ts
index b19eb208..26435610 100644
--- a/apps/portal/src/pages/api/auth/[...nextauth].ts
+++ b/apps/portal/src/pages/api/auth/[...nextauth].ts
@@ -13,7 +13,9 @@ export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
- if (session.user) {
+ if (session.user != null) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
session.user.id = user.id;
}
return session;
diff --git a/apps/portal/src/pages/example.tsx b/apps/portal/src/pages/example.tsx
new file mode 100644
index 00000000..3b675af9
--- /dev/null
+++ b/apps/portal/src/pages/example.tsx
@@ -0,0 +1,30 @@
+import { trpc } from '~/utils/trpc';
+
+export default function Example() {
+ const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
+ const getAll = trpc.useQuery(['example.getAll']);
+
+ return (
+ <>
+
+ {/* Primary column */}
+
+
+ Photos
+
+
+ {hello.data ?
{hello.data.greeting}
:
Loading..
}
+
+ {JSON.stringify(getAll.data, null, 2)}
+
+
+
+ {/* Secondary column (hidden on smaller screens) */}
+
+ >
+ );
+}
diff --git a/apps/portal/src/pages/index.tsx b/apps/portal/src/pages/index.tsx
index d1597aa6..22a31105 100644
--- a/apps/portal/src/pages/index.tsx
+++ b/apps/portal/src/pages/index.tsx
@@ -1,91 +1,9 @@
-import type { NextPage } from 'next';
-import Head from 'next/head';
-import { CounterButton } from '@tih/ui';
-
-import { trpc } from '~/utils/trpc';
-
-const Home: NextPage = () => {
- const hello = trpc.useQuery(['example.hello', { text: 'from tRPC!' }]);
- const getAll = trpc.useQuery(['example.getAll']);
-
+export default function HomePage() {
return (
- <>
-
- Create T3 App
-
-
-
-
-
- Create T3 App
-
-
- This stack uses:
-
-
-
-
-
-
-
-
-
- {hello.data ?
{hello.data.greeting}
:
Loading..
}
-
- {JSON.stringify(getAll.data, null, 2)}
-
- >
- );
-};
-
-export default Home;
-
-type TechnologyCardProps = {
- description: string;
- documentation: string;
- name: string;
-};
-
-function TechnologyCard({
- name,
- description,
- documentation,
-}: TechnologyCardProps) {
- return (
-
+
+
+
Homepage
+
+
);
}
diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx
new file mode 100644
index 00000000..a57d4f94
--- /dev/null
+++ b/apps/portal/src/pages/offers/index.tsx
@@ -0,0 +1,9 @@
+export default function OffersHomePage() {
+ return (
+
+
+
Offers Research
+
+
+ );
+}
diff --git a/apps/portal/src/pages/profile.tsx b/apps/portal/src/pages/profile.tsx
new file mode 100644
index 00000000..bf849d02
--- /dev/null
+++ b/apps/portal/src/pages/profile.tsx
@@ -0,0 +1,25 @@
+import { useSession } from 'next-auth/react';
+
+export default function ProfilePage() {
+ const { data: session, status } = useSession();
+ const isSessionLoading = status === 'loading';
+
+ if (isSessionLoading) {
+ return null;
+ }
+
+ return (
+
+ Profile
+ {session?.user?.image && (
+
+ )}
+ {session?.user?.email && {session?.user?.email}
}
+ {session?.user?.name && {session?.user?.name}
}
+
+ );
+}
diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx
new file mode 100644
index 00000000..c3e45c7e
--- /dev/null
+++ b/apps/portal/src/pages/questions/index.tsx
@@ -0,0 +1,9 @@
+export default function QuestionsHomePage() {
+ return (
+
+
+
Interview Questions
+
+
+ );
+}
diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx
new file mode 100644
index 00000000..b53b4070
--- /dev/null
+++ b/apps/portal/src/pages/resumes/index.tsx
@@ -0,0 +1,9 @@
+export default function ResumeHomePage() {
+ return (
+
+
+
Resume Reviews
+
+
+ );
+}
diff --git a/apps/portal/tailwind.config.cjs b/apps/portal/tailwind.config.cjs
index db9aac2d..8fb08202 100644
--- a/apps/portal/tailwind.config.cjs
+++ b/apps/portal/tailwind.config.cjs
@@ -1,8 +1,23 @@
+const defaultTheme = require('tailwindcss/defaultTheme');
+const colors = require('tailwindcss/colors');
+
/** @type {import('tailwindcss').Config} */
module.exports = {
- content: ['./src/**/*.{js,ts,jsx,tsx}'],
+ content: ['./src/**/*.{js,jsx,ts,tsx,md,mdx}'],
theme: {
- extend: {},
+ extend: {
+ fontFamily: {
+ sans: ['Inter var', ...defaultTheme.fontFamily.sans],
+ },
+ colors: {
+ primary: colors.purple,
+ },
+ },
},
- plugins: [],
+ plugins: [
+ require('@tailwindcss/aspect-ratio'),
+ require('@tailwindcss/forms'),
+ require('@tailwindcss/line-clamp'),
+ require('@tailwindcss/typography'),
+ ],
};
diff --git a/packages/eslint-config-tih/index.js b/packages/eslint-config-tih/index.js
index a8e86895..8ee2fa69 100644
--- a/packages/eslint-config-tih/index.js
+++ b/packages/eslint-config-tih/index.js
@@ -13,7 +13,7 @@ module.exports = {
'typescript-sort-keys',
],
extends: [
- 'next',
+ 'next/core-web-vitals',
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
diff --git a/yarn.lock b/yarn.lock
index c3970f6c..bf188695 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1053,6 +1053,14 @@
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
+"@headlessui/react@^1.7.2":
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.2.tgz#e6a6a8d38342064a53182f1eb2bf6d9c1e53ba6a"
+
+"@heroicons/react@^2.0.11":
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.0.11.tgz#2c6cf4c66d81142ec87c102502407d8c353558bb"
+
"@humanwhocodes/config-array@^0.10.5":
version "0.10.6"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.6.tgz#70b53559baf544dc2cc5eea6082bf90467ccb1dc"
@@ -2171,6 +2179,29 @@
dependencies:
tslib "^2.4.0"
+"@tailwindcss/aspect-ratio@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba"
+
+"@tailwindcss/forms@^0.5.3":
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.3.tgz#e4d7989686cbcaf416c53f1523df5225332a86e7"
+ dependencies:
+ mini-svg-data-uri "^1.2.3"
+
+"@tailwindcss/line-clamp@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz#f353c5a8ab2c939c6267ac5b907f012e5ee130f9"
+
+"@tailwindcss/typography@^0.5.7":
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.7.tgz#e0b95bea787ee14c5a34a74fc824e6fe86ea8855"
+ dependencies:
+ lodash.castarray "^4.4.0"
+ lodash.isplainobject "^4.0.6"
+ lodash.merge "^4.6.2"
+ postcss-selector-parser "6.0.10"
+
"@trpc/client@^9.27.2":
version "9.27.2"
resolved "https://registry.yarnpkg.com/@trpc/client/-/client-9.27.2.tgz#1b4ae3c12c5666e81322fddd787d48b0901b75a1"
@@ -3735,6 +3766,10 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
+clsx@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
+
collapse-white-space@^1.0.2:
version "1.0.6"
resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
@@ -6722,10 +6757,18 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
+lodash.castarray@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
+
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@@ -7011,6 +7054,10 @@ min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
+mini-svg-data-uri@^1.2.3:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
+
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@@ -7896,7 +7943,7 @@ postcss-nested@5.0.6:
dependencies:
postcss-selector-parser "^6.0.6"
-postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.6:
+postcss-selector-parser@6.0.10, postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.6:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
dependencies: