Skip to content
Merged
36 changes: 26 additions & 10 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,49 @@ const eslintConfig = [
"indent": ["error", 2],
"eol-last": ["error", "always"],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
}],
"no-trailing-spaces": "error",
"no-multiple-empty-lines": "error",
"no-irregular-whitespace": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"semi": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
// "comma-dangle": ["error", "always-multiline"],
"comma-spacing": ["error", {
"before": false,
"after": true,
}],
"keyword-spacing": "error",
"space-before-blocks": "error",
"quotes": ["error", "double"],
"arrow-spacing": ["error", {
before: true,
after: true,
}],

"jsx-quotes": ["error", "prefer-double"],
"@typescript-eslint/no-empty-object-type": "off"
"react/jsx-tag-spacing": ["error", {
"beforeSelfClosing": "always",
}],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",

"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
}],
},
ignores: [
"src/app/(payload)/**",
]
],
},
{
files: ["src/migrations/*"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"quotes": "off"
}
}
"quotes": "off",
},
},
];

export default eslintConfig;
30 changes: 30 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"graphql": "^16.10.0",
"jotai": "^2.13.0",
"next": "15.3.3",
"payload": "^3.41.0",
"react": "^19.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/app/(frontend)/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const Blog = async ({ params } : {params: Promise<{slug: string}>}) => {
collection: "blogs",
select: {
metadata: { slug: true },
content: { title: true, body: true }
content: { title: true, body: true },
},
where: { "metadata.slug": { "equals": slug } }
where: { "metadata.slug": { "equals": slug } },
});

const content = post.docs[0].content;
Expand Down
2 changes: 1 addition & 1 deletion src/app/(frontend)/blogs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Blogs = async () => {
collection: "blogs",
select: {
metadata: { slug: true },
content: { title: true }
content: { title: true },
},
where: { _status: { equals: "published" } },
page: 1,
Expand Down
4 changes: 3 additions & 1 deletion src/app/(frontend)/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import ContactLinks from "@/components/ContactLinks";
import DarkModeToggle from "@/components/DarkModeToggle";

const Contact = () => {
return (
Expand All @@ -14,10 +15,11 @@ const Contact = () => {
<h1>connect or message me!</h1>
<ContactLinks />
</section>
<DarkModeToggle float mobileOnly />
</main>

<footer>
<Footer hideContent/>
<Footer hideContent />
</footer>
</div>
);
Expand Down
14 changes: 7 additions & 7 deletions src/app/(frontend)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import type { Metadata } from "next";
import "@/styles/globals.css";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import Providers from "@/components/Providers";

export const metadata: Metadata = {
title: "Silvia Tan - Software Engineer",
description: "Silvia is a full stack software engineer who likes to build user-friendly, accessible, and intuitive applications.",
keywords: "Silvia, Silvia Silvia, Silvia Tan, Portfolio, Software Engineer, Full Stack, Startup, Web, Frontend",
};

const RootLayout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
return (
<html lang="en">
<head>
<link rel="icon" href="/favicon.png" type="image/png"/>
<title>{`${metadata.title}`}</title>
<link rel="icon" href="/favicon.png" type="image/png" />
</head>
<body className="antialiased">
{children}
<Providers>
{children}
</Providers>
<Analytics />
<SpeedInsights />
</body>
Expand Down
2 changes: 2 additions & 0 deletions src/app/(frontend)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import DarkModeToggle from "@/components/DarkModeToggle";

const Home = () => {
return (
Expand All @@ -13,6 +14,7 @@ const Home = () => {
<h1>hello i&apos;m <span className="name">silvia</span></h1>
<p>a full stack software engineer who loves to build intuitive user-friendly web applications.</p>
</div>
<DarkModeToggle float mobileOnly />
</main>

<footer>
Expand Down
6 changes: 4 additions & 2 deletions src/app/(frontend)/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Footer from "@/components/Footer";
import { cmsClient } from "@/services/cms-client";
import React from "react";
import ProjectCards from "@/components/ProjectCards";
import DarkModeToggle from "@/components/DarkModeToggle";

const Projects = async () => {
const posts = await cmsClient.find({
Expand All @@ -15,7 +16,7 @@ const Projects = async () => {
page: 1,
limit: 10,
pagination: true,
sort: "_order"
sort: "_order",
});

return (
Expand All @@ -30,8 +31,9 @@ const Projects = async () => {
<p>
Stuff I&apos;ve built, including this website!
</p>
<ProjectCards projects={posts}/>
<ProjectCards projects={posts} />
</section>
<DarkModeToggle float mobileOnly />
</main>

<footer>
Expand Down
61 changes: 61 additions & 0 deletions src/atoms/themeAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

enum Theme {
System = "system",
Light = "light",
Dark = "dark",
}

const getSystemTheme = () => {
if (typeof window === "undefined") return;

const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return systemDark ? Theme.Dark : Theme.Light;
};

const localThemeAtom = atomWithStorage("theme", Theme.System);
const systemThemeAtom = atom(getSystemTheme());

// track the actual system theme and update on changes
systemThemeAtom.onMount = (set) => {
if (typeof window === "undefined") return;

const systemColorSchemeMedia = window.matchMedia("(prefers-color-scheme: dark)");
const setSystemThemeAtom = () => set(getSystemTheme());

systemColorSchemeMedia.addEventListener("change", setSystemThemeAtom);
return () => systemColorSchemeMedia.removeEventListener("change", setSystemThemeAtom);
};

const themeAtom = atom(
(get) => {
const localTheme = get(localThemeAtom);
const systemTheme = get(systemThemeAtom);
const resolvedTheme = localTheme === Theme.System ? systemTheme : localTheme;

return {
theme: resolvedTheme,
themeStatus: {
isDark: localTheme === Theme.Dark,
isLight: localTheme === Theme.Light,
isSystem: localTheme === Theme.System,
},
};
},
(_get, set) => {
set(localThemeAtom, (currTheme) => {
switch (currTheme) {
case Theme.System: return Theme.Light;
case Theme.Light: return Theme.Dark;
case Theme.Dark:
default:
return Theme.System;
}
});
},
);

export {
themeAtom,
};
33 changes: 22 additions & 11 deletions src/components/DarkModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
const DarkModeToggle = () => {
const toggleTheme = () => {
document.documentElement.classList.toggle("dark",);
};
"use client";

import { IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
import { useAtomValue, useSetAtom } from "jotai";
import { themeAtom } from "@/atoms/themeAtom";

const DarkModeToggle = ({ float, desktopOnly, mobileOnly }:{ float?: true, desktopOnly?: true, mobileOnly?: true }) => {
const themeStatus = useAtomValue(themeAtom).themeStatus;
const toggleTheme = useSetAtom(themeAtom);

return (
<button className="rounded-full bg-red-800" onClick={toggleTheme}>
<div className="px-4 py-2 rounded-full -translate-y-1 bg-red-200 text-red-600 font-semibold active:translate-y-0 transition-all active:shadow-inner active:shadow-red-800">
{/*<IconDeviceDesktop />*/}
{/*<IconSun />*/}
{/*<IconMoon />*/}
</div>
</button>
<div className={`
${desktopOnly ? "hide-on-mobile" : ""}
${mobileOnly ? "hide-on-desktop" : ""}
${float ? "sticky self-end bottom-6 -mb-6 md:mr-8" : ""}
`}>
<button className="rounded-full bg-red-800 dark:bg-red-950" onClick={toggleTheme}>
<div className="p-3 md:p-2 rounded-full -translate-y-1 bg-red-200 dark:bg-red-900 text-red-600 dark:text-red-300 font-semibold active:translate-y-0 transition-all active:shadow-inner active:shadow-red-800 dark:active:shadow-black">
{themeStatus.isDark && <IconMoon />}
{themeStatus.isSystem && <IconDeviceDesktop />}
{themeStatus.isLight && <IconSun />}
</div>
</button>
</div>
);
};

Expand Down
4 changes: 3 additions & 1 deletion src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { ReactNode, useState } from "react";
import { IconExternalLink } from "@tabler/icons-react";
import DarkModeToggle from "@/components/DarkModeToggle";

const NavbarLink = ({ children, className = "", link = "", newTab } : {children?: ReactNode, className?: string, link?: string, newTab?: true}) => {
const pathname = usePathname();
Expand All @@ -24,7 +25,8 @@ const Navbar = () => {

return (
<nav className="navbar">
<div className="logo">
<div className="logo gap-4">
<DarkModeToggle desktopOnly />
<h1>{"silvia's"}</h1>
<button className={`burger-menu ${isBurgerOpened ? "opened" : ""}`} onClick={handleBurgerClick}>menu</button>
</div>
Expand Down
38 changes: 38 additions & 0 deletions src/components/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { ReactNode, useEffect } from "react";
import dynamic from "next/dynamic";
import { useAtomValue } from "jotai";
import { themeAtom } from "@/atoms/themeAtom";

// note: dynamically load jotai provider so that theme atom won't flicker
const Provider = dynamic(() => import("jotai").then((e) => e.Provider), { ssr: false });

const Theme = ({ children }: {children: ReactNode}) => {
const theme = useAtomValue(themeAtom).theme;

useEffect(() => {
// apply theme class to html element for webkit scrollbar support
const htmlElement = document.documentElement;
htmlElement.classList.remove("light", "dark");
if (theme) htmlElement.classList.add(theme);
}, [theme]);

return (
<div className={`${theme}`}>
{children}
</div>
);
};

const Providers = ({ children }: { children: ReactNode }) => {
return (
<Provider>
<Theme>
{children}
</Theme>
</Provider>
);
};

export default Providers;
Loading