diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..d6f9b14 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,2 @@ +tabWidth: 4 +semi: true \ No newline at end of file diff --git a/components/dock/dropdown/account.tsx b/components/dock/dropdown/account.tsx index 6885802..c0c201f 100644 --- a/components/dock/dropdown/account.tsx +++ b/components/dock/dropdown/account.tsx @@ -12,18 +12,27 @@ import { useAppStore } from "@/stores/app-store"; import { useUserPreferences } from "@/stores/user-preferences"; import { driver } from "driver.js"; import { Workflow, User, LifeBuoyIcon } from "lucide-react"; +import Image from "next/image"; +import { useHasHydrated } from "@/hooks/user-has-hydrated"; type Props = {}; function AccountDropdown({}: Props) { + const hasHydrated = useHasHydrated(); const setAccountConnectionsModal = useAppStore( - (state) => state.setAccountConnectionsModal + (state) => state.setAccountConnectionsModal, ); const setHasTakenTour = useUserPreferences( - (state) => state.setHasTakenTour + (state) => state.setHasTakenTour, ); + const userData = useUserPreferences((state) => state.userData); + const fullName = + userData !== undefined + ? `${userData.firstName} ${userData.lastName}` + : null; + const takeATour = () => { const config = driverObj(setHasTakenTour); driver(config).drive(); @@ -31,16 +40,29 @@ function AccountDropdown({}: Props) { return ( - - - + {hasHydrated && userData ? ( + + + + ) : null} My Account diff --git a/components/modals/OnboardingModal.tsx b/components/modals/OnboardingModal.tsx new file mode 100644 index 0000000..d0ece2c --- /dev/null +++ b/components/modals/OnboardingModal.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { FormEventHandler, useCallback, useRef } from "react"; +import { useUserPreferences } from "@/stores/user-preferences"; +import { IUserData } from "@/types"; + +export function OnboardingModal() { + const firstNameRef = useRef(null); + const lastNameRef = useRef(null); + const imageUrlRef = useRef(null); + + const userData = useUserPreferences((state) => state.userData); + const setUserPreferencesAction = useUserPreferences( + (state) => state.setUserData, + ); + + const setUserPreferences: FormEventHandler = useCallback( + (e) => { + e.preventDefault(); + const data: IUserData = { + firstName: firstNameRef.current!.value, + lastName: lastNameRef.current!.value, + imageUrl: imageUrlRef.current!.value, + }; + setUserPreferencesAction(data); + }, + [setUserPreferencesAction], + ); + + return ( + + +
+
+ + +
+ + +
+
+
+ ); +} diff --git a/hooks/user-has-hydrated.ts b/hooks/user-has-hydrated.ts new file mode 100644 index 0000000..9bd4654 --- /dev/null +++ b/hooks/user-has-hydrated.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; + +/** + * @desc Small hack around the persisted store causing hydration issues + * @see https://github.com/pmndrs/zustand/issues/324 + * @see https://github.com/pmndrs/zustand/issues/938 + */ +export function useHasHydrated() { + const [hasHydrated, setHasHydrated] = useState(false); + + useEffect(() => { + setHasHydrated(true); + }, []); + + return hasHydrated; +} diff --git a/package.json b/package.json index f3cdb56..24d74f4 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,11 @@ "eslint": "^8", "eslint-config-next": "14.0.4", "postcss": "^8", + "prettier": "^3.2.4", "tailwindcss": "^3.3.0", "typescript": "^5" + }, + "volta": { + "node": "20.11.0" } } diff --git a/pages/index.tsx b/pages/index.tsx index 9304d8a..7cb6dfe 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -19,56 +19,66 @@ import { useAppStore } from "@/stores/app-store"; import { useUserPreferences } from "@/stores/user-preferences"; import { BackgroundBlur } from "@/types.d"; import clsx from "clsx"; +import { OnboardingModal } from "@/components/modals/OnboardingModal"; +import { useHasHydrated } from "@/hooks/user-has-hydrated"; export default function Page() { + const hasHydrated = useHasHydrated(); const currentTab = useStore(useAppStore, (state) => state.currentTab); const wallpaper = useStore(useUserPreferences, (state) => state.wallpaper); const backgroundBlur = useStore( useUserPreferences, - (state) => state.backgroundBlur + (state) => state.backgroundBlur, ); const searchEnabled = useStore( useUserPreferences, - (state) => state.searchEnabled + (state) => state.searchEnabled, ); + const userData = useUserPreferences((state) => state.userData); + return ( <> - - - - - - - -
-
- - {currentTab == "home" && } - {currentTab == "atlassian" && } - {currentTab == "news" && } - {searchEnabled && } -
+ {hasHydrated && userData === undefined && } + {hasHydrated && userData !== undefined && ( + <> + + + + + + + +
+
+ + {currentTab == "home" && } + {currentTab == "atlassian" && } + {currentTab == "news" && } + {searchEnabled && } +
+ + )} {/* */} ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 845246b..a2f989d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,6 +136,9 @@ devDependencies: postcss: specifier: ^8 version: 8.4.32 + prettier: + specifier: ^3.2.4 + version: 3.2.4 tailwindcss: specifier: ^3.3.0 version: 3.4.0 @@ -4120,6 +4123,12 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /prettier@3.2.4: + resolution: {integrity: sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==} + engines: {node: '>=14'} + hasBin: true + dev: true + /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} dependencies: diff --git a/stores/user-preferences.ts b/stores/user-preferences.ts index 16f7595..c9b26fb 100644 --- a/stores/user-preferences.ts +++ b/stores/user-preferences.ts @@ -5,6 +5,7 @@ import { persist } from "zustand/middleware"; import { BackgroundBlur, IConnection, + IUserData, IUserPreferences, IUserTag, SearchEngine, @@ -17,7 +18,7 @@ type Actions = { addShortcut: (shortcut: Shortcut) => void; editShortcut: ( shortcut: Shortcut, - currentlyEditedShortcut: Shortcut + currentlyEditedShortcut: Shortcut, ) => void; changeShortcutIndex: (from: number, to: number) => void; changeWallpaper: (wallpaper: string, blurLevel?: BackgroundBlur) => void; @@ -30,6 +31,7 @@ type Actions = { removeConnection: (connectionName: string) => void; generateClientStateHash: () => string; setHasTakenTour: (hasTakenTour: boolean) => void; + setUserData: (userData: IUserData) => void; }; const INITIAL_STATE: IUserPreferences = { @@ -56,7 +58,7 @@ export const useUserPreferences = create()( }, addShortcut: (shortcut: Shortcut) => { const exists = get().shortcuts.some( - (s) => s.url === shortcut.url + (s) => s.url === shortcut.url, ); if (exists) { @@ -72,13 +74,13 @@ export const useUserPreferences = create()( removeShortcut: (shortcut: Shortcut) => { // check if shortcut exists const exists = get().shortcuts.some( - (s) => s.url === shortcut.url + (s) => s.url === shortcut.url, ); if (exists) { set({ shortcuts: get().shortcuts.filter( - (s) => s.url !== shortcut.url + (s) => s.url !== shortcut.url, ), }); } else { @@ -87,10 +89,10 @@ export const useUserPreferences = create()( }, editShortcut: ( newShortcut: Shortcut, - currentlyEditedShortcut: Shortcut + currentlyEditedShortcut: Shortcut, ) => { const exists = get().shortcuts.some( - (s) => s.url === currentlyEditedShortcut.url + (s) => s.url === currentlyEditedShortcut.url, ); if (exists) { @@ -98,7 +100,7 @@ export const useUserPreferences = create()( get().shortcuts.some( (s) => s.url === newShortcut.url && - s.url !== currentlyEditedShortcut.url + s.url !== currentlyEditedShortcut.url, ); if (newShortcutCollidesButNotWithItself) { @@ -131,7 +133,7 @@ export const useUserPreferences = create()( }, changeWallpaper: ( wallpaper: string, - blurLevel?: BackgroundBlur + blurLevel?: BackgroundBlur, ) => { if (blurLevel) { set({ wallpaper, backgroundBlur: blurLevel }); @@ -144,13 +146,13 @@ export const useUserPreferences = create()( }, deleteTag: (tag: IUserTag) => { const exists = get().filterTags.some( - (t) => t.name === tag.name + (t) => t.name === tag.name, ); if (exists) { set({ filterTags: get().filterTags.filter( - (t) => t.name !== tag.name + (t) => t.name !== tag.name, ), }); } else { @@ -159,7 +161,7 @@ export const useUserPreferences = create()( }, addTag: (tag: IUserTag) => { const exists = get().filterTags.some( - (t) => t.name === tag.name + (t) => t.name === tag.name, ); if (exists) { @@ -173,7 +175,7 @@ export const useUserPreferences = create()( }, addConnection: (connection: IConnection) => { const exists = get().connections.some( - (c) => c.name === connection.name + (c) => c.name === connection.name, ); if (exists) { @@ -184,7 +186,7 @@ export const useUserPreferences = create()( }, editConnection: (providerName: string, connection: IConnection) => { const exists = get().connections.some( - (c) => c.name === providerName + (c) => c.name === providerName, ); if (exists) { @@ -203,13 +205,13 @@ export const useUserPreferences = create()( }, removeConnection: (connectionName: string) => { const exists = get().connections.some( - (c) => c.name === connectionName + (c) => c.name === connectionName, ); if (exists) { set({ connections: get().connections.filter( - (c) => c.name !== connectionName + (c) => c.name !== connectionName, ), }); } else { @@ -224,22 +226,23 @@ export const useUserPreferences = create()( for (let i = 0; i < length; i++) text += possible.charAt( - Math.floor(Math.random() * possible.length) + Math.floor(Math.random() * possible.length), ); return text; } - const hash = generateRandomString(32); - - return hash; + return generateRandomString(32); }, setHasTakenTour: (hasTakenTour: boolean) => { set({ hasTakenTour }); }, + setUserData: (userData: IUserData) => { + set({ userData }); + }, }), { name: "user-preferences", - } - ) + }, + ), ); diff --git a/types.d.ts b/types.d.ts index 65c30ce..094c3c2 100644 --- a/types.d.ts +++ b/types.d.ts @@ -33,6 +33,13 @@ export interface IConnection { email: string; organizationDomain?: string; } + +export interface IUserData { + firstName: string; + lastName: string; + imageUrl?: string; +} + export interface IUserPreferences { searchEngine: SearchEngine; shortcuts: Shortcut[]; @@ -42,6 +49,7 @@ export interface IUserPreferences { searchEnabled: boolean; connections: IConnection[]; hasTakenTour: boolean; + userData?: IUserData; } // App diff --git a/types/async.ts b/types/async.ts new file mode 100644 index 0000000..d92117b --- /dev/null +++ b/types/async.ts @@ -0,0 +1,32 @@ +// these types will be useful for later when +// managing asynchronous behavior in stores +// by creating saga-like effects + +export enum LoadingState { + Idle = "idle", + Loading = "loading", + Success = "success", + Error = "error", +} + +export type AsyncState = { + data?: TData; +} & (IdleStatus | LoadingStatus | SuccessStatus | ErrorStatus); + +export type IdleStatus = { + status: LoadingState.Idle; +}; + +export type LoadingStatus = { + status: LoadingState.Loading; +}; + +export type SuccessStatus = { + status: LoadingState.Success; + data: TData; +}; + +export type ErrorStatus = { + status: LoadingState.Error; + error: TError; +};