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 ( + +
+ + Open user menu + {session?.user?.image == null ? ( + Render some icon + ) : ( + {session?.user?.email + )} + +
+ + + {userNavigation.map((item) => ( + + {({ active }) => ( + + {item.name} + + )} + + ))} + + +
+ ); +} + +export default function AppShell({ children }: Props) { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + return ( +
+ {/* Narrow sidebar */} +
+
+
+ Your Company +
+
+ {sidebarNavigation.map((item) => ( + +
+
+
+ + {/* Mobile menu */} + + + +
+ + +
+ + + +
+ +
+
+
+ Your Company +
+
+ +
+
+
+ +
+
+
+ + {/* Content area */} +
+
+
+ +
+
Some menu items
+
+ +
+
+
+
+ + {/* 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 ( -
-

{name}

-

{description}

- - Documentation - -
+
+
+

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?.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: