diff --git a/.tool-versions b/.tool-versions index 27552eb..d064b3e 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 18.15.0 +nodejs 18.19.0 diff --git a/app/app.css.ts b/app/app.css.ts index e6f6007..0600ae9 100644 --- a/app/app.css.ts +++ b/app/app.css.ts @@ -9,7 +9,7 @@ globalStyle("*, *:before, *:after", { boxSizing: "border-box", }); -globalStyle("p", { +globalStyle("p, ul", { margin: 0, }); diff --git a/app/login/components/ClockIndicator/ClockIndicator.css.ts b/app/components/ClockIndicator/ClockIndicator.css.ts similarity index 100% rename from app/login/components/ClockIndicator/ClockIndicator.css.ts rename to app/components/ClockIndicator/ClockIndicator.css.ts diff --git a/app/login/components/ClockIndicator/index.tsx b/app/components/ClockIndicator/index.tsx similarity index 92% rename from app/login/components/ClockIndicator/index.tsx rename to app/components/ClockIndicator/index.tsx index c54feb8..2b87fd8 100644 --- a/app/login/components/ClockIndicator/index.tsx +++ b/app/components/ClockIndicator/index.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from "react"; import { format } from "date-fns"; -import { useClock } from "../../../hooks"; +import { useClock } from "@/hooks"; import * as css from "./ClockIndicator.css"; export const ClockIndicator = () => { diff --git a/app/components/Layer.tsx b/app/components/Layer.tsx index ec51687..4103fde 100644 --- a/app/components/Layer.tsx +++ b/app/components/Layer.tsx @@ -9,8 +9,8 @@ import { } from "react"; import type { ReactNode } from "react"; -import { LayerContext } from "../contexts/LayerContext"; -import type { ILayerContext } from "../contexts/LayerContext"; +import { LayerContext } from "@/contexts/LayerContext"; +import type { ILayerContext } from "@/contexts/LayerContext"; const BASE_Z_INDEX = 10; diff --git a/app/login/components/LoginProfile/LoginProfile.css.ts b/app/components/LoginProfile/LoginProfile.css.ts similarity index 100% rename from app/login/components/LoginProfile/LoginProfile.css.ts rename to app/components/LoginProfile/LoginProfile.css.ts diff --git a/app/login/components/LoginProfile/index.tsx b/app/components/LoginProfile/index.tsx similarity index 72% rename from app/login/components/LoginProfile/index.tsx rename to app/components/LoginProfile/index.tsx index 5aa8351..75ac676 100644 --- a/app/login/components/LoginProfile/index.tsx +++ b/app/components/LoginProfile/index.tsx @@ -1,11 +1,13 @@ +"use client"; import Image from "next/image"; -import Link from "next/link"; +import { useContext } from "react"; import { PiArrowCircleRightThin } from "react-icons/pi"; +import { LockContext } from "@/contexts/LockContext"; import * as css from "./LoginProfile.css"; - export const LoginProfile = () => { + const { unlock } = useContext(LockContext); return (
@@ -18,9 +20,9 @@ export const LoginProfile = () => { />

Jaewook Ahn

- + Click to unlock - +
); diff --git a/app/components/Menu/Menu.css.ts b/app/components/Menu/Menu.css.ts new file mode 100644 index 0000000..982ce4f --- /dev/null +++ b/app/components/Menu/Menu.css.ts @@ -0,0 +1,42 @@ +import { style } from "@vanilla-extract/css"; + +const MENU_BORDER_RADIUS = 8; +const MENU_ITEM_BORDER_RADIUS = 6; + +export const menuIndicator = style({ + display: "flex", + flexFlow: "row", + gap: 4, + color: "#fff", + fontSize: 14, + alignItems: "center", + position: "relative", +}); + +export const itemFrame = style({ + position: "absolute", + top: "100%", + left: -8, + // TODO regularize z-index values + zIndex: 20, +}); + +export const itemWrapper = style({ + color: "#fff", + boxShadow: "rgba(0, 0, 0, 0.55) 0px 10px 34px", + backgroundColor: "#333", + border: "1px solid #5c5c5c", + borderRadius: MENU_BORDER_RADIUS, + overflow: "hidden", + padding: "6px", + whiteSpace: "nowrap", +}); + +export const item = style({ + padding: "4px 8px", + borderRadius: MENU_ITEM_BORDER_RADIUS, + userSelect: "none", + ":hover": { + backgroundColor: "rgb(22 116 218)", + }, +}); diff --git a/app/components/Menu/index.tsx b/app/components/Menu/index.tsx new file mode 100644 index 0000000..885e75c --- /dev/null +++ b/app/components/Menu/index.tsx @@ -0,0 +1,42 @@ +"use client"; +import { useCallback, useContext } from "react"; + +import type { MenuItem } from "../../contexts/MenuBarContext"; +import { menuBarContext } from "../../contexts/MenuBarContext"; +import { useOutsideClick } from "../../hooks"; +import * as css from "./Menu.css"; + +interface Props { + menuId: string; + name: string | React.ReactNode; + menuItems: MenuItem[]; +} + +export const Menu = (props: Props) => { + const { menuId, name, menuItems } = props; + const { openedMenuId, openMenu, closeMenu } = useContext(menuBarContext); + const ref = useOutsideClick(() => closeMenu()); + + const handleClick = useCallback(() => { + if (openedMenuId !== menuId) { + openMenu(menuId); + } else { + closeMenu(); + } + }, [openedMenuId, menuId, closeMenu, openMenu]); + + return ( +
+

{name}

+
+
    + {menuItems.map((menuItem) => ( +
  • + {menuItem.name} +
  • + ))} +
+
+
+ ); +}; diff --git a/app/components/MenuBar/MenuBar.css.ts b/app/components/MenuBar/MenuBar.css.ts index 433560b..c5bf545 100644 --- a/app/components/MenuBar/MenuBar.css.ts +++ b/app/components/MenuBar/MenuBar.css.ts @@ -13,18 +13,15 @@ export const container = style({ display: "flex", }); -export const menuWrapper = style({ +export const wrapper = style({ display: "inline-flex", + flexFlow: "row", alignItems: "center", + gap: 20, flex: 1, }); -export const rightMenuWrapper = style([menuWrapper, { - flexDirection: "row-reverse", +export const rightMenuWrapper = style([wrapper, { + flexFlow: "row-reverse", marginLeft: "auto", }]); - -export const clock = style({ - color: "#fff", - fontSize: 14, -}); diff --git a/app/components/MenuBar/index.tsx b/app/components/MenuBar/index.tsx index 00edcd1..6a478dc 100644 --- a/app/components/MenuBar/index.tsx +++ b/app/components/MenuBar/index.tsx @@ -1,21 +1,33 @@ "use client"; -import { format } from "date-fns"; -import { useMemo } from "react"; +import { useCallback, useState } from "react"; -import { useClock } from "../../hooks"; +import { menuBarContext } from "../../contexts/MenuBarContext"; +import type { IMenuBarContext } from "../../contexts/MenuBarContext"; import * as css from "./MenuBar.css"; -export const MenuBar = () => { - const time = useClock(); - const clock = useMemo(() => format(time, "E MMM d") + "\u00A0\u00A0" + format(time, "h:mm a"), [time]); +interface Props { + leftMenu: React.ReactNode; + rightMenu: React.ReactNode; +} + +export const MenuBar = (props: Props) => { + const { leftMenu, rightMenu } = props; + const [openedMenuId, setOpenedMenuId] = useState(null); + + const openMenu = useCallback((menuId: string) => setOpenedMenuId(menuId), []); + const closeMenu = useCallback(() => setOpenedMenuId(null), []); + const value: IMenuBarContext = { openedMenuId, openMenu, closeMenu }; return ( - + + + ); }; diff --git a/app/components/Menus/Menus.css.ts b/app/components/Menus/Menus.css.ts new file mode 100644 index 0000000..8ebead0 --- /dev/null +++ b/app/components/Menus/Menus.css.ts @@ -0,0 +1,7 @@ +import { style } from "@vanilla-extract/css"; + +import { menuIndicator } from "../Menu/Menu.css"; + +export const batteryIndicator = style([menuIndicator, { + fontSize: 12, +}]); diff --git a/app/components/Menus/index.tsx b/app/components/Menus/index.tsx new file mode 100644 index 0000000..e022c77 --- /dev/null +++ b/app/components/Menus/index.tsx @@ -0,0 +1,78 @@ +"use client"; +import { format } from "date-fns"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { IoBatteryCharging, IoBatteryFull, IoBatteryHalf, IoCodeSlash } from "react-icons/io5"; + +import { LockContext } from "@/contexts/LockContext"; +import { useClock } from "@/hooks"; + +import { Menu } from "../Menu"; +import { menuIndicator } from "../Menu/Menu.css"; +import * as css from "./Menus.css"; + +export const PortfolioMenu = () => { + const { lock } = useContext(LockContext); + return ( + } + menuItems={[ + { + id: "about-this-site", + name: "About This Site", + type: "default", + checked: false, + disabled: false, + onClick: () => {}, + }, + { + id: "lock-screen", + name: "Lock Screen", + type: "default", + checked: false, + disabled: false, + onClick: lock, + } + ]} + /> + ); +}; + +export const ClockIndicator = () => { + const time = useClock(); + const clock = useMemo(() => format(time, "E MMM d") + "\u00A0\u00A0" + format(time, "h:mm a"), [time]); + + return

{clock}

; +}; + +export const BatteryIndicator = () => { + const [battery, setBattery] = useState({ + charging: false, + chargingTime: 0, + dischargingTime: 0, + level: 1, + }); + const batteryPercent = useMemo(() => (battery.level * 100).toString() + "%", [battery]); + const batteryIcon = useMemo(() => { + if (battery.charging) { + return ; + } + + if (battery.level !== 1) { + return ; + } + + return ; + }, [battery]); + + useEffect(() => { + window.navigator.getBattery?.().then((battery) => setBattery(battery)); + }, []); + + return ( +

+ {batteryPercent} + {batteryIcon} +

+ ); +}; diff --git a/app/home/components/Profile/Profile.css.ts b/app/components/Profile/Profile.css.ts similarity index 100% rename from app/home/components/Profile/Profile.css.ts rename to app/components/Profile/Profile.css.ts diff --git a/app/home/components/Profile/index.tsx b/app/components/Profile/index.tsx similarity index 95% rename from app/home/components/Profile/index.tsx rename to app/components/Profile/index.tsx index dc35376..04d28ab 100644 --- a/app/home/components/Profile/index.tsx +++ b/app/components/Profile/index.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; import { PiGithubLogo, PiLinkedinLogo } from "react-icons/pi"; -import { Window } from "../../../components/Window"; +import { Window } from "../Window"; import * as css from "./Profile.css"; export const Profile = () => { diff --git a/app/home/components/Resume/Resume.css.ts b/app/components/Resume/Resume.css.ts similarity index 100% rename from app/home/components/Resume/Resume.css.ts rename to app/components/Resume/Resume.css.ts diff --git a/app/home/components/Resume/index.tsx b/app/components/Resume/index.tsx similarity index 91% rename from app/home/components/Resume/index.tsx rename to app/components/Resume/index.tsx index 21b40da..bf6873d 100644 --- a/app/home/components/Resume/index.tsx +++ b/app/components/Resume/index.tsx @@ -3,8 +3,8 @@ import axios from "axios"; import { PDFium } from "pdfium.js"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Window } from "../../../components/Window"; -import { PDFRenderer } from "../../../modules/pdf-renderer"; +import { Window } from "../Window"; +import { PDFRenderer } from "../../modules/pdf-renderer"; import * as css from "./Resume.css"; export const Resume = () => { diff --git a/app/home/components/Settings/Settings.css.ts b/app/components/Settings/Settings.css.ts similarity index 100% rename from app/home/components/Settings/Settings.css.ts rename to app/components/Settings/Settings.css.ts diff --git a/app/home/components/Settings/index.tsx b/app/components/Settings/index.tsx similarity index 92% rename from app/home/components/Settings/index.tsx rename to app/components/Settings/index.tsx index e3982f0..f085385 100644 --- a/app/home/components/Settings/index.tsx +++ b/app/components/Settings/index.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from "react"; -import { Window } from "../../../components/Window"; +import { Window } from "../Window"; import * as css from "./Settings.css"; interface RowProps { diff --git a/app/home/components/Shortcuts.tsx b/app/components/Shortcuts.tsx similarity index 94% rename from app/home/components/Shortcuts.tsx rename to app/components/Shortcuts.tsx index df959cb..143129d 100644 --- a/app/home/components/Shortcuts.tsx +++ b/app/components/Shortcuts.tsx @@ -2,8 +2,8 @@ import Image from "next/image"; import { useCallback, useContext } from "react"; -import { Shortcut } from "../../components/Shortcut"; -import { LayerContext } from "../../contexts/LayerContext"; +import { LayerContext } from "../contexts/LayerContext"; +import { Shortcut } from "./Shortcut"; export const ProfileShortcut = () => { const { addLayer } = useContext(LayerContext); diff --git a/app/components/Window/index.tsx b/app/components/Window/index.tsx index c3291a5..a3f11ad 100644 --- a/app/components/Window/index.tsx +++ b/app/components/Window/index.tsx @@ -2,8 +2,9 @@ import { assignInlineVars } from "@vanilla-extract/dynamic"; import { useCallback, useState } from "react"; -import { useDrag } from "../../hooks"; -import type { DragEvent, DragEventHandler } from "../../hooks/useDrag"; +import { useDrag } from "@/hooks"; +import type { DragEvent, DragEventHandler } from "@/hooks/useDrag"; + import { Layer } from "../Layer"; import * as css from "./Window.css"; diff --git a/app/components/lock-screen/index.tsx b/app/components/lock-screen/index.tsx new file mode 100644 index 0000000..d963a1a --- /dev/null +++ b/app/components/lock-screen/index.tsx @@ -0,0 +1,32 @@ +"use client"; +import { assignInlineVars } from "@vanilla-extract/dynamic"; +import { useContext, useEffect, useState } from "react"; + +import { LockContext } from "@/contexts/LockContext"; +import { ClockIndicator } from "../ClockIndicator"; +import { LoginProfile } from "../LoginProfile"; +import { Wallpaper } from "../Wallpaper"; +import * as css from "./lock-screen.css"; + +export const LockScreen = () => { + const { isLocked } = useContext(LockContext); + const [isVisible, setVisible] = useState(true); + + useEffect(() => { + if (isLocked && !isVisible) { + setVisible(true); + } + }, [isLocked, isVisible]); + + return ( +
setVisible(false)} + > + + + +
+ ); +}; diff --git a/app/components/lock-screen/lock-screen.css.ts b/app/components/lock-screen/lock-screen.css.ts new file mode 100644 index 0000000..c21bbbe --- /dev/null +++ b/app/components/lock-screen/lock-screen.css.ts @@ -0,0 +1,40 @@ +import { createVar, keyframes, style } from "@vanilla-extract/css"; + +export const isVisible = createVar(); + +export const wrapper = style({ + position: "absolute", + inset: 0, + zIndex: 100, + visibility: isVisible, +}); + +const unlock = keyframes({ + "0%": { + opacity: 1, + }, + "100%": { + opacity: 0, + } +}); + +const lock = keyframes({ + "0%": { + opacity: 0, + }, + "100%": { + opacity: 1, + }, +}); + +export const unlockAnim = style([wrapper, { + animationName: unlock, + animationDuration: "0.25s", + animationFillMode: "forwards", +}]); + +export const lockAnim = style([wrapper, { + animationName: lock, + animationDuration: "0.25s", + animationFillMode: "forwards", +}]); diff --git a/app/contexts/LockContext.ts b/app/contexts/LockContext.ts new file mode 100644 index 0000000..b5dc3fd --- /dev/null +++ b/app/contexts/LockContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; + +export interface ILockContext { + isLocked: boolean; + lock: () => void; + unlock: () => void; +} + +export const LockContext = createContext({ + isLocked: false, + lock: () => {}, + unlock: () => {}, +}); diff --git a/app/contexts/MenuBarContext.ts b/app/contexts/MenuBarContext.ts new file mode 100644 index 0000000..7a04892 --- /dev/null +++ b/app/contexts/MenuBarContext.ts @@ -0,0 +1,23 @@ +import { createContext } from "react"; + +type MenuItemType = "default" | "check" | "sub-menu" | "divider"; +export interface MenuItem { + id: string; + name: string; + type: MenuItemType; + disabled: boolean; + checked: boolean; + onClick: () => void; +} + +export interface IMenuBarContext { + openedMenuId: string | null; + openMenu: (menuId: string) => void; + closeMenu: () => void; +} + +export const menuBarContext = createContext({ + openedMenuId: null, + openMenu: (_) => {}, + closeMenu: () => {}, +}); diff --git a/app/home/page.tsx b/app/home/page.tsx deleted file mode 100644 index 77fb5df..0000000 --- a/app/home/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { LayerManager } from "../components/Layer"; -import { MenuBar } from "../components/MenuBar"; -import { Profile } from "./components/Profile"; -import { Resume } from "./components/Resume"; -import { Settings } from "./components/Settings"; -import * as Shortcut from "./components/Shortcuts"; -// import { scrollbarStyle } from "../../components/WindowFrame"; - -// const GitHubRepoWrapper = styled.div` -// width: 100%; -// margin: 4px 0; -// padding: 12px 10px; -// display: flex;ㄷㄷㄴㅇㅊㄴㅌ -// flex-direction: column; -// border: 1px rgba(255, 255, 255, 0.25) solid; -// border-radius: 3px; -// cursor: pointer; -// transition: all 0.15s ease-out; -// :hover { -// background-color: rgba(255, 255, 255, 0.02); -// } -// > .repo-title { -// display: flex; -// align-items: baseline; -// margin-bottom: 8px; -// > .ant-typography { -// margin-left: 6px; -// font-size: 16px; -// font-weight: 500; -// } -// } -// > .ant-typography { -// font-size: 12px; -// font-weight: 300; -// color: rgba(255, 255, 255, 0.8); -// } -// > .repo-language { -// margin-top: 12px; -// display: flex; -// align-items: baseline; - -// > .lang-color { -// width: 8px; -// height: 8px; -// border-radius: 4px; -// margin-right: 4px; -// } -// > .ant-typography { -// color: rgba(255, 255, 255, 0.6); -// font-size: 10px; -// } -// } -// `; - -// const RepositoryTab = (props: RepositoryTabProps) => { -// const { loading, repositories } = props; - -// return ( -// -// {loading ? ( -// } /> -// ) : repositories.length ? ( -// repositories.map((repo, i) => ) -// ) : ( -// No repository -// )} -// -// ); -// }; - -const Main = () => { - return ( - - - - - - - - - - - - ); -}; - -export default Main; diff --git a/app/login/page.tsx b/app/login/page.tsx deleted file mode 100644 index 952b5e1..0000000 --- a/app/login/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ClockIndicator } from "./components/ClockIndicator"; -import { LoginProfile } from "./components/LoginProfile"; - -const Login = () => { - return ( - <> - - - - ); -}; - -export default Login; diff --git a/app/page.tsx b/app/page.tsx index 0313b5a..6d7b335 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,50 @@ -import { redirect } from "next/navigation"; +"use client"; +import { useCallback, useState } from "react"; + +import { LockContext } from "./contexts/LockContext"; +import type { ILockContext } from "./contexts/LockContext"; +import { LockScreen } from "./components/lock-screen"; +import { LayerManager } from "./components/Layer"; +import { MenuBar } from "./components/MenuBar"; +import * as Menus from "./components/Menus"; +import * as Shortcut from "./components/Shortcuts"; +import { Resume } from "./components/Resume"; +import { Profile } from "./components/Profile"; +import { Settings } from "./components/Settings"; const Index = () => { - redirect("/login"); + const [isLocked, setLock] = useState(true); + const lock = useCallback(() => setLock(true), []); + const unlock = useCallback(() => setLock(false), []); + + const lockContextValue: ILockContext = { + isLocked, + lock, + unlock, + }; + + return ( + + + ]} + rightMenu={[ + , + , + ]} + /> + + + + + + + + + + + + ); }; export default Index; diff --git a/app/window.d.ts b/app/window.d.ts new file mode 100644 index 0000000..73cad7a --- /dev/null +++ b/app/window.d.ts @@ -0,0 +1,15 @@ + +declare global { + interface Battery { + charging: boolean; + chargingTime: number; + dischargingTime: number; + level: number; + } + + interface Navigator extends globalThis.Navigator { + getBattery?: () => Promise; + } +} + +export { Battery }; diff --git a/tsconfig.json b/tsconfig.json index 28f53ec..b3761fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,10 @@ { "name": "next" } - ] + ], + "paths": { + "@/*": ["./app/*"], + } }, "include": [ "**/*.ts",