Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"mode": "pre",
"tag": "alpha",
"tag": "beta",
"initialVersions": {
"portfolio": "1.0.4"
},
Expand Down
14 changes: 14 additions & 0 deletions .changeset/release-1780454025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"portfolio": patch
---

- release: switch 1.0.4 from alpha to beta
- chore: remove old file of changeset
- chore(config): adjust allowed dev origins and remote patterns in next.config.ts
- feat(config): support explicit id fields for social link redirects
- chore(config): remove legacy redirects from next.config.ts
- chore(config): dynamically generate redirects for social links
- chore(portfolio): configure experiences, sponsors, social links, and tech stack
- ui(tech-stack): re-order entries and add linux, bash, and antigravity
- ui(tech-stack): fetch technology logos dynamically from GitHub repos
- ui(projects): fallback to github avatar for projects and update OG assets
90 changes: 41 additions & 49 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import type { NextConfig } from "next"

import { SOCIAL_LINKS } from "./src/data/portfolio/social-links"

const nextConfig: NextConfig = {
reactStrictMode: true,
transpilePackages: ["next-mdx-remote"],
allowedDevOrigins: ["ncdai.localhost", "ncdai.local"],
allowedDevOrigins: ["wistant.localhost", "wistant.local"],
devIndicators: false,
images: {
remotePatterns: [
{
protocol: "https",
hostname: "assets.chanhdai.com",
hostname: "github.com",
port: "",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
port: "",
},
{
Expand All @@ -29,53 +36,38 @@ const nextConfig: NextConfig = {
}
: undefined,
async redirects() {
return [
{
source: "/:section(blog|components)/writing-effect-inspired-by-apple",
destination: "/:section/apple-hello-effect",
permanent: true,
},
{
source: "/:section(blog|components)/work-experience",
destination: "/:section/work-experience-component",
permanent: true,
},
{
source: "/:section(blog|components)/theme-switcher-component",
destination: "/:section/theme-switcher",
permanent: true,
},
{
source: "/wall-of-love",
destination: "/testimonials",
permanent: true,
},
{
source: "/blocks/content",
destination: "/blocks/marketing",
permanent: true,
},
{
source: "/blocks/content/blog-01",
destination: "/blocks/marketing/blog-01",
permanent: true,
},
{
source: "/blocks/content/blog-02",
destination: "/blocks/marketing/blog-02",
permanent: true,
},
{
source: "/blocks/content/experience-01",
destination: "/blocks/marketing/experience-01",
permanent: true,
},
{
source: "/blocks/content/team-01",
destination: "/blocks/marketing/team-01",
permanent: true,
},
]
const socialRedirects = SOCIAL_LINKS.flatMap((link) => {
const primarySlug =
link.id || link.title.toLowerCase().replace(/[^a-z0-9]/g, "")
const aliases = [primarySlug]
if (
primarySlug === "x" ||
primarySlug === "twitter" ||
primarySlug === "xtwitter"
) {
if (!aliases.includes("x")) aliases.push("x")
if (!aliases.includes("twitter")) aliases.push("twitter")
}
if (primarySlug === "github" || primarySlug === "git") {
if (!aliases.includes("github")) aliases.push("github")
if (!aliases.includes("git")) aliases.push("git")
}
if (primarySlug === "telegram" || primarySlug === "tg") {
if (!aliases.includes("telegram")) aliases.push("telegram")
if (!aliases.includes("tg")) aliases.push("tg")
}
if (primarySlug === "bluesky" || primarySlug === "bsky") {
if (!aliases.includes("bluesky")) aliases.push("bluesky")
if (!aliases.includes("bsky")) aliases.push("bsky")
}
return aliases.map((alias) => ({
source: `/${alias}`,
destination: link.href,
permanent: false,
}))
})

return socialRedirects
},
async rewrites() {
return [
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/components/experiences/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function Experiences() {
return (
<Panel id="experience">
<PanelHeader>
<PanelTitle>Work Experiences 🧿</PanelTitle>
<PanelTitle>Work Experiences 💼</PanelTitle>
</PanelHeader>

<div className="pr-2 pl-4">
Expand Down
9 changes: 6 additions & 3 deletions src/app/(app)/components/projects/project-item.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Image from "next/image"
import Link from "next/link"
import { getDocBySlug } from "@/data/doc/documents"
import { USER } from "@/data/portfolio/user"
import { addQueryParams } from "@/utils/url"
import { BoxIcon, InfinityIcon, LinkIcon } from "lucide-react"

Expand Down Expand Up @@ -36,17 +37,19 @@ export function ProjectItem({
const isSinglePeriod = end === start
const hasLocalPage = !!getDocBySlug(project.id)

const logoSrc = project.logo || USER.logo || USER.avatar

return (
<Collapsible className={className} defaultOpen={project.isExpanded}>
<div className="flex items-center hover:bg-accent-muted">
{project.logo ? (
{logoSrc ? (
<Image
src={project.logo}
src={logoSrc}
alt={project.title}
width={32}
height={32}
quality={100}
className="mx-4 flex size-6 shrink-0 select-none"
className="mx-4 flex size-6 shrink-0 rounded-md object-cover select-none"
unoptimized
aria-hidden
/>
Expand Down
90 changes: 90 additions & 0 deletions src/app/(app)/components/sponsors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Link from "next/link"
import { SPONSORS } from "@/data/sponsor-data"
import { addQueryParams } from "@/utils/url"
import { ArrowUpRightIcon, HeartIcon } from "lucide-react"

import { SPONSORSHIP_URL, UTM_PARAMS } from "@/config/site"
import { Button } from "@/components/base/ui/button"

import {
Panel,
PanelContent,
PanelHeader,
PanelTitle,
PanelTitleSup,
} from "./panel"

export function Sponsors() {
const featuredSponsors = SPONSORS.slice(0, 6)

return (
<Panel id="sponsors">
<PanelHeader>
<PanelTitle>
Sponsors 💖
<PanelTitleSup>[{SPONSORS.length}]</PanelTitleSup>
</PanelTitle>
</PanelHeader>

<PanelContent className="space-y-6">
<p className="font-mono text-sm text-muted-foreground">
Grateful to the partners and individuals supporting this open-source
work. You can sponsor my projects by choosing a tier (Platinum, Gold,
Silver, or Spark Supporter) directly on GitHub.
</p>

{featuredSponsors.length > 0 && (
<div className="relative">
<div className="pointer-events-none absolute inset-0 -z-1 grid grid-cols-2 gap-4 sm:grid-cols-3">
<div className="border-r border-line" />
<div className="border-r border-line max-sm:hidden" />
<div className="border-l border-line" />
</div>

<ul className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{featuredSponsors.map((item) => (
<li
key={item.name}
className="flex h-16 items-center justify-center border-b border-line px-4 text-muted-foreground/60 transition-colors duration-200 hover:text-foreground [&_svg]:max-h-8 [&_svg]:w-full"
>
<a
href={addQueryParams(item.url, UTM_PARAMS)}
target="_blank"
rel="noopener"
aria-label={`${item.name} logo`}
className="flex size-full items-center justify-center"
>
<item.logo aria-hidden />
</a>
</li>
))}
</ul>
</div>
)}

<div className="flex flex-col items-center justify-center gap-3 pt-2 sm:flex-row">
<Button
className="w-full gap-2 border-none pr-2.5 pl-3 sm:w-auto"
size="sm"
nativeButton={false}
render={<a href={SPONSORSHIP_URL} target="_blank" rel="noopener" />}
>
Become a Sponsor
<HeartIcon className="size-3.5 fill-current text-rose-500" />
</Button>

<Button
variant="secondary"
className="w-full gap-2 border-none pr-2.5 pl-3 sm:w-auto"
size="sm"
nativeButton={false}
render={<Link href="/sponsors" />}
>
All Sponsors
<ArrowUpRightIcon className="size-3.5" />
</Button>
</div>
</PanelContent>
</Panel>
)
}
11 changes: 10 additions & 1 deletion src/app/(app)/components/tech-stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export function TechStack() {
<ul className="flex flex-wrap gap-2">
{TECH_STACK.map((tech) => {
const slug = KEY_MAP[tech.key] || tech.key
const owner = tech.github?.split("/")[0] || tech.github
const logoSrc = owner ? `https://github.com/${owner}.png` : null

return (
<li key={tech.key} className="flex">
<a
Expand All @@ -29,7 +32,13 @@ export function TechStack() {
aria-label={tech.title}
className="flex items-center gap-1.5 rounded-full bg-zinc-50 px-1.5 py-0.5 text-xs tracking-wide text-foreground ring-1 ring-border/80 select-none dark:bg-zinc-900 [&_img]:size-3.5"
>
{tech.theme ? (
{logoSrc ? (
<img
className="size-3.5 rounded-full object-cover"
src={logoSrc}
alt={`${tech.title} icon`}
/>
) : tech.theme ? (
<>
<img
className="hidden size-3.5 [html.light_&]:block"
Expand Down
5 changes: 4 additions & 1 deletion src/app/(app)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Insights } from "./components/insights"
import { ProfileHeader } from "./components/profile-header"
import { Projects } from "./components/projects"
import { SocialLinks } from "./components/social-links"
import { Sponsors } from "./components/sponsors"
import { TechStack } from "./components/tech-stack"

export const metadata: Metadata = {
Expand Down Expand Up @@ -56,7 +57,9 @@ export default function HomePage() {
<Experiences />
<Separator />

{/*<Sponsors />*/}
<Separator />
<Separator />
<Sponsors />
<Separator />

<Separator />
Expand Down
17 changes: 7 additions & 10 deletions src/app/og/domain/route.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { ImageResponse } from "next/og"
import { USER } from "@/data/portfolio/user"

const geistMedium = readFileSync(
join(process.cwd(), "src/assets/fonts/Geist-Medium.ttf")
Expand Down Expand Up @@ -56,17 +57,13 @@ export async function GET(request: Request) {
<div tw="absolute flex inset-x-0 h-px bg-zinc-200 bottom-16" />

<div tw="absolute flex bottom-16 right-16">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 256"
width={128}
<img
src={USER.logo || USER.avatar}
alt="Logo"
width={64}
height={64}
>
<path
fill="currentColor"
d="M192 256H64v-64h128v64ZM448 64H320v128h128v64H256V0h192v64ZM64 192H0V64h64v128ZM512 192h-64V64h64v128ZM192 64H64V0h128v64Z"
/>
</svg>
style={{ borderRadius: 8 }}
/>
</div>
</div>,
{
Expand Down
17 changes: 7 additions & 10 deletions src/app/og/simple/route.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { ImageResponse } from "next/og"
import { USER } from "@/data/portfolio/user"

const geistSemiBold = readFileSync(
join(process.cwd(), "src/assets/fonts/Geist-SemiBold.ttf")
Expand All @@ -24,17 +25,13 @@ export async function GET(request: Request) {
<div tw="absolute inset-x-0 bottom-12 flex h-px border border-zinc-800" />

<div tw="absolute top-18 left-18 flex">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 256"
width={128}
<img
src={USER.logo || USER.avatar}
alt="Logo"
width={64}
height={64}
>
<path
fill="currentColor"
d="M192 256H64v-64h128v64ZM448 64H320v128h128v64H256V0h192v64ZM64 192H0V64h64v128ZM512 192h-64V64h64v128ZM192 64H64V0h128v64Z"
/>
</svg>
style={{ borderRadius: 8 }}
/>
</div>

<div tw="absolute inset-x-0 top-40 bottom-24 flex flex-col justify-end border-t-2 border-zinc-800">
Expand Down
4 changes: 2 additions & 2 deletions src/config/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export const MOBILE_NAV: NavItem[] = [
...MAIN_NAV,
]

export const X_HANDLE = "@iamnwistant"
export const X_HANDLE = "@iamwistant"
export const GITHUB_USERNAME = "wistant"
export const SOURCE_CODE_GITHUB_REPO = "portfolio"
export const SOURCE_CODE_GITHUB_URL = "https://github.com/wistant/portfolio"

export const SPONSORSHIP_URL = "https://github.com/sponsors/witant"
export const SPONSORSHIP_URL = "https://github.com/sponsors/wistant"

export const UTM_PARAMS = {
utm_source: "wistant.me",
Expand Down
Loading
Loading