diff --git a/scripts/tools/dev-menu/components/sections/system-info-setion.tsx b/scripts/tools/dev-menu/components/sections/system-info-setion.tsx
index 4547e72d..17d23ada 100644
--- a/scripts/tools/dev-menu/components/sections/system-info-setion.tsx
+++ b/scripts/tools/dev-menu/components/sections/system-info-setion.tsx
@@ -8,10 +8,7 @@ type Props = {
onToggle: () => void
}
-export function SystemInfoSection({
- isExpanded,
- onToggle
-}: Props) {
+export function SystemInfoSection({ isExpanded, onToggle }: Props) {
return (
<>
@@ -60,7 +57,7 @@ export function SystemInfoSection({
{typeof window !== 'undefined'
? navigator.userAgent.slice(0, 80) +
- '...'
+ '...'
: 'n/a'}
diff --git a/src/app/(marketing)/blog/posts/engineering/measuring-postgres-search-indexes.md b/src/app/(marketing)/blog/posts/engineering/measuring-postgres-search-indexes.md
index dc3dcb88..6ae9044b 100644
--- a/src/app/(marketing)/blog/posts/engineering/measuring-postgres-search-indexes.md
+++ b/src/app/(marketing)/blog/posts/engineering/measuring-postgres-search-indexes.md
@@ -70,12 +70,12 @@ The important part is not just creating indexes. The important part is measuring
## The Result
-| Term | Before | After | Change |
-| --- | ---: | ---: | ---: |
-| `http` | `827.190 ms` | `21.621 ms` | ~38x faster |
-| `photo` | `313.753 ms` | `4.566 ms` | ~69x faster |
-| `thanks` | `327.628 ms` | `4.210 ms` | ~78x faster |
-| `test` | `323.533 ms` | `5.479 ms` | ~59x faster |
+| Term | Before | After | Change |
+| -------- | -----------: | ----------: | ----------: |
+| `http` | `827.190 ms` | `21.621 ms` | ~38x faster |
+| `photo` | `313.753 ms` | `4.566 ms` | ~69x faster |
+| `thanks` | `327.628 ms` | `4.210 ms` | ~78x faster |
+| `test` | `323.533 ms` | `5.479 ms` | ~59x faster |
The post-index plan changed to this:
diff --git a/src/app/(marketing)/blog/posts/yappin/what-ive-been-shipping-lately.md b/src/app/(marketing)/blog/posts/yappin/what-ive-been-shipping-lately.md
index ddeccbb3..1c328c67 100644
--- a/src/app/(marketing)/blog/posts/yappin/what-ive-been-shipping-lately.md
+++ b/src/app/(marketing)/blog/posts/yappin/what-ive-been-shipping-lately.md
@@ -18,110 +18,115 @@ It is less of a polished launch post and more of a snapshot. A place to document
- **[ @remcostoeten/analytics ]**
There was no real reason for this, besides me wanting to roll my own service and learn some more back-end. It's a privacy-first, event-sourced analytics engine built on **Bun**, **Hono** (Vercel Edge), and **Neon/Postgres**. It leverages JSONB for a schema-less event stream that supports advanced forensics (Browser/OS parsing) out of the box.
-
- - **Possibilities**: Track Web Vitals (LCP, CLS, INP), E-commerce revenue, A/B test variants, and full user funnels without migrations.
- - **Usage**:
- ```tsx
- import { Analytics, trackEvent, useAnalytics } from '@remcostoeten/analytics';
-
- // 1. Initialize globally with robust user segmentation
- export default function RootLayout({ children }) {
- return (
-
- {children}
-
- );
- }
-
- // 2. Rich e-commerce tracking integrated into feature components
- function CheckoutButton({ cart }) {
- const { identify, track } = useAnalytics();
-
- const handleCheckout = () => {
- // Tie subsequent events to a specific user identity securely
- identify('USR_8921', { trait: 'premium_tier', LTV: 450 });
-
- // Fire a complex schema-less event with full type-safety payload
- track('checkout_initiated', {
- cartValue: cart.total,
- currency: 'EUR',
- items: cart.items.map(i => i.sku),
- appliedDiscount: 'SUMMER_SALES_26',
- abTestVariant: 'checkout_v2_red_btn'
- });
- };
-
- return
Checkout ;
- }
- ```
- - **GitHub**: [remcostoeten/analytics](https://github.com/remcostoeten/analytics)
- - **Live Demo**: [analytics.remcostoeten.nl](https://analytics.remcostoeten.nl)
+ - **Possibilities**: Track Web Vitals (LCP, CLS, INP), E-commerce revenue, A/B test variants, and full user funnels without migrations.
+ - **Usage**:
+
+ ```tsx
+ import {
+ Analytics,
+ trackEvent,
+ useAnalytics
+ } from '@remcostoeten/analytics'
+
+ // 1. Initialize globally with robust user segmentation
+ export default function RootLayout({ children }) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ // 2. Rich e-commerce tracking integrated into feature components
+ function CheckoutButton({ cart }) {
+ const { identify, track } = useAnalytics()
+
+ const handleCheckout = () => {
+ // Tie subsequent events to a specific user identity securely
+ identify('USR_8921', { trait: 'premium_tier', LTV: 450 })
+
+ // Fire a complex schema-less event with full type-safety payload
+ track('checkout_initiated', {
+ cartValue: cart.total,
+ currency: 'EUR',
+ items: cart.items.map(i => i.sku),
+ appliedDiscount: 'SUMMER_SALES_26',
+ abTestVariant: 'checkout_v2_red_btn'
+ })
+ }
+
+ return
Checkout
+ }
+ ```
+
+ - **GitHub**: [remcostoeten/analytics](https://github.com/remcostoeten/analytics)
+ - **Live Demo**: [analytics.remcostoeten.nl](https://analytics.remcostoeten.nl)
- **[ @remcostoeten/use-shortcut ]**
- I built this because existing shortcut libraries felt clunky. I wanted a "human-readable" API that felt like writing a sentence, with perfect TypeScript autocompletion for every key and modifier.
-
- - **Tech Talk**: A zero-dependency React hook built with a fluent/chainable builder pattern. It handles event listener cleanup automatically and supports advanced modifier combinations without manual string parsing.
- - **Possibilities**: Support for command combos (Ctrl/Cmd + K), sequential key presses (G then H), global/scoped listeners, and automatic documentation generation via description tags.
- - **Usage**:
- ````tsx
- import { useShortcut, ScopeProvider } from '@remcostoeten/use-shortcut';
-
- function AdvancedCommandPalette() {
- const searchRef = useRef
(null);
-
- // Builder pattern enables complex sequencing and scope locking
- useShortcut()
- .sequence('g', 't') // Wait for 'g' THEN 't' (Go To)
- .or()
- .cmd().shift().key('p') // Or use standard modifier combo
- .scope(searchRef) // Only trigger if this element doesn't have focus
- .preventDefault()
- .debounce(150) // Prevent rapid firing bounds
- .action((event, meta) => {
- console.log(`Triggered via: ${meta.triggerType}`);
- searchRef.current?.focus();
- })
- .description('Navigate to specific project view')
- .build();
-
- return ;
- }
- ````
- - **GitHub**: [remcostoeten/use-shortcut](https://github.com/remcostoeten/use-shortcut)
- - **Live Demo**: [use-shortcut.vercel.app](https://use-shortcut.vercel.app)
+ I built this because existing shortcut libraries felt clunky. I wanted a "human-readable" API that felt like writing a sentence, with perfect TypeScript autocompletion for every key and modifier.
+ - **Tech Talk**: A zero-dependency React hook built with a fluent/chainable builder pattern. It handles event listener cleanup automatically and supports advanced modifier combinations without manual string parsing.
+ - **Possibilities**: Support for command combos (Ctrl/Cmd + K), sequential key presses (G then H), global/scoped listeners, and automatic documentation generation via description tags.
+ - **Usage**:
+
+ ```tsx
+ import { useShortcut, ScopeProvider } from '@remcostoeten/use-shortcut'
+
+ function AdvancedCommandPalette() {
+ const searchRef = useRef(null)
+
+ // Builder pattern enables complex sequencing and scope locking
+ useShortcut()
+ .sequence('g', 't') // Wait for 'g' THEN 't' (Go To)
+ .or()
+ .cmd()
+ .shift()
+ .key('p') // Or use standard modifier combo
+ .scope(searchRef) // Only trigger if this element doesn't have focus
+ .preventDefault()
+ .debounce(150) // Prevent rapid firing bounds
+ .action((event, meta) => {
+ console.log(`Triggered via: ${meta.triggerType}`)
+ searchRef.current?.focus()
+ })
+ .description('Navigate to specific project view')
+ .build()
+
+ return
+ }
+ ```
+
+ - **GitHub**: [remcostoeten/use-shortcut](https://github.com/remcostoeten/use-shortcut)
+ - **Live Demo**: [use-shortcut.vercel.app](https://use-shortcut.vercel.app)
- **[ oauth-app-automator ]**
I hate creating OAuth applications when using social providers. Thus I automated the process with a Python program. Authenticate once and from there you're able to create, (bulk) delete apps and test credentials. Write secret and client ID to clipboard, or straight in your environment variable.
-
- - **Tech Talk**: Python-based automation that scripts the actual browser interactions (via Selenium/Playwright) to fetch credentials directly from provider portals (GitHub/Google).
- - **Possibilities**: Bulk creation of developer apps, automatic `.env` file updates, and a clean interactive CLI/TUI for managing credentials.
- - **GitHub**: [remcostoeten/oauth-app-automator](https://github.com/remcostoeten/oauth-app-automator)
+ - **Tech Talk**: Python-based automation that scripts the actual browser interactions (via Selenium/Playwright) to fetch credentials directly from provider portals (GitHub/Google).
+ - **Possibilities**: Bulk creation of developer apps, automatic `.env` file updates, and a clean interactive CLI/TUI for managing credentials.
+ - **GitHub**: [remcostoeten/oauth-app-automator](https://github.com/remcostoeten/oauth-app-automator)
- **[ Kuizer ]**
Maintaining large TypeScript projects often leads to "dead code rot." I needed a fast, reliable way to prune the tree without manual auditing—so I built Kuizer to handle the heavy lifting.
-
- - **Tech Talk**: A high-performance CLI built with **Bun**. It analyzes the TypeScript import graph to detect unreachable files and unused exports by deep-diving into the AST.
- - **Possibilities**: Browse findings in an interactive terminal UI (TUI), categorize unused exports (types vs components), and safe snapshot-based rollbacks for every automated fix.
- - **Usage**:
- ````bash
- # Run a deep AST dependency graph analysis to find dead code
- # -i: Interactive TUI mode for reviewing findings
- # --fix: Automatically treeshake and safely remove unused code
- # --snapshot: Create a rollback snapshot before mutating files
- # --exclude: Glob array pattern for files to ignore during analysis
- kuizer analyze \
- --interactive \
- --fix \
- --snapshot ./.kuizer-backups/ \
- --exclude "**/*.test.ts" "src/legacy-api/**" \
- --threshold "medium"
- ````
- - **GitHub**: [remcostoeten/kuizer](https://github.com/remcostoeten/kuizer)
-
+ - **Tech Talk**: A high-performance CLI built with **Bun**. It analyzes the TypeScript import graph to detect unreachable files and unused exports by deep-diving into the AST.
+ - **Possibilities**: Browse findings in an interactive terminal UI (TUI), categorize unused exports (types vs components), and safe snapshot-based rollbacks for every automated fix.
+ - **Usage**:
+ ```bash
+ # Run a deep AST dependency graph analysis to find dead code
+ # -i: Interactive TUI mode for reviewing findings
+ # --fix: Automatically treeshake and safely remove unused code
+ # --snapshot: Create a rollback snapshot before mutating files
+ # --exclude: Glob array pattern for files to ignore during analysis
+ kuizer analyze \
+ --interactive \
+ --fix \
+ --snapshot ./.kuizer-backups/ \
+ --exclude "**/*.test.ts" "src/legacy-api/**" \
+ --threshold "medium"
+ ```
+ - **GitHub**: [remcostoeten/kuizer](https://github.com/remcostoeten/kuizer)
diff --git a/src/app/api/activity/combined/combine.ts b/src/app/api/activity/combined/combine.ts
index 2b7a0ff0..f29a01f0 100644
--- a/src/app/api/activity/combined/combine.ts
+++ b/src/app/api/activity/combined/combine.ts
@@ -24,7 +24,9 @@ export async function getCombinedActivity(
getCachedGitHubContributions(previousYear),
getCachedGitHubActivity(activityLimit),
getSpotifyTracks(tracksLimit),
- hasYTMusicCredentials() ? getYTMusicTracks(tracksLimit) : Promise.resolve([] as any[]),
+ hasYTMusicCredentials()
+ ? getYTMusicTracks(tracksLimit)
+ : Promise.resolve([] as any[])
])
const contributionsMap: Record<
diff --git a/src/app/api/ytmusic/recent/route.ts b/src/app/api/ytmusic/recent/route.ts
index fd07a139..24f82d54 100644
--- a/src/app/api/ytmusic/recent/route.ts
+++ b/src/app/api/ytmusic/recent/route.ts
@@ -25,6 +25,9 @@ export async function GET(request: Request) {
return NextResponse.json({ tracks })
} catch (error) {
console.error('[YTM API] Error:', error)
- return NextResponse.json({ error: 'Failed to fetch tracks', tracks: [] }, { status: 500 })
+ return NextResponse.json(
+ { error: 'Failed to fetch tracks', tracks: [] },
+ { status: 500 }
+ )
}
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 8d137028..c93728ca 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -115,10 +115,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
href="//avatars.githubusercontent.com"
/>
-
+
- new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
+ const isNew =
+ new Date(post.metadata.publishedAt) >
+ new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
- function handleToggleDraft() {
- startTransition(async () => {
- try {
- const result = await toggleBlogDraft(post.slug);
- if (result.success) {
- setIsDraft(result.draft);
- } else {
- console.error("Failed to toggle draft:", result.error);
- }
- } catch (error) {
- console.error("Failed to toggle draft:", error);
- }
- });
- }
+ function handleToggleDraft() {
+ startTransition(async () => {
+ try {
+ const result = await toggleBlogDraft(post.slug)
+ if (result.success) {
+ setIsDraft(result.draft)
+ } else {
+ console.error('Failed to toggle draft:', result.error)
+ }
+ } catch (error) {
+ console.error('Failed to toggle draft:', error)
+ }
+ })
+ }
- return (
-
-
-
-
-
-
-
-
- {isPending ? (
-
- ) : isDraft ? (
- "Draft"
- ) : (
- "Published"
- )}
-
-
- {isNew && !isDraft && (
-
- New
-
- )}
-
-
- {post.metadata.title}
-
-
-
-
- {new Date(post.metadata.publishedAt).toLocaleDateString(
- "en-US",
- {
- month: "short",
- day: "numeric",
- },
- )}
-
-
-
- {post.metadata.readTime || "N/A"}
-
-
-
-
-
-
-
- {post.totalViews}
-
-
- {post.uniqueViews} unique
-
-
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+ {isPending ? (
+
+ ) : isDraft ? (
+ 'Draft'
+ ) : (
+ 'Published'
+ )}
+
+
+ {isNew && !isDraft && (
+
+ New
+
+ )}
+
+
+ {post.metadata.title}
+
+
+
+
+ {new Date(
+ post.metadata.publishedAt
+ ).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric'
+ })}
+
+
+
+ {post.metadata.readTime || 'N/A'}
+
+
+
+
+
+
+
+ {post.totalViews}
+
+
+ {post.uniqueViews} unique
+
+
+
+
+
+
+
+
+
+ )
}
function SortButton({
- field,
- currentField,
- direction,
- onSort,
- children,
+ field,
+ currentField,
+ direction,
+ onSort,
+ children
}: {
- field: SortField;
- currentField: SortField;
- direction: SortDirection;
- onSort: (field: SortField) => void;
- children: React.ReactNode;
+ field: SortField
+ currentField: SortField
+ direction: SortDirection
+ onSort: (field: SortField) => void
+ children: React.ReactNode
}) {
- const isActive = field === currentField;
- return (
- onSort(field)}
- className={`flex items-center gap-1 text-xs px-2 py-1 rounded transition-colors ${
- isActive
- ? "bg-primary/10 text-primary"
- : "text-muted-foreground hover:text-foreground hover:bg-muted"
- }`}
- >
- {children}
- {isActive ? (
- direction === "asc" ? (
-
- ) : (
-
- )
- ) : (
-
- )}
-
- );
+ const isActive = field === currentField
+ return (
+ onSort(field)}
+ className={`flex items-center gap-1 text-xs px-2 py-1 rounded transition-colors ${
+ isActive
+ ? 'bg-primary/10 text-primary'
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
+ }`}
+ >
+ {children}
+ {isActive ? (
+ direction === 'asc' ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+ )
}
export function BlogList({ posts }: { posts: BlogPost[] }) {
- const [search, setSearch] = useState("");
- const [sortField, setSortField] = useState("views");
- const [sortDirection, setSortDirection] = useState("desc");
+ const [search, setSearch] = useState('')
+ const [sortField, setSortField] = useState('views')
+ const [sortDirection, setSortDirection] = useState('desc')
- const handleSort = (field: SortField) => {
- if (field === sortField) {
- setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
- } else {
- setSortField(field);
- setSortDirection("desc");
- }
- };
+ const handleSort = (field: SortField) => {
+ if (field === sortField) {
+ setSortDirection(prev => (prev === 'asc' ? 'desc' : 'asc'))
+ } else {
+ setSortField(field)
+ setSortDirection('desc')
+ }
+ }
- const filteredAndSortedPosts = useMemo(() => {
- let result = [...posts];
+ const filteredAndSortedPosts = useMemo(() => {
+ let result = [...posts]
- if (search) {
- const searchLower = search.toLowerCase();
- result = result.filter(
- (post) =>
- post.metadata.title.toLowerCase().includes(searchLower) ||
- post.slug.toLowerCase().includes(searchLower),
- );
- }
+ if (search) {
+ const searchLower = search.toLowerCase()
+ result = result.filter(
+ post =>
+ post.metadata.title.toLowerCase().includes(searchLower) ||
+ post.slug.toLowerCase().includes(searchLower)
+ )
+ }
- result.sort((a, b) => {
- let comparison = 0;
- switch (sortField) {
- case "title":
- comparison = a.metadata.title.localeCompare(b.metadata.title);
- break;
- case "date":
- comparison =
- new Date(a.metadata.publishedAt).getTime() -
- new Date(b.metadata.publishedAt).getTime();
- break;
- case "views":
- comparison = a.totalViews - b.totalViews;
- break;
- }
- return sortDirection === "asc" ? comparison : -comparison;
- });
+ result.sort((a, b) => {
+ let comparison = 0
+ switch (sortField) {
+ case 'title':
+ comparison = a.metadata.title.localeCompare(
+ b.metadata.title
+ )
+ break
+ case 'date':
+ comparison =
+ new Date(a.metadata.publishedAt).getTime() -
+ new Date(b.metadata.publishedAt).getTime()
+ break
+ case 'views':
+ comparison = a.totalViews - b.totalViews
+ break
+ }
+ return sortDirection === 'asc' ? comparison : -comparison
+ })
- return result;
- }, [posts, search, sortField, sortDirection]);
+ return result
+ }, [posts, search, sortField, sortDirection])
- const totalViews = posts.reduce((sum, p) => sum + p.totalViews, 0);
- const publishedCount = posts.filter((p) => !p.metadata.draft).length;
- const draftCount = posts.filter((p) => p.metadata.draft).length;
+ const totalViews = posts.reduce((sum, p) => sum + p.totalViews, 0)
+ const publishedCount = posts.filter(p => !p.metadata.draft).length
+ const draftCount = posts.filter(p => p.metadata.draft).length
- return (
-
-
-
-
-
-
-
Blog Posts
-
- {publishedCount} published · {draftCount} drafts ·{" "}
- {totalViews.toLocaleString()} total views
-
-
-
-
-
- setSearch(e.target.value)}
- className="pl-9 h-9 w-full md:w-[200px]"
- />
-
-
-
- Sort:
-
- Views
-
-
- Date
-
-
- Title
-
-
-
-
- {filteredAndSortedPosts.length === 0 ? (
-
- {search ? `No posts matching "${search}"` : "No posts yet"}
-
- ) : (
-
- {filteredAndSortedPosts.map((post) => (
-
- ))}
-
- )}
-
-
- );
+ return (
+
+
+
+
+
+
+
+ Blog Posts
+
+
+ {publishedCount} published · {draftCount} drafts
+ · {totalViews.toLocaleString()} total views
+
+
+
+
+
+ setSearch(e.target.value)}
+ className="pl-9 h-9 w-full md:w-[200px]"
+ />
+
+
+
+ Sort:
+
+ Views
+
+
+ Date
+
+
+ Title
+
+
+
+
+ {filteredAndSortedPosts.length === 0 ? (
+
+ {search
+ ? `No posts matching "${search}"`
+ : 'No posts yet'}
+
+ ) : (
+
+ {filteredAndSortedPosts.map(post => (
+
+ ))}
+
+ )}
+
+
+ )
}
diff --git a/src/components/blog/home-blog-posts-client.tsx b/src/components/blog/home-blog-posts-client.tsx
index d7bb3bf0..6e931627 100644
--- a/src/components/blog/home-blog-posts-client.tsx
+++ b/src/components/blog/home-blog-posts-client.tsx
@@ -79,14 +79,16 @@ export function HomeBlogPostsClient({ posts }: Props) {
{allTags.length > 0 && (
- {allTags.slice(0, 3).map(tag => (
-
- {tag}
-
- ))}
+ {allTags
+ .slice(0, 3)
+ .map(tag => (
+
+ {tag}
+
+ ))}
)}
diff --git a/src/components/blog/home-blog-posts.tsx b/src/components/blog/home-blog-posts.tsx
index d4368314..ada305af 100644
--- a/src/components/blog/home-blog-posts.tsx
+++ b/src/components/blog/home-blog-posts.tsx
@@ -1,34 +1,34 @@
-import { getVisibleBlogPosts, BLOG_DESCRIPTION } from "@/features/blog";
-import { Section } from "../ui/section";
-import { HomeBlogPostsClient } from "./home-blog-posts-client";
+import { getVisibleBlogPosts, BLOG_DESCRIPTION } from '@/features/blog'
+import { Section } from '../ui/section'
+import { HomeBlogPostsClient } from './home-blog-posts-client'
function HomePostCountHeader({ count }: { count: number }) {
- return (
-
- {count}
- posts
-
- );
+ return (
+
+ {count}
+ posts
+
+ )
}
export async function HomeBlogPosts() {
- const posts = await getVisibleBlogPosts(false);
+ const posts = await getVisibleBlogPosts(false)
- return (
-
}
- noHeaderMargin
- >
-
-
-
- {BLOG_DESCRIPTION}
-
-
-
-
-
- );
+ return (
+
}
+ noHeaderMargin
+ >
+
+
+
+ {BLOG_DESCRIPTION}
+
+
+
+
+
+ )
}
diff --git a/src/components/blog/mdx.tsx b/src/components/blog/mdx.tsx
index b9fdd5c1..b0689743 100644
--- a/src/components/blog/mdx.tsx
+++ b/src/components/blog/mdx.tsx
@@ -120,11 +120,7 @@ function Video({ src, ...props }: { src: string; [key: string]: any }) {
}
function PassthroughHTML({ children, tagName, ...props }: any) {
- return React.createElement(
- tagName || 'div',
- props,
- children
- )
+ return React.createElement(tagName || 'div', props, children)
}
const htmlElementsToPass = [
@@ -413,10 +409,7 @@ export function CustomMDX(props) {
remarkDirective,
remarkCalloutDirectives
],
- rehypePlugins: [
- rehypeExtractCodeMeta,
- rehypeRaw
- ]
+ rehypePlugins: [rehypeExtractCodeMeta, rehypeRaw]
},
allowDangerousHtml: true
}}
diff --git a/src/components/blog/posts-client.tsx b/src/components/blog/posts-client.tsx
index 6dc32d8d..2df8b517 100644
--- a/src/components/blog/posts-client.tsx
+++ b/src/components/blog/posts-client.tsx
@@ -157,10 +157,7 @@ function BlogCardSkeleton() {
)
}
-export function BlogPostsClient({
- posts,
- isAdmin = false
-}: BlogPostsProps) {
+export function BlogPostsClient({ posts, isAdmin = false }: BlogPostsProps) {
const [showAll, setShowAll] = useState(true)
const { filter, setFilter } = useBlogFilter()
diff --git a/src/components/blog/posts-server.tsx b/src/components/blog/posts-server.tsx
index 89ad7e72..68873cec 100644
--- a/src/components/blog/posts-server.tsx
+++ b/src/components/blog/posts-server.tsx
@@ -1,32 +1,35 @@
-import { getVisibleBlogPosts, BLOG_DESCRIPTION } from "@/features/blog";
-import { isAdmin } from "@/utils/is-admin";
-import { BlogPostsClient, PostCountHeader } from "./posts-client";
-import { Section } from "../ui/section";
+import { getVisibleBlogPosts, BLOG_DESCRIPTION } from '@/features/blog'
+import { isAdmin } from '@/utils/is-admin'
+import { BlogPostsClient, PostCountHeader } from './posts-client'
+import { Section } from '../ui/section'
export async function BlogPosts({
- checkAdmin = true,
+ checkAdmin = true
}: {
- checkAdmin?: boolean;
+ checkAdmin?: boolean
}) {
- const userIsAdmin = checkAdmin ? await isAdmin() : false;
- const sortedBlogs = await getVisibleBlogPosts(userIsAdmin);
+ const userIsAdmin = checkAdmin ? await isAdmin() : false
+ const sortedBlogs = await getVisibleBlogPosts(userIsAdmin)
- return (
-
}
- noHeaderMargin
- >
-
-
-
- {BLOG_DESCRIPTION}
-
-
-
-
-
-
-
- );
+ return (
+
}
+ noHeaderMargin
+ >
+
+
+
+ {BLOG_DESCRIPTION}
+
+
+
+
+
+
+
+ )
}
diff --git a/src/components/blog/reaction-bar.tsx b/src/components/blog/reaction-bar.tsx
index 287f3245..042a58fe 100644
--- a/src/components/blog/reaction-bar.tsx
+++ b/src/components/blog/reaction-bar.tsx
@@ -49,11 +49,9 @@ export function ReactionBar({ slug }: ReactionBarProps) {
loadReactions()
}, [slug])
- const handleReaction = async (emoji: EmojiType) => {
+ async function handleReaction(emoji: EmojiType) {
setLoadingEmoji(emoji)
- const wasReacted = reactions[emoji].hasReacted
-
setReactions(prev => ({
...prev,
[emoji]: {
diff --git a/src/components/devtools-banner.tsx b/src/components/devtools-banner.tsx
index 99832203..7d4ea3a6 100644
--- a/src/components/devtools-banner.tsx
+++ b/src/components/devtools-banner.tsx
@@ -113,4 +113,4 @@ export function DevToolsBanner() {
)
-}
\ No newline at end of file
+}
diff --git a/src/components/landing/activity/activity-feed.tsx b/src/components/landing/activity/activity-feed.tsx
index 0e93acf2..4fbf908a 100644
--- a/src/components/landing/activity/activity-feed.tsx
+++ b/src/components/landing/activity/activity-feed.tsx
@@ -1,1022 +1,1067 @@
-import { useState, useEffect, useMemo, useCallback } from "react";
-import { motion, AnimatePresence, PanInfo } from "motion/react";
+import { useState, useEffect, useMemo, useCallback } from 'react'
+import { motion, AnimatePresence, PanInfo } from 'motion/react'
import {
- Music,
- GitCommit,
- GitPullRequest,
- Star,
- AlertCircle,
- Eye,
- Box,
- Copy,
- Plus,
- GitBranch,
- Lock,
- Globe,
-} from "lucide-react";
-import type { GitHubEventDetail } from "@/hooks/use-github";
+ Music,
+ GitCommit,
+ GitPullRequest,
+ Star,
+ AlertCircle,
+ Eye,
+ Box,
+ Copy,
+ Plus,
+ GitBranch,
+ Lock,
+ Globe
+} from 'lucide-react'
+import type { GitHubEventDetail } from '@/hooks/use-github'
import {
- COMBINED_ACTIVITY_LIMIT,
- COMBINED_TRACKS_LIMIT,
- useCombinedActivity,
-} from "@/hooks/use-combined-activity";
-import { ProjectHoverWrapper, SpotifyHoverWrapper } from "./hover-wrappers";
-import { useSpotifyPlayback } from "@/hooks/use-spotify-playback";
-import type { SpotifyTrack } from "@/features/spotify/client";
+ COMBINED_ACTIVITY_LIMIT,
+ COMBINED_TRACKS_LIMIT,
+ useCombinedActivity
+} from '@/hooks/use-combined-activity'
+import { ProjectHoverWrapper, SpotifyHoverWrapper } from './hover-wrappers'
+import { useSpotifyPlayback } from '@/hooks/use-spotify-playback'
+import type { SpotifyTrack } from '@/features/spotify/client'
-const SMOOTH_EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
-const SPOTIFY_TRACKS_CACHE_KEY = "activity-feed:spotify-tracks";
+const SMOOTH_EASE = [0.22, 1, 0.36, 1] as [number, number, number, number]
+const SPOTIFY_TRACKS_CACHE_KEY = 'activity-feed:spotify-tracks'
// Sentence variants for automatic transitions with stagger
const sentenceVariants = {
- initial: { opacity: 0 },
- animate: {
- opacity: 1,
- transition: {
- staggerChildren: 0.08,
- delayChildren: 0.1,
- },
- },
- exit: {
- opacity: 0,
- transition: {
- staggerChildren: 0.04,
- staggerDirection: -1,
- },
- },
-};
+ initial: { opacity: 0 },
+ animate: {
+ opacity: 1,
+ transition: {
+ staggerChildren: 0.08,
+ delayChildren: 0.1
+ }
+ },
+ exit: {
+ opacity: 0,
+ transition: {
+ staggerChildren: 0.04,
+ staggerDirection: -1
+ }
+ }
+}
// Directional variants for manual slide transitions
const slideVariants = {
- enter: (direction: number) => ({
- x: direction > 0 ? 300 : -300,
- opacity: 0,
- scale: 0.95,
- }),
- center: {
- zIndex: 1,
- x: 0,
- opacity: 1,
- scale: 1,
- transition: {
- x: { type: "spring" as const, stiffness: 300, damping: 30 },
- opacity: { duration: 0.2 },
- scale: { duration: 0.2 },
- },
- },
- exit: (direction: number) => ({
- zIndex: 0,
- x: direction < 0 ? 300 : -300,
- opacity: 0,
- scale: 0.95,
- transition: {
- x: { type: "spring" as const, stiffness: 300, damping: 30 },
- opacity: { duration: 0.2 },
- scale: { duration: 0.2 },
- },
- }),
-};
+ enter: (direction: number) => ({
+ x: direction > 0 ? 300 : -300,
+ opacity: 0,
+ scale: 0.95
+ }),
+ center: {
+ zIndex: 1,
+ x: 0,
+ opacity: 1,
+ scale: 1,
+ transition: {
+ x: { type: 'spring' as const, stiffness: 300, damping: 30 },
+ opacity: { duration: 0.2 },
+ scale: { duration: 0.2 }
+ }
+ },
+ exit: (direction: number) => ({
+ zIndex: 0,
+ x: direction < 0 ? 300 : -300,
+ opacity: 0,
+ scale: 0.95,
+ transition: {
+ x: { type: 'spring' as const, stiffness: 300, damping: 30 },
+ opacity: { duration: 0.2 },
+ scale: { duration: 0.2 }
+ }
+ })
+}
const wordVariants = {
- initial: {
- y: 20,
- opacity: 0,
- },
- animate: {
- y: 0,
- opacity: 1,
- transition: {
- duration: 0.3,
- ease: SMOOTH_EASE,
- },
- },
- exit: {
- y: -10,
- opacity: 0,
- transition: {
- duration: 0.2,
- ease: SMOOTH_EASE,
- },
- },
-};
+ initial: {
+ y: 20,
+ opacity: 0
+ },
+ animate: {
+ y: 0,
+ opacity: 1,
+ transition: {
+ duration: 0.3,
+ ease: SMOOTH_EASE
+ }
+ },
+ exit: {
+ y: -10,
+ opacity: 0,
+ transition: {
+ duration: 0.2,
+ ease: SMOOTH_EASE
+ }
+ }
+}
const highlightVariants = {
- initial: {
- y: 24,
- opacity: 0,
- scale: 0.9,
- },
- animate: {
- y: 0,
- opacity: 1,
- scale: 1,
- transition: {
- duration: 0.3,
- ease: SMOOTH_EASE,
- },
- },
- exit: {
- y: -12,
- opacity: 0,
- scale: 0.95,
- transition: {
- duration: 0.25,
- ease: SMOOTH_EASE,
- },
- },
-};
-
-function getEventIcon(type: GitHubEventDetail["type"]) {
- switch (type) {
- case "commit":
- return