mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 09:25:22 -04:00
initial tailwind conversion
This commit is contained in:
parent
f277119407
commit
c9a8b3eaa5
@ -16,15 +16,18 @@
|
||||
"git.fetchOnPull": true,
|
||||
"git.rebaseWhenSync": true,
|
||||
"telemetry.telemetryLevel": "off",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.surveys.enabled": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.tsserver.log": "off",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||
},
|
||||
"extensions": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"esbenp.prettier-vscode",
|
||||
"stylelint.vscode-stylelint"
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
1
.npmrc
1
.npmrc
@ -1,3 +1,2 @@
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
public-hoist-pattern[]=*stylelint*
|
||||
|
@ -1,12 +1,13 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
singleQuote: false,
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
jsxSingleQuote: false,
|
||||
printWidth: 120,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
quoteProps: "as-needed",
|
||||
singleQuote: false,
|
||||
tabWidth: 2,
|
||||
trailingComma: "es5",
|
||||
useTabs: false,
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -1,19 +0,0 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
|
||||
/** @type {import("stylelint").Config} */
|
||||
export default {
|
||||
extends: ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-prettier/recommended"],
|
||||
rules: {
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"media-feature-range-notation": null,
|
||||
"rule-empty-line-before": [
|
||||
"always-multi-line",
|
||||
{
|
||||
except: ["after-single-line-comment"],
|
||||
ignore: ["inside-block"],
|
||||
},
|
||||
],
|
||||
"color-hex-length": "long",
|
||||
},
|
||||
};
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss",
|
||||
"*.mdx": "markdown"
|
||||
},
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"typescript.preferences.importModuleSpecifierEnding": "minimal",
|
||||
"typescript.surveys.enabled": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"typescript.tsserver.log": "off",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||
}
|
@ -97,7 +97,7 @@ const ContactForm = () => {
|
||||
Markdown syntax
|
||||
</Link>{" "}
|
||||
is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
|
||||
<Link href="https://jarv.is" plain>
|
||||
<Link href="https://jarv.is" className="hover:no-underline">
|
||||
links
|
||||
</Link>
|
||||
](https://jarv.is), and <code>`code`</code>.
|
||||
|
@ -11,7 +11,7 @@ export const GeistSans = GeistSansLoader({
|
||||
"system-ui",
|
||||
"sans-serif",
|
||||
],
|
||||
variable: "--fonts-sans",
|
||||
variable: "--font-geist-sans",
|
||||
preload: true,
|
||||
});
|
||||
|
||||
@ -29,6 +29,6 @@ export const GeistMono = GeistMonoLoader({
|
||||
"monospace",
|
||||
],
|
||||
adjustFontFallback: false,
|
||||
variable: "--fonts-mono",
|
||||
variable: "--font-geist-mono",
|
||||
preload: true,
|
||||
});
|
||||
|
155
app/globals.css
155
app/globals.css
@ -1,69 +1,100 @@
|
||||
/*!
|
||||
* modern-normalize v3.0.1 | MIT License | https://github.com/sindresorhus/modern-normalize/tree/v3.0.1
|
||||
*/
|
||||
@import "tailwindcss";
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
@theme inline {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.15;
|
||||
tab-size: 4;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
@theme {
|
||||
--container-default: var(--container-4xl);
|
||||
|
||||
--color-*: initial;
|
||||
--color-background-inner: #ffffff;
|
||||
--color-background-outer: #fcfcfc;
|
||||
--color-text: #202020;
|
||||
--color-medium-dark: #515151;
|
||||
--color-medium: #5e5e5e;
|
||||
--color-medium-light: #757575;
|
||||
--color-light: #d2d2d2;
|
||||
--color-kinda-light: #e3e3e3;
|
||||
--color-super-light: #f4f4f4;
|
||||
--color-super-duper-light: #fbfbfb;
|
||||
--color-link: #0e6dc2;
|
||||
--color-success: #44a248;
|
||||
--color-error: #ff1b1b;
|
||||
--color-warning: #f78200;
|
||||
|
||||
--animate-wave: wave 5s ease 1s infinite;
|
||||
--animate-heartbeat: heartbeat 10s ease 7.5s infinite;
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
2% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
4% {
|
||||
transform: scale(1);
|
||||
}
|
||||
6% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
8% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--fonts-sans);
|
||||
background-color: var(--colors-background-inner);
|
||||
}
|
||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-size: 1em;
|
||||
font-family: var(--fonts-mono);
|
||||
font-variant-ligatures: none; /* i hate them. fwiw. */
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-appearance: button;
|
||||
[data-theme="dark"] {
|
||||
--color-background-inner: #1e1e1e;
|
||||
--color-background-outer: #252525;
|
||||
--color-text: #f1f1f1;
|
||||
--color-medium-dark: #d7d7d7;
|
||||
--color-medium: #b1b1b1;
|
||||
--color-medium-light: #959595;
|
||||
--color-light: #646464;
|
||||
--color-kinda-light: #535353;
|
||||
--color-super-light: #272727;
|
||||
--color-super-duper-light: #1f1f1f;
|
||||
--color-link: #88c7ff;
|
||||
--color-success: #78df55;
|
||||
--color-error: #ff5151;
|
||||
--color-warning: #f2b702;
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.default {
|
||||
width: 100%;
|
||||
padding: 1.5em;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.7;
|
||||
color: var(--colors-text);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.default {
|
||||
font-size: 0.925em;
|
||||
line-height: 1.85;
|
||||
}
|
||||
}
|
@ -1,27 +1,27 @@
|
||||
import { env } from "../lib/env";
|
||||
import { JsonLd } from "react-schemaorg";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import clsx from "clsx";
|
||||
import { ThemeProvider, ThemeScript } from "../contexts/ThemeContext";
|
||||
import Header from "../components/Header";
|
||||
import Footer from "../components/Footer";
|
||||
import { SkipNavLink, SkipNavTarget } from "../components/SkipNav";
|
||||
import cn from "../lib/helpers/classnames";
|
||||
import { defaultMetadata } from "../lib/helpers/metadata";
|
||||
import * as config from "../lib/config";
|
||||
import { MAX_WIDTH } from "../lib/config/constants";
|
||||
import type { Person, WebSite } from "schema-dts";
|
||||
|
||||
import { GeistMono, GeistSans } from "./fonts";
|
||||
import "./globals.css";
|
||||
import "./themes.css";
|
||||
|
||||
import styles from "./layout.module.css";
|
||||
|
||||
export const metadata = defaultMetadata;
|
||||
|
||||
const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
return (
|
||||
<html lang={env.NEXT_PUBLIC_SITE_LOCALE} suppressHydrationWarning>
|
||||
<html
|
||||
lang={env.NEXT_PUBLIC_SITE_LOCALE}
|
||||
className={cn(GeistSans.variable, GeistMono.variable)}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<ThemeScript />
|
||||
|
||||
@ -62,21 +62,16 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body
|
||||
className={clsx(GeistSans.variable, GeistMono.variable)}
|
||||
style={{ ["--max-width" as string]: `${MAX_WIDTH}px` }}
|
||||
>
|
||||
<body className="bg-background-outer text-text font-sans">
|
||||
<ThemeProvider>
|
||||
<SkipNavLink />
|
||||
|
||||
<div className={styles.layout}>
|
||||
<div className="mx-auto flex min-h-screen flex-col">
|
||||
<Header />
|
||||
|
||||
<main className={styles.default}>
|
||||
<div className={styles.container}>
|
||||
<SkipNavTarget />
|
||||
{children}
|
||||
</div>
|
||||
<main className="bg-background-inner w-full text-sm">
|
||||
<SkipNavTarget />
|
||||
<div className="max-w-default mx-auto p-5">{children}</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
@ -88,7 +88,10 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
/>
|
||||
|
||||
<div className={styles.meta}>
|
||||
<Link href={`/${POSTS_DIR}/${frontmatter!.slug}`} plain className={clsx(styles.metaItem, styles.metaLink)}>
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
|
||||
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
|
||||
>
|
||||
<CalendarDaysIcon size="1.25em" className={styles.metaIcon} />
|
||||
<Time date={frontmatter!.date} format="MMMM d, y" />
|
||||
</Link>
|
||||
@ -109,8 +112,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
<Link
|
||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter!.slug}/index.mdx`}
|
||||
title={`Edit "${frontmatter!.title}" on GitHub`}
|
||||
plain
|
||||
className={clsx(styles.metaItem, styles.metaLink)}
|
||||
className={clsx("hover:no-underline", styles.metaItem, styles.metaLink)}
|
||||
>
|
||||
<SquarePenIcon size="1.25em" className={styles.metaIcon} />
|
||||
<span>Improve This Post</span>
|
||||
@ -139,8 +141,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter!.slug}`}
|
||||
dangerouslySetInnerHTML={{ __html: frontmatter!.htmlTitle || frontmatter!.title }}
|
||||
plain
|
||||
className={styles.link}
|
||||
className={clsx("hover:no-underline", styles.link)}
|
||||
/>
|
||||
</h1>
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
.index h1 {
|
||||
margin: 0 0 0.5em -1px; /* misaligned left margin, super nitpicky */
|
||||
font-size: 1.925em;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.index h2 {
|
||||
margin: 0.5em 0 0.5em -1px;
|
||||
font-size: 1.3em;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.index p {
|
||||
margin: 0.85em 0;
|
||||
font-size: 1.05em;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.index p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.index sup {
|
||||
margin: 0 0.1em;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.wave {
|
||||
margin-left: 0.1em;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.wave {
|
||||
animation: wave 5s ease 1s infinite;
|
||||
transform-origin: 65% 80%;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
5% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
15% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.index h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.index h2 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.index p {
|
||||
font-size: 1em;
|
||||
line-height: 1.9;
|
||||
}
|
||||
}
|
169
app/page.tsx
169
app/page.tsx
@ -1,105 +1,58 @@
|
||||
import clsx from "clsx";
|
||||
import hash from "@emotion/hash";
|
||||
import { rgba } from "polished";
|
||||
import Link from "../components/Link";
|
||||
import cn from "../lib/helpers/classnames";
|
||||
import { LockIcon } from "lucide-react";
|
||||
import UnstyledLink from "../components/Link";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
const Link = ({
|
||||
lightColor,
|
||||
darkColor,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}: ComponentPropsWithoutRef<typeof UnstyledLink> & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
}) => {
|
||||
if (lightColor && darkColor) {
|
||||
const uniqueId = hash(`${lightColor},${darkColor}`);
|
||||
|
||||
return (
|
||||
<UnstyledLink className={clsx(`t_${uniqueId}`, className)} {...rest}>
|
||||
{children}
|
||||
|
||||
<style
|
||||
// workaround to have react combine all of these inline styles into a single <style> tag up top, see:
|
||||
// https://react.dev/reference/react-dom/components/style#rendering-an-inline-css-stylesheet
|
||||
href={uniqueId}
|
||||
precedence={styles.index}
|
||||
>
|
||||
{`.t_${uniqueId}{--colors-link:${lightColor};--colors-link-underline:${rgba(lightColor, 0.4)}}[data-theme="dark"] .t_${uniqueId}{--colors-link:${darkColor};--colors-link-underline:${rgba(darkColor, 0.4)}}`}
|
||||
</style>
|
||||
</UnstyledLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstyledLink className={className} {...rest}>
|
||||
{children}
|
||||
</UnstyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className={styles.index}>
|
||||
<h1>
|
||||
Hi there! I’m Jake. <span className={styles.wave}>👋</span>
|
||||
<>
|
||||
<h1 className="mb-2 text-3xl leading-5 font-medium">
|
||||
Hi there! I’m Jake. <span className="animate-wave ml-0.5 inline-block origin-[65%_80%] text-3xl">👋</span>
|
||||
</h1>
|
||||
|
||||
<h2>
|
||||
<h2 className="my-4 text-xl leading-6 font-normal">
|
||||
I’m a frontend web developer based in the{" "}
|
||||
<Link
|
||||
href="https://www.youtube-nocookie.com/embed/rLwbzGyC6t4?hl=en&fs=1&showinfo=1&rel=0&iv_load_policy=3"
|
||||
title='"Boston Accent Trailer - Late Night with Seth Meyers" on YouTube'
|
||||
lightColor="#fb4d42"
|
||||
darkColor="#ff5146"
|
||||
className={cn(`[--color-link:#fb4d42]`, `dark:[--color-link:#ff5146]`)}
|
||||
>
|
||||
Boston
|
||||
</Link>{" "}
|
||||
area.
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||
I specialize in using{" "}
|
||||
<Link
|
||||
href="https://www.typescriptlang.org/"
|
||||
title="TypeScript Official Website"
|
||||
lightColor="#235a97"
|
||||
darkColor="#59a8ff"
|
||||
className={cn(`[--color-link:#235a97]`, `dark:[--color-link:#59a8ff]`)}
|
||||
>
|
||||
TypeScript
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link href="https://reactjs.org/" title="React Official Website" lightColor="#1091b3" darkColor="#6fcbe3">
|
||||
<Link href="https://reactjs.org/" className={cn(`[--color-link:#1091b3]`, `dark:[--color-link:#6fcbe3]`)}>
|
||||
React
|
||||
</Link>
|
||||
, and{" "}
|
||||
<Link href="https://nextjs.org/" title="Next.js Official Website" lightColor="#5e7693" darkColor="#a8b9c0">
|
||||
<Link href="https://nextjs.org/" className={cn(`[--color-link:#5e7693]`, `dark:[--color-link:#a8b9c0]`)}>
|
||||
Next.js
|
||||
</Link>{" "}
|
||||
to make lightweight{" "}
|
||||
<Link
|
||||
href="https://jamstack.org/glossary/jamstack/"
|
||||
title="Jamstack Glossary"
|
||||
lightColor="#04a699"
|
||||
darkColor="#08bbac"
|
||||
className={cn(`[--color-link:#04a699]`, `dark:[--color-link:#08bbac]`)}
|
||||
>
|
||||
Jamstack sites
|
||||
</Link>{" "}
|
||||
with dynamic and powerful{" "}
|
||||
<Link href="https://nodejs.org/en/" title="Node.js Official Website" lightColor="#6fbc4e" darkColor="#84d95f">
|
||||
<Link href="https://nodejs.org/en/" className={cn(`[--color-link:#6fbc4e]`, `dark:[--color-link:#84d95f]`)}>
|
||||
Node
|
||||
</Link>{" "}
|
||||
backends. But I still know my way around{" "}
|
||||
<Link
|
||||
href="https://www.jetbrains.com/lp/php-25/"
|
||||
title="25 Years of PHP History"
|
||||
lightColor="#8892bf"
|
||||
darkColor="#a4afe3"
|
||||
className={cn(`[--color-link:#8892bf]`, `dark:[--color-link:#a4afe3]`)}
|
||||
>
|
||||
less buzzwordy
|
||||
</Link>{" "}
|
||||
@ -107,21 +60,19 @@ const Page = () => {
|
||||
<Link
|
||||
href="https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/"
|
||||
title='"The Cost of Javascript Frameworks" by Tim Kadlec'
|
||||
lightColor="#f48024"
|
||||
darkColor="#e18431"
|
||||
className={cn(`[--color-link:#f48024]`, `dark:[--color-link:#e18431]`)}
|
||||
>
|
||||
vanilla JavaScript
|
||||
</Link>
|
||||
), too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||
Whenever possible, I also apply my experience in{" "}
|
||||
<Link
|
||||
href="https://bugcrowd.com/jakejarvis"
|
||||
title="Jake Jarvis on Bugcrowd"
|
||||
lightColor="#00b81a"
|
||||
darkColor="#57f06d"
|
||||
className={cn(`[--color-link:#00b81a]`, `dark:[--color-link:#57f06d]`)}
|
||||
>
|
||||
application security
|
||||
</Link>
|
||||
@ -129,8 +80,7 @@ const Page = () => {
|
||||
<Link
|
||||
href="https://www.cloudflare.com/learning/serverless/what-is-serverless/"
|
||||
title='"What is serverless computing?" on Cloudflare'
|
||||
lightColor="#0098ec"
|
||||
darkColor="#43b9fb"
|
||||
className={cn(`[--color-link:#0098ec]`, `dark:[--color-link:#43b9fb]`)}
|
||||
>
|
||||
serverless stacks
|
||||
</Link>
|
||||
@ -138,21 +88,19 @@ const Page = () => {
|
||||
<Link
|
||||
href="https://github.com/jakejarvis?tab=repositories&q=github-actions&type=source&language=&sort=stargazers"
|
||||
title='My repositories tagged with "github-actions" on GitHub'
|
||||
lightColor="#ff6200"
|
||||
darkColor="#f46c16"
|
||||
className={cn(`[--color-link:#ff6200]`, `dark:[--color-link:#f46c16]`)}
|
||||
>
|
||||
DevOps automation
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||
I fell in love with{" "}
|
||||
<Link
|
||||
href="/previously"
|
||||
title="My Terrible, Horrible, No Good, Very Bad First Websites"
|
||||
lightColor="#4169e1"
|
||||
darkColor="#8ca9ff"
|
||||
className={cn(`[--color-link:#4169e1]`, `dark:[--color-link:#8ca9ff]`)}
|
||||
>
|
||||
frontend web design
|
||||
</Link>{" "}
|
||||
@ -160,8 +108,7 @@ const Page = () => {
|
||||
<Link
|
||||
href="/notes/my-first-code"
|
||||
title="Jake's Bulletin Board, circa 2003"
|
||||
lightColor="#9932cc"
|
||||
darkColor="#d588fb"
|
||||
className={cn(`[--color-link:#9932cc]`, `dark:[--color-link:#d588fb]`)}
|
||||
>
|
||||
backend programming
|
||||
</Link>{" "}
|
||||
@ -169,86 +116,76 @@ const Page = () => {
|
||||
<Link
|
||||
href="/birthday"
|
||||
title="🎉 Cranky Birthday Boy on VHS Tape 📼"
|
||||
lightColor="#e40088"
|
||||
darkColor="#fd40b1"
|
||||
className={cn(`[--color-link:#e40088]`, `dark:[--color-link:#fd40b1]`)}
|
||||
style={{
|
||||
cursor: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='30' style='font-size:24px'><text y='50%' transform='rotate(-70 0 0) translate(-20, 6)'>🪄</text></svg>") 5 5, auto`,
|
||||
}}
|
||||
>
|
||||
the Tooth Fairy
|
||||
</Link>
|
||||
. <span style={{ color: "var(--colors-medium-light)" }}>I’ve improved a bit since then, I think? 🤷</span>
|
||||
. <span className="text-medium-light">I’ve improved a bit since then, I think? 🤷</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<p className="my-3 text-base leading-7 md:text-[0.975rem]">
|
||||
Over the years, some of my side projects{" "}
|
||||
<Link
|
||||
href="/leo"
|
||||
title="Powncer segment on The Lab with Leo Laporte (G4techTV)"
|
||||
lightColor="#ff1b1b"
|
||||
darkColor="#f06060"
|
||||
className={cn(`[--color-link:#ff1b1b]`, `dark:[--color-link:#f06060]`)}
|
||||
>
|
||||
have
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://tuftsdaily.com/news/2012/04/06/student-designs-iphone-joeytracker-app/"
|
||||
title='"Student designs iPhone JoeyTracker app" on The Tufts Daily'
|
||||
lightColor="#f78200"
|
||||
darkColor="#fd992a"
|
||||
className={cn(`[--color-link:#f78200]`, `dark:[--color-link:#fd992a]`)}
|
||||
>
|
||||
been
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://www.google.com/books/edition/The_Facebook_Effect/RRUkLhyGZVgC?hl=en&gbpv=1&dq=%22jake%20jarvis%22&pg=PA226&printsec=frontcover&bsq=%22jake%20jarvis%22"
|
||||
title='"The Facebook Effect" by David Kirkpatrick (Google Books)'
|
||||
lightColor="#f2b702"
|
||||
darkColor="#ffcc2e"
|
||||
className={cn(`[--color-link:#f2b702]`, `dark:[--color-link:#ffcc2e]`)}
|
||||
>
|
||||
featured
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://money.cnn.com/2007/06/01/technology/facebookplatform.fortune/index.htm"
|
||||
title='"The new Facebook is on a roll" on CNN Money'
|
||||
lightColor="#5ebd3e"
|
||||
darkColor="#78df55"
|
||||
className={cn(`[--color-link:#5ebd3e]`, `dark:[--color-link:#78df55]`)}
|
||||
>
|
||||
by
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://www.wired.com/2007/04/our-web-servers/"
|
||||
title='"Middio: A YouTube Scraper for Major Label Music Videos" on Wired'
|
||||
lightColor="#009cdf"
|
||||
darkColor="#29bfff"
|
||||
className={cn(`[--color-link:#009cdf]`, `dark:[--color-link:#29bfff]`)}
|
||||
>
|
||||
various
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://gigaom.com/2009/10/06/fresh-faces-in-tech-10-kid-entrepreneurs-to-watch/6/"
|
||||
title='"Fresh Faces in Tech: 10 Kid Entrepreneurs to Watch" on Gigaom'
|
||||
lightColor="#3e49bb"
|
||||
darkColor="#7b87ff"
|
||||
className={cn(`[--color-link:#3e49bb]`, `dark:[--color-link:#7b87ff]`)}
|
||||
>
|
||||
media
|
||||
</Link>{" "}
|
||||
<Link
|
||||
href="https://adage.com/article/small-agency-diary/client-ceo-s-son/116723/"
|
||||
title='"Your Next Client? The CEO's Son" on Advertising Age'
|
||||
lightColor="#973999"
|
||||
darkColor="#db60dd"
|
||||
className={cn(`[--color-link:#973999]`, `dark:[--color-link:#db60dd]`)}
|
||||
>
|
||||
outlets
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<p className="text-base leading-7 md:text-[0.975rem]">
|
||||
You can find my work on{" "}
|
||||
<Link
|
||||
href="https://github.com/jakejarvis"
|
||||
rel="me"
|
||||
title="Jake Jarvis on GitHub"
|
||||
lightColor="#8d4eff"
|
||||
darkColor="#a379f0"
|
||||
className={cn(`[--color-link:#8d4eff]`, `dark:[--color-link:#a379f0]`)}
|
||||
>
|
||||
GitHub
|
||||
</Link>{" "}
|
||||
@ -256,44 +193,34 @@ const Page = () => {
|
||||
<Link
|
||||
href="https://www.linkedin.com/in/jakejarvis/"
|
||||
rel="me"
|
||||
title="Jake Jarvis on LinkedIn"
|
||||
lightColor="#0073b1"
|
||||
darkColor="#3b9dd2"
|
||||
className={cn(`[--color-link:#0073b1]`, `dark:[--color-link:#3b9dd2]`)}
|
||||
>
|
||||
LinkedIn
|
||||
</Link>
|
||||
. I’m always available to connect over{" "}
|
||||
<Link href="/contact" title="Send an email" lightColor="#de0c0c" darkColor="#ff5050">
|
||||
<Link
|
||||
href="/contact"
|
||||
title="Send an email"
|
||||
className={cn(`[--color-link:#de0c0c]`, `dark:[--color-link:#ff5050]`)}
|
||||
>
|
||||
email
|
||||
</Link>{" "}
|
||||
<sup>
|
||||
<sup className="mx-0.5 text-[0.6rem]">
|
||||
<Link
|
||||
href="https://jrvs.io/pgp"
|
||||
rel="pgpkey"
|
||||
title="My Public Key"
|
||||
lightColor="#757575"
|
||||
darkColor="#959595"
|
||||
plain
|
||||
className={cn(`[--color-link:#757575]`, `dark:[--color-link:#959595]`, "hover:no-underline")}
|
||||
>
|
||||
<LockIcon size="1.25em" style={{ verticalAlign: "text-top" }} />{" "}
|
||||
<code
|
||||
style={{
|
||||
margin: "0 0.15em",
|
||||
letterSpacing: "0.075em",
|
||||
wordSpacing: "-0.4em",
|
||||
}}
|
||||
>
|
||||
2B0C 9CF2 51E6 9A39
|
||||
</code>
|
||||
<LockIcon size="1.25em" className="inline align-text-top" />{" "}
|
||||
<code className="mx-0.5 tracking-wider [word-spacing:-4px]">2B0C 9CF2 51E6 9A39</code>
|
||||
</Link>
|
||||
</sup>
|
||||
,{" "}
|
||||
<Link
|
||||
href="https://bsky.app/profile/jarv.is"
|
||||
rel="me"
|
||||
title="Jake Jarvis on Bluesky"
|
||||
lightColor="#0085ff"
|
||||
darkColor="#208bfe"
|
||||
className={cn(`[--color-link:#0085ff]`, `dark:[--color-link:#208bfe]`)}
|
||||
>
|
||||
Bluesky
|
||||
</Link>
|
||||
@ -301,15 +228,13 @@ const Page = () => {
|
||||
<Link
|
||||
href="https://fediverse.jarv.is/@jake"
|
||||
rel="me"
|
||||
title="Jake Jarvis on Mastodon"
|
||||
lightColor="#6d6eff"
|
||||
darkColor="#7b87ff"
|
||||
className={cn(`[--color-link:#6d6eff]`, `dark:[--color-link:#7b87ff]`)}
|
||||
>
|
||||
Mastodon
|
||||
</Link>{" "}
|
||||
as well!
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,94 +0,0 @@
|
||||
.heading {
|
||||
font-size: 1.4em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
--activity-0: #ebedf0;
|
||||
--activity-1: #9be9a8;
|
||||
--activity-2: #40c463;
|
||||
--activity-3: #30a14e;
|
||||
--activity-4: #216e39;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .calendar {
|
||||
--activity-0: #252525;
|
||||
--activity-1: #033a16;
|
||||
--activity-2: #196c2e;
|
||||
--activity-3: #2ea043;
|
||||
--activity-4: #56d364;
|
||||
}
|
||||
|
||||
.calendar :global(.react-activity-calendar) {
|
||||
margin: 1em auto 2em;
|
||||
}
|
||||
|
||||
.calendar :global(.react-activity-calendar__count),
|
||||
.calendar :global(.react-activity-calendar__legend-month) {
|
||||
color: var(--colors-medium);
|
||||
}
|
||||
|
||||
.calendar :global(.react-activity-calendar__legend-colors) {
|
||||
color: var(--colors-medium-light);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
line-height: 1.1;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-grow: 1;
|
||||
width: 370px;
|
||||
padding: 1.2em 1.2em 0.8em;
|
||||
border: 1px solid var(--colors-kinda-light);
|
||||
border-radius: 1em;
|
||||
font-size: 0.9em;
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
||||
|
||||
.card .name {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.4em;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card .description {
|
||||
margin: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.card .meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
|
||||
.card .metaItem {
|
||||
margin: 0.3em 1.5em 0.3em 0;
|
||||
color: var(--colors-medium);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card .metaLink {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.card .metaLink:hover,
|
||||
.card .metaLink:focus-visible {
|
||||
color: var(--colors-link);
|
||||
}
|
||||
|
||||
.card .metaIcon {
|
||||
display: inline-block;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
margin-right: 0.5em;
|
||||
vertical-align: text-top;
|
||||
}
|
@ -6,11 +6,10 @@ import Calendar from "./calendar";
|
||||
import PageTitle from "../../components/PageTitle";
|
||||
import Link from "../../components/Link";
|
||||
import RelativeTime from "../../components/RelativeTime";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import { createMetadata } from "../../lib/helpers/metadata";
|
||||
import { getContributions, getRepos } from "./github";
|
||||
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title: "Projects",
|
||||
description: `Most-starred repositories by @${env.NEXT_PUBLIC_GITHUB_USERNAME} on GitHub`,
|
||||
@ -32,41 +31,55 @@ const Page = async () => {
|
||||
<>
|
||||
<PageTitle canonical="/projects">Projects</PageTitle>
|
||||
|
||||
<h2 className={styles.heading}>
|
||||
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`} style={{ color: "inherit" }} plain>
|
||||
<h2 className="my-3.5 text-[1.4em] font-normal">
|
||||
<Link
|
||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}
|
||||
className="text-inherit hover:no-underline"
|
||||
>
|
||||
Contribution activity
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<Suspense fallback={<p>Failed to generate activity calendar.</p>}>
|
||||
<Calendar data={contributions} className={styles.calendar} />
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto mt-4 mb-8",
|
||||
"[--activity-0:#ebedf0] [--activity-1:#9be9a8] [--activity-2:#40c463] [--activity-3:#30a14e] [--activity-4:#216e39]",
|
||||
"dark:[--activity-0:#252525] dark:[--activity-1:#033a16] dark:[--activity-2:#196c2e] dark:[--activity-3:#2ea043] dark:[--activity-4:#56d364]",
|
||||
String.raw`[&_.react-activity-calendar\_\_count]:text-medium [&_.react-activity-calendar\_\_legend-month]:text-medium [&_.react-activity-calendar\_\_legend-colors]:text-medium-light`
|
||||
)}
|
||||
>
|
||||
<Calendar data={contributions} />
|
||||
</div>
|
||||
</Suspense>
|
||||
|
||||
<h2 className={styles.heading}>
|
||||
<h2 className="my-3.5 text-[1.4em] font-normal">
|
||||
<Link
|
||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}?tab=repositories&sort=stargazers`}
|
||||
style={{ color: "inherit" }}
|
||||
plain
|
||||
className="text-inherit hover:no-underline"
|
||||
>
|
||||
Popular repositories
|
||||
</Link>
|
||||
</h2>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<div className="flex w-full flex-row flex-wrap items-start justify-between gap-[1em] leading-[1.1]">
|
||||
{repos?.map((repo) => (
|
||||
<div key={repo!.name} className={styles.card}>
|
||||
<Link href={repo!.url} className={styles.name}>
|
||||
<div
|
||||
key={repo!.name}
|
||||
className="border-kinda-light text-medium-dark w-[370px] grow rounded-[1em] border border-solid px-[1.2em] pt-[1.2em] pb-[0.8em] text-[0.9em]"
|
||||
>
|
||||
<Link href={repo!.url} className="mb-[0.4em] inline-block text-[1.2em] font-semibold">
|
||||
{repo!.name}
|
||||
</Link>
|
||||
|
||||
{repo!.description && <p className={styles.description}>{repo!.description}</p>}
|
||||
{repo!.description && <p className="m-0 leading-[1.7]">{repo!.description}</p>}
|
||||
|
||||
<div className={styles.meta}>
|
||||
<div className="mt-[0.4em] flex flex-wrap">
|
||||
{repo!.primaryLanguage && (
|
||||
<div className={styles.metaItem}>
|
||||
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||
{repo!.primaryLanguage.color && (
|
||||
<span
|
||||
className={styles.metaIcon}
|
||||
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
|
||||
style={{ backgroundColor: repo!.primaryLanguage.color, borderRadius: "50%" }}
|
||||
/>
|
||||
)}
|
||||
@ -75,41 +88,38 @@ const Page = async () => {
|
||||
)}
|
||||
|
||||
{repo!.stargazerCount > 0 && (
|
||||
<div className={styles.metaItem}>
|
||||
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||
<Link
|
||||
href={`${repo!.url}/stargazers`}
|
||||
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)} ${repo!.stargazerCount === 1 ? "star" : "stars"}`}
|
||||
plain
|
||||
className={styles.metaLink}
|
||||
className="hover:text-link text-inherit hover:no-underline"
|
||||
>
|
||||
<StarIcon size="1.25em" className={styles.metaIcon} />
|
||||
<StarIcon size="1.25em" className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top" />
|
||||
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.stargazerCount)}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{repo!.forkCount > 0 && (
|
||||
<div className={styles.metaItem}>
|
||||
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||
<Link
|
||||
href={`${repo!.url}/network/members`}
|
||||
title={`${Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)} ${repo!.forkCount === 1 ? "fork" : "forks"}`}
|
||||
plain
|
||||
className={styles.metaLink}
|
||||
className="hover:text-link text-inherit hover:no-underline"
|
||||
>
|
||||
<GitForkIcon size="1.25em" className={styles.metaIcon} />
|
||||
<GitForkIcon
|
||||
size="1.25em"
|
||||
className="mr-[0.5em] inline-block h-[1.25em] w-[1.25em] align-text-top"
|
||||
/>
|
||||
<span>{Intl.NumberFormat(env.NEXT_PUBLIC_SITE_LOCALE).format(repo!.forkCount)}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.metaItem}>
|
||||
<div className="text-medium my-[0.3em] mr-[1.5em] ml-0 whitespace-nowrap">
|
||||
<span
|
||||
className={styles.metaIcon}
|
||||
style={{
|
||||
// invisible icon hack to fix line height
|
||||
width: 0,
|
||||
marginRight: 0,
|
||||
}}
|
||||
// invisible icon hack to fix line height
|
||||
className="mr-0 inline-block h-[1.25em] w-0 align-text-top"
|
||||
/>
|
||||
<span>
|
||||
Updated <RelativeTime date={repo!.pushedAt} />
|
||||
@ -120,13 +130,7 @@ const Page = async () => {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginBottom: 0,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<p className="mt-4 mb-0 text-center text-base font-medium">
|
||||
<Link href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_USERNAME}`}>
|
||||
View more on{" "}
|
||||
<svg
|
||||
@ -134,13 +138,7 @@ const Page = async () => {
|
||||
viewBox="0 0 24 24"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
style={{
|
||||
width: "1.2em",
|
||||
height: "1.2em",
|
||||
verticalAlign: "text-top",
|
||||
margin: "0 0.1em 0 0.25em",
|
||||
fill: "var(--colors-text)",
|
||||
}}
|
||||
className="fill-text mr-[0.1em] ml-[0.25em] inline h-[1.2em] w-[1.2em] align-text-top"
|
||||
>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||
</svg>{" "}
|
||||
|
@ -45,7 +45,7 @@ const Page = () => {
|
||||
>
|
||||
<span style={{ color: "#f95757" }}>sundar</span>@<span style={{ color: "#3b9dd2" }}>google</span>:
|
||||
<span style={{ color: "#78df55" }}>~</span>$ <span style={{ color: "#d588fb" }}>mv</span> /root
|
||||
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} plain>
|
||||
<Link href="https://killedbygoogle.com/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||
/stable_products_that_people_rely_on/
|
||||
</Link>
|
||||
googledomains.zip /tmp/
|
||||
@ -64,7 +64,7 @@ const Page = () => {
|
||||
<br />
|
||||
<span style={{ color: "#78df55" }}>@monthly</span>
|
||||
<span style={{ color: "#d588fb" }}>rm</span> <span style={{ color: "#fd992a" }}>-f</span> /tmp/
|
||||
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} plain>
|
||||
<Link href="https://fuckyougoogle.zip/" style={{ color: "inherit" }} className="hover:no-underline">
|
||||
*.zip
|
||||
</Link>
|
||||
<br />
|
||||
|
@ -1,6 +0,0 @@
|
||||
.blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 1.25em;
|
||||
border-left: 0.25em solid var(--colors-link);
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
import clsx from "clsx";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Blockquote.module.css";
|
||||
|
||||
export type BlockquoteProps = ComponentPropsWithoutRef<"blockquote">;
|
||||
|
||||
const Blockquote = ({ className, ...rest }: BlockquoteProps) => (
|
||||
<blockquote className={clsx(styles.blockquote, className)} {...rest} />
|
||||
<blockquote className={cn("border-l-link text-medium-dark ml-0 border-l-4 pl-5", className)} {...rest} />
|
||||
);
|
||||
|
||||
export default Blockquote;
|
||||
|
@ -1,88 +0,0 @@
|
||||
.footer {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1.25em 1.5em;
|
||||
border-top: 1px solid var(--colors-kinda-light);
|
||||
background-color: var(--colors-background-outer);
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
font-size: 0.8em;
|
||||
line-height: 2.3;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
.link:has(.icon):hover,
|
||||
.link:has(.icon):focus-visible {
|
||||
color: var(--colors-medium) !important;
|
||||
}
|
||||
|
||||
.link.underline {
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 1px solid var(--colors-light);
|
||||
}
|
||||
|
||||
.link.underline:hover,
|
||||
.link.underline:focus-visible {
|
||||
border-bottom-color: var(--colors-kinda-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
margin: 0 0.1em;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.heart {
|
||||
color: var(--colors-error);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.heart {
|
||||
animation: pulse 10s ease 7.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
2% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
4% {
|
||||
transform: scale(1);
|
||||
}
|
||||
6% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
8% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* pause for ~9 out of 10 seconds */
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer {
|
||||
padding: 1em 1.25em;
|
||||
}
|
||||
|
||||
/* stack columns on left instead of flexboxing across */
|
||||
.row {
|
||||
display: block;
|
||||
}
|
||||
}
|
@ -1,38 +1,44 @@
|
||||
import { env } from "../../lib/env";
|
||||
import clsx from "clsx";
|
||||
import { HeartIcon } from "lucide-react";
|
||||
import Link from "../Link";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import * as config from "../../lib/config";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
export type FooterProps = ComponentPropsWithoutRef<"footer">;
|
||||
|
||||
const Footer = ({ className, ...rest }: FooterProps) => {
|
||||
return (
|
||||
<footer className={clsx(styles.footer, className)} {...rest}>
|
||||
<div className={styles.row}>
|
||||
<footer
|
||||
className={cn("border-t-kinda-light bg-background-outer text-medium-dark w-full border-t py-4", className)}
|
||||
{...rest}
|
||||
>
|
||||
<div className="max-w-default mx-auto flex w-full flex-col justify-between px-5 text-[0.8rem] leading-9 md:flex-row">
|
||||
<div>
|
||||
Content{" "}
|
||||
<Link href="/license" title={config.license} plain className={styles.link}>
|
||||
<Link href="/license" title={config.license} className="text-medium-dark hover:no-underline">
|
||||
licensed under {config.licenseAbbr}
|
||||
</Link>
|
||||
,{" "}
|
||||
<Link href="/previously" title="Previously on..." plain className={styles.link}>
|
||||
<Link href="/previously" title="Previously on..." className="text-medium-dark hover:no-underline">
|
||||
{config.copyrightYearStart}
|
||||
</Link>{" "}
|
||||
– {new Date().getUTCFullYear()}.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Made with <HeartIcon size="1.25em" fill="currentColor" className={clsx(styles.icon, styles.heart)} /> and{" "}
|
||||
Made with{" "}
|
||||
<HeartIcon
|
||||
size="1.25em"
|
||||
fill="currentColor"
|
||||
className="animate-heartbeat text-error mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
|
||||
/>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://nextjs.org/"
|
||||
title="Powered by Next.js"
|
||||
aria-label="Next.js"
|
||||
plain
|
||||
className={styles.link}
|
||||
className="text-medium-dark hover:text-medium hover:no-underline"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -42,7 +48,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
|
||||
viewBox="0 0 24 24"
|
||||
width="1.25em"
|
||||
height="1.25em"
|
||||
className={styles.icon}
|
||||
className="mx-0.25 inline h-[1.25em] w-[1.25em] align-text-top"
|
||||
>
|
||||
<path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" />
|
||||
</svg>
|
||||
@ -51,8 +57,7 @@ const Footer = ({ className, ...rest }: FooterProps) => {
|
||||
<Link
|
||||
href={`https://github.com/${env.NEXT_PUBLIC_GITHUB_REPO}`}
|
||||
title="View Source on GitHub"
|
||||
plain
|
||||
className={clsx(styles.link, styles.underline)}
|
||||
className="border-b-light text-medium-dark hover:border-b-kinda-light border-b-2 pb-0.5 hover:no-underline"
|
||||
>
|
||||
View source.
|
||||
</Link>
|
||||
|
@ -1,83 +0,0 @@
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 4.5em;
|
||||
padding: 0.7em 1.5em;
|
||||
border-bottom: 1px solid var(--colors-kinda-light);
|
||||
background-color: var(--colors-background-header);
|
||||
|
||||
/* make sticky */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
|
||||
/* blurry glass-like background effect (except on firefox...?) */
|
||||
backdrop-filter: saturate(180%) blur(5px);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 1px solid var(--colors-light);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
.home:hover,
|
||||
.home:focus-visible {
|
||||
color: var(--colors-link) !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0 0.6em;
|
||||
font-size: 1.15em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0.75em 1.25em;
|
||||
height: 5.9em;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.home:hover .avatar,
|
||||
.home:focus-visible .avatar {
|
||||
border-color: var(--colors-link-underline);
|
||||
}
|
||||
|
||||
.name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu {
|
||||
max-width: 325px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.menu {
|
||||
max-width: 225px;
|
||||
}
|
||||
}
|
@ -1,34 +1,46 @@
|
||||
import Image from "next/image";
|
||||
import clsx from "clsx";
|
||||
import Link from "../Link";
|
||||
import Menu from "../Menu";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import * as config from "../../lib/config";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
import avatarImg from "../../app/avatar.jpg";
|
||||
|
||||
export type HeaderProps = ComponentPropsWithoutRef<"header">;
|
||||
|
||||
const Header = ({ className, ...rest }: HeaderProps) => {
|
||||
return (
|
||||
<header className={clsx(styles.header, className)} {...rest}>
|
||||
<nav className={styles.nav}>
|
||||
<Link dynamicOnHover href="/" rel="author" aria-label={config.authorName} plain className={styles.home}>
|
||||
<header
|
||||
className={cn(
|
||||
"bg-background-outer/70 border-kinda-light sticky top-0 z-[1000] h-24 w-full border-b backdrop-blur-[5px] backdrop-saturate-[180%] md:h-18",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<nav className="max-w-default mx-auto flex h-full w-full items-center justify-between px-5 py-3 md:py-5">
|
||||
<Link
|
||||
dynamicOnHover
|
||||
href="/"
|
||||
rel="author"
|
||||
aria-label={config.authorName}
|
||||
className="text-medium-dark hover:text-link flex flex-shrink-0 items-center hover:no-underline"
|
||||
>
|
||||
<Image
|
||||
src={avatarImg}
|
||||
alt={`Photo of ${config.authorName}`}
|
||||
className={styles.avatar}
|
||||
className="border-light h-[70px] w-[70px] rounded-full border-2 md:h-[48px] md:w-[48px] md:border"
|
||||
width={70}
|
||||
height={70}
|
||||
quality={50}
|
||||
priority
|
||||
/>
|
||||
<span className={styles.name}>{config.authorName}</span>
|
||||
<span className="mx-2.5 hidden text-lg leading-none font-medium tracking-[0.02em] md:block">
|
||||
{config.authorName}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<Menu className={styles.menu} />
|
||||
<Menu className="ml-6 w-full max-w-64 sm:ml-4 sm:max-w-96 md:ml-0 md:max-w-none" />
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ export type HeadingAnchorProps = Omit<ComponentPropsWithoutRef<typeof Link>, "hr
|
||||
|
||||
const HeadingAnchor = ({ id, ...rest }: HeadingAnchorProps) => {
|
||||
return (
|
||||
<Link href={`#${id}`} plain {...rest}>
|
||||
<Link href={`#${id}`} className="hover:no-underline" {...rest}>
|
||||
<LinkIcon size="0.8em" />
|
||||
</Link>
|
||||
);
|
||||
|
@ -1,25 +0,0 @@
|
||||
.link {
|
||||
color: var(--colors-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* fancy underline effect on hover */
|
||||
.link:not(.plain) {
|
||||
background-image: linear-gradient(var(--colors-link-underline), var(--colors-link-underline));
|
||||
background-position: 0% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 0% 2px;
|
||||
transition: background-size 0.2s ease-in-out;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.link:not(.plain):hover,
|
||||
.link:not(.plain):focus-visible {
|
||||
background-size: 100% 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.link:not(.plain) {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
@ -1,18 +1,13 @@
|
||||
import NextLink from "next/link";
|
||||
import clsx from "clsx";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Link.module.css";
|
||||
|
||||
export type LinkProps = ComponentPropsWithoutRef<typeof NextLink> & {
|
||||
/** Disables fancy text-decoration effect when true. */
|
||||
plain?: boolean;
|
||||
|
||||
// https://github.com/vercel/next.js/pull/77866/files#diff-040f76a8f302dd3a8ec7de0867048475271f052b094cd73d2d0751b495c02f7dR30
|
||||
dynamicOnHover?: boolean;
|
||||
};
|
||||
|
||||
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, className, ...rest }: LinkProps) => {
|
||||
const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, className, ...rest }: LinkProps) => {
|
||||
// This component auto-detects whether or not this link should open in the same window (the default for internal
|
||||
// links) or a new tab (the default for external links). Defaults can be overridden with `target="_blank"`.
|
||||
const isExternal = typeof href === "string" && !["/", "#"].includes(href[0]);
|
||||
@ -25,10 +20,8 @@ const Link = ({ href, rel, target, prefetch = false, dynamicOnHover, plain, clas
|
||||
href={href}
|
||||
target={target || (isExternal ? "_blank" : undefined)}
|
||||
rel={`${rel ? `${rel} ` : ""}${target === "_blank" || isExternal ? "noopener noreferrer" : ""}` || undefined}
|
||||
className={clsx(
|
||||
styles.link,
|
||||
// eslint-disable-next-line css-modules/no-undef-class
|
||||
plain && styles.plain,
|
||||
className={cn(
|
||||
"text-link hover:decoration-link/40 hover:underline hover:decoration-2 hover:underline-offset-4",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
|
@ -1,36 +0,0 @@
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.menu {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.menu {
|
||||
margin-left: 1.4em;
|
||||
}
|
||||
|
||||
/* the home icon is kinda redundant when space is SUPER tight */
|
||||
.item:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,39 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import clsx from "clsx";
|
||||
import MenuItem from "../MenuItem";
|
||||
import ThemeToggle from "../ThemeToggle";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import { menuItems } from "../../lib/config/menu";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./Menu.module.css";
|
||||
|
||||
export type MenuProps = ComponentPropsWithoutRef<"ul">;
|
||||
|
||||
const Menu = ({ className, ...rest }: MenuProps) => {
|
||||
const pathname = usePathname() || "";
|
||||
|
||||
return (
|
||||
<ul className={clsx(styles.menu, className)} {...rest}>
|
||||
<ul className={cn("flex flex-row justify-between md:justify-end", className)} {...rest}>
|
||||
{menuItems.map((item, index) => {
|
||||
// kinda weird/hacky way to determine if the *first part* of the current path matches this href
|
||||
const isCurrent = item.href === `/${pathname.split("/")[1]}`;
|
||||
|
||||
return (
|
||||
<li className={styles.item} key={item.text || index}>
|
||||
<li className="first-of-type:hidden sm:first-of-type:block md:ml-4" key={item.text || index}>
|
||||
<MenuItem {...item} current={isCurrent} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
<li
|
||||
className={styles.item}
|
||||
style={{
|
||||
// manually align the theme toggle with the rest of the menu icons
|
||||
paddingTop: "0.2em",
|
||||
}}
|
||||
>
|
||||
<li className="-mr-2.5 md:ml-4">
|
||||
<MenuItem
|
||||
// @ts-expect-error
|
||||
icon={ThemeToggle}
|
||||
|
@ -1,43 +0,0 @@
|
||||
.link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.6em;
|
||||
margin-top: 0.2em;
|
||||
color: var(--colors-medium-dark) !important;
|
||||
}
|
||||
|
||||
/* indicate active page/section */
|
||||
.link.current {
|
||||
margin-bottom: -0.2em;
|
||||
border-bottom: 0.2em solid var(--colors-link-underline);
|
||||
}
|
||||
|
||||
.link:not(.current):hover,
|
||||
.link:not(.current):focus-visible {
|
||||
margin-bottom: -0.2em;
|
||||
border-bottom: 0.2em solid var(--colors-kinda-light);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 0.7em;
|
||||
font-size: 0.925em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
import clsx from "clsx";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import Link from "../Link";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import styles from "./MenuItem.module.css";
|
||||
|
||||
export type MenuItemProps = Omit<ComponentPropsWithoutRef<typeof Link>, "href"> & {
|
||||
text?: string;
|
||||
href?: string;
|
||||
@ -17,8 +15,8 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
|
||||
|
||||
const item = (
|
||||
<>
|
||||
{Icon && <Icon size="1.25em" className={styles.icon} />}
|
||||
{text && <span className={styles.label}>{text}</span>}
|
||||
{Icon && <Icon size="1.25em" className="block h-[1.8em] w-[1.8em] md:h-[1.25em] md:w-[1.25em]" />}
|
||||
{text && <span className="ml-3 hidden text-sm leading-none font-medium tracking-[0.02em] md:block">{text}</span>}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -29,8 +27,12 @@ const MenuItem = ({ text, href, icon, current, className, ...rest }: MenuItemPro
|
||||
dynamicOnHover
|
||||
href={href}
|
||||
aria-label={text}
|
||||
plain
|
||||
className={clsx(styles.link, current && styles.current, className)}
|
||||
data-current={current || undefined}
|
||||
className={cn(
|
||||
"text-medium-dark hover:border-kinda-light -mb-[0.2em] inline-flex items-center p-2.5 hover:border-b-[0.2em] hover:no-underline",
|
||||
current && "border-link/40 hover:border-link/40 border-b-[0.2em]",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{item}
|
||||
|
@ -1,15 +0,0 @@
|
||||
.title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.6em;
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.slug::before {
|
||||
content: "\002E\002F"; /* "./" */
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--colors-medium-light);
|
||||
margin-right: -0.1em;
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
import clsx from "clsx";
|
||||
import cn from "../../lib/helpers/classnames";
|
||||
import Link from "../Link";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
import styles from "./PageTitle.module.css";
|
||||
|
||||
export type PageTitleProps = ComponentPropsWithoutRef<"h1"> & {
|
||||
canonical: string;
|
||||
};
|
||||
|
||||
const PageTitle = ({ canonical, className, children, ...rest }: PageTitleProps) => {
|
||||
return (
|
||||
<h1 className={clsx(styles.title, className)} {...rest}>
|
||||
<Link href={canonical} plain className={styles.slug}>
|
||||
<h1 className={cn("mt-1 mb-6 text-left text-3xl font-medium lowercase", className)} {...rest}>
|
||||
<Link
|
||||
href={canonical}
|
||||
className="before:text-medium-light before:mr-[-0.1em] before:tracking-widest before:content-['\002E\002F']"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</h1>
|
||||
|
@ -1,26 +0,0 @@
|
||||
.toggle {
|
||||
display: block;
|
||||
border: 0;
|
||||
padding: 0.6em;
|
||||
margin-right: -0.6em;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: var(--colors-medium-dark);
|
||||
}
|
||||
|
||||
.toggle:hover,
|
||||
.toggle:focus-visible {
|
||||
color: var(--colors-warning);
|
||||
}
|
||||
|
||||
/* hacky way to avoid flashing icon for a few milliseconds on initial render */
|
||||
.toggle > .sun,
|
||||
[data-theme="dark"] .toggle > .moon {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
.toggle > .moon,
|
||||
[data-theme="dark"] .toggle > .sun {
|
||||
display: none;
|
||||
}
|
@ -6,8 +6,6 @@ import { useTheme } from "../../hooks";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import styles from "./ThemeToggle.module.css";
|
||||
|
||||
export type ThemeToggleProps = ComponentPropsWithoutRef<LucideIcon>;
|
||||
|
||||
const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
|
||||
@ -17,10 +15,10 @@ const ThemeToggle = ({ className, ...rest }: ThemeToggleProps) => {
|
||||
<button
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
aria-label="Toggle Theme"
|
||||
className={styles.toggle}
|
||||
className="hover:text-warning block bg-none p-2.5 hover:cursor-pointer hover:border-none"
|
||||
>
|
||||
<SunIcon className={clsx(styles.sun, className)} {...rest} />
|
||||
<MoonIcon className={clsx(styles.moon, className)} {...rest} />
|
||||
<SunIcon className={clsx("!block dark:!hidden", className)} {...rest} />
|
||||
<MoonIcon className={clsx("!hidden dark:!block", className)} {...rest} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -2,4 +2,4 @@
|
||||
export const POSTS_DIR = "notes";
|
||||
|
||||
/** Maximum width of content wrapper (e.g. for images) in pixels. */
|
||||
export const MAX_WIDTH = 865;
|
||||
export const MAX_WIDTH = 896;
|
||||
|
8
lib/helpers/classnames.ts
Normal file
8
lib/helpers/classnames.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import clsx, { type ClassValue } from "clsx";
|
||||
|
||||
const cn = (...inputs: ClassValue[]) => {
|
||||
return twMerge(clsx(inputs));
|
||||
};
|
||||
|
||||
export default cn;
|
14
package.json
14
package.json
@ -20,7 +20,6 @@
|
||||
"dependencies": {
|
||||
"@date-fns/tz": "^1.2.0",
|
||||
"@date-fns/utc": "^2.1.0",
|
||||
"@emotion/hash": "^0.9.2",
|
||||
"@giscus/react": "^3.1.0",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
@ -41,7 +40,6 @@
|
||||
"html-entities": "^2.6.0",
|
||||
"lucide-react": "0.503.0",
|
||||
"next": "15.4.0-canary.5",
|
||||
"polished": "^4.3.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "19.1.0",
|
||||
"react-activity-calendar": "^2.7.10",
|
||||
@ -69,6 +67,8 @@
|
||||
"resend": "^4.4.0",
|
||||
"server-only": "0.0.1",
|
||||
"shiki": "^3.3.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"unified": "^11.0.5",
|
||||
"valibot": "^1.0.0"
|
||||
},
|
||||
@ -76,6 +76,7 @@
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@jakejarvis/eslint-config": "^4.0.7",
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
@ -97,12 +98,10 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.1",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"schema-dts": "^1.1.5",
|
||||
"stylelint": "^16.19.0",
|
||||
"stylelint-config-css-modules": "^4.4.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-prettier": "^5.0.3",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@ -119,9 +118,6 @@
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,mdx}": [
|
||||
"eslint"
|
||||
],
|
||||
"*.css": [
|
||||
"stylelint"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
1108
pnpm-lock.yaml
generated
1108
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -23,6 +23,10 @@
|
||||
"groupName": "react",
|
||||
"rangeStrategy": "pin"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["tailwindcss", "@tailwindcss/*", "tailwind-merge", "prettier-plugin-tailwindcss"],
|
||||
"groupName": "tailwindcss"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["@mdx-js/*", "remark-*", "rehype-*", "unified", "unist-*", "@types/mdx"],
|
||||
"groupName": "mdx"
|
||||
@ -32,8 +36,8 @@
|
||||
"groupName": "eslint"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["stylelint", "stylelint-*"],
|
||||
"groupName": "stylelint"
|
||||
"matchPackageNames": ["prettier", "prettier-*", "!prettier-plugin-tailwindcss"],
|
||||
"groupName": "prettier"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["typescript"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user