This commit is contained in:
Mackie 2026-05-29 22:26:58 +08:00
parent ff9edec0e3
commit f01df5a60f
4 changed files with 104 additions and 106 deletions

View file

@ -10,20 +10,20 @@ export async function Footer() {
const navItems = footerData?.navItems || [] const navItems = footerData?.navItems || []
return ( return (
<footer className="mt-auto border-t border-white/10 text-white relative z-10"> <footer className="mt-auto border-t border-foreground/10 relative z-10">
<div className="container py-8 flex flex-col items-center gap-4"> <div className="container py-8 flex flex-col items-center gap-4">
<nav className="flex flex-wrap justify-center gap-6"> <nav className="flex flex-wrap justify-center gap-6">
{navItems.map(({ link }, i) => { {navItems.map(({ link }, i) => {
return <CMSLink className="text-white/60 hover:text-white transition-colors text-sm" key={i} {...link} /> return <CMSLink className="text-foreground/60 hover:text-foreground transition-colors text-sm" key={i} {...link} />
})} })}
</nav> </nav>
<p className="text-white/40 text-sm text-center"> <p className="text-foreground/40 text-sm text-center">
&copy; {new Date().getFullYear()} ByMackie. All rights reserved. &copy; {new Date().getFullYear()} ByMackie. All rights reserved.
</p> </p>
</div> </div>
</footer> </footer>
) )
} }

View file

@ -1,80 +1,88 @@
'use client'
import React from 'react' import React from 'react'
import { cn } from '@/utilities/ui' import { cn } from '@/utilities/ui'
import { InfiniteSlider } from '@/components/ui/infinite-slider'
type SkillCategory = { const LOGOS = [
title: string { src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/aftereffects-plain.svg', alt: 'aftereffects' },
icon: string { src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/almalinux-original.svg', alt: 'almalinux' },
tags: { tag: string }[] { src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/amazonwebservices-original-wordmark.svg', alt: 'amazonwebservices' },
} { src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/android-plain-wordmark.svg', alt: 'android' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/angularjs-plain-wordmark.svg', alt: 'angularjs' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/apache-line-wordmark.svg', alt: 'apache' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/apple-original.svg', alt: 'apple' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/azure-original-wordmark.svg', alt: 'azure' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/blender-original.svg', alt: 'blender' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/bootstrap-plain-wordmark.svg', alt: 'bootstrap' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/cloudflare-original-wordmark.svg', alt: 'cloudflare' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/cpanel-original-wordmark.svg', alt: 'cpanel' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/docker-plain-wordmark.svg', alt: 'docker' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/drupal-plain-wordmark.svg', alt: 'drupal' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/eslint-plain-wordmark.svg', alt: 'eslint' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/expo-original-wordmark.svg', alt: 'expo' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/figma-original.svg', alt: 'figma' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/flutter-original.svg', alt: 'flutter' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/forgejo-original.svg', alt: 'forgejo' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/fusion-original.svg', alt: 'fusion' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/gimp-original.svg', alt: 'gimp' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/github-original-wordmark.svg', alt: 'github' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/gitlab-plain-wordmark.svg', alt: 'gitlab' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/googlecloud-original-wordmark.svg', alt: 'googlecloud' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/html5-original-wordmark.svg', alt: 'html5' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/illustrator-original.svg', alt: 'illustrator' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/inkscape-plain-wordmark.svg', alt: 'inkscape' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/javascript-original.svg', alt: 'javascript' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/jira-original-wordmark.svg', alt: 'jira' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/jquery-plain-wordmark.svg', alt: 'jquery' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/kalilinux-original-wordmark.svg', alt: 'kalilinux' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/linux-original.svg', alt: 'linux' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/magento-plain-wordmark.svg', alt: 'magento' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/mongodb-original-wordmark.svg', alt: 'mongodb' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/nextjs-original-wordmark.svg', alt: 'nextjs' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/nginx-original.svg', alt: 'nginx' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/nodejs-original-wordmark.svg', alt: 'nodejs' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/opencl-original.svg', alt: 'opencl' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/photoshop-original.svg', alt: 'photoshop' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/php-original.svg', alt: 'php' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/premierepro-original.svg', alt: 'premierepro' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/prometheus-plain-wordmark.svg', alt: 'prometheus' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/proxmox-original-wordmark.svg', alt: 'proxmox' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/python-original-wordmark.svg', alt: 'python' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/rails-plain-wordmark.svg', alt: 'rails' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/reactnative-original-wordmark.svg', alt: 'reactnative' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/reactnavigation-original.svg', alt: 'reactnavigation' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/slack-original-wordmark.svg', alt: 'slack' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/swift-original-wordmark.svg', alt: 'swift' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/typescript-original.svg', alt: 'typescript' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/web3js-original.svg', alt: 'web3js' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/wordpress-original.svg', alt: 'wordpress' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/xcode-original.svg', alt: 'xcode' },
{ src: 'https://portfolio-media.s3web.bymackie.com/Skill_logo/zend-original-wordmark.svg', alt: 'zend' },
]
type SkillsBlockProps = { type SkillsMarqueeBlockProps = {
heading?: string heading?: string
keySkills?: { skill: string }[]
categories?: SkillCategory[]
} }
export function SkillsBlock({ heading, keySkills, categories }: SkillsBlockProps) { export function SkillsMarqueeBlock({ heading }: SkillsMarqueeBlockProps) {
return ( return (
<section className="w-full max-w-5xl mx-auto px-6 py-16"> <section className="relative border-t border-foreground/10 py-8">
{/* Heading */} <p className="mb-6 text-center text-xs tracking-widest uppercase px-6 text-foreground/40">
{heading && ( {heading || 'Skills'}
<p className="text-center text-xs tracking-widest uppercase text-white/30 mb-6"> </p>
{heading} <div className="[mask-image:linear-gradient(to_right,transparent,black,transparent)]">
</p> <InfiniteSlider gap={48} duration={120}>
)} {LOGOS.map((l) => (
<img
{/* Key skills pills */} key={l.alt}
{Array.isArray(keySkills) && keySkills.length > 0 && ( src={l.src}
<div className="flex justify-center gap-3 flex-wrap mb-12"> alt={l.alt}
{keySkills.map(({ skill }, i) => ( className="h-14 w-auto flex-shrink-0 pointer-events-none select-none grayscale opacity-25 dark:opacity-40 dark:invert"
<span />
key={i}
className="text-xs text-white/70 border border-white/25 rounded-full px-4 py-1.5"
>
{skill}
</span>
))} ))}
</div> </InfiniteSlider>
)} </div>
{/* Categories grid */}
{Array.isArray(categories) && categories.length > 0 && (
<div
className="border border-white/8 rounded-xl overflow-hidden"
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
}}
>
{categories.map((cat, i) => {
const isLastRow = i >= categories.length - (categories.length % 3 || 3)
const isLastCol = (i + 1) % 3 === 0
return (
<div
key={i}
className={cn(
'p-6 flex flex-col gap-3',
!isLastCol && 'border-r border-white/8',
!isLastRow && 'border-b border-white/8',
)}
>
<i className={`ti ${cat.icon} text-white/40`} style={{ fontSize: 20 }} aria-hidden="true" />
<p className="text-sm font-medium text-white/85">{cat.title}</p>
<div className="flex flex-wrap gap-1.5">
{Array.isArray(cat.tags) && cat.tags.map(({ tag }, j) => (
<span
key={j}
className="text-xs text-white/35 border border-white/10 rounded-full px-2.5 py-0.5"
>
{tag}
</span>
))}
</div>
</div>
)
})}
</div>
)}
</section> </section>
) )
} }

View file

@ -83,8 +83,8 @@ export function SkillsMarqueeBlock({ heading }: SkillsMarqueeBlockProps) {
key={l.alt} key={l.alt}
src={l.src} src={l.src}
alt={l.alt} alt={l.alt}
style={{ filter: 'grayscale(1)', opacity: isDark ? 0.35 : 0.25 }} style={{ filter: 'grayscale(1) var(--tw-invert)' }}
className="h-14 w-auto flex-shrink-0 pointer-events-none select-none" className="h-14 w-auto flex-shrink-0 pointer-events-none select-none opacity-30 dark:opacity-40 dark:invert"
/> />
))} ))}
</InfiniteSlider> </InfiniteSlider>

View file

@ -164,31 +164,25 @@ function Background({ isDark }: { isDark: boolean }) {
type HeroProps = Pick<Page['hero'], 'richText' | 'links'> type HeroProps = Pick<Page['hero'], 'richText' | 'links'>
function HeroSection({ isDark, richText, links }: HeroProps & { isDark: boolean }) { function HeroSection({ richText, links }: HeroProps) {
const borderLine = isDark ? 'bg-white/10' : 'bg-black/10'
const borderLineFaint = isDark ? 'bg-white/5' : 'bg-black/5'
const richTextColor = isDark ? 'text-white' : 'text-black'
return ( return (
<section className="relative mx-auto w-full max-w-5xl"> <section className="relative mx-auto w-full max-w-5xl">
{/* Outer border lines */}
<div aria-hidden="true" className="absolute inset-0 mx-auto hidden min-h-screen w-full max-w-5xl lg:block pointer-events-none"> <div aria-hidden="true" className="absolute inset-0 mx-auto hidden min-h-screen w-full max-w-5xl lg:block pointer-events-none">
<div className={cn('mask-y-from-80% mask-y-to-100% absolute inset-y-0 left-0 z-10 h-full w-px', borderLine)} /> <div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 left-0 z-10 h-full w-px bg-foreground/10" />
<div className={cn('mask-y-from-80% mask-y-to-100% absolute inset-y-0 right-0 z-10 h-full w-px', borderLine)} /> <div className="mask-y-from-80% mask-y-to-100% absolute inset-y-0 right-0 z-10 h-full w-px bg-foreground/10" />
</div> </div>
{/* Inner border lines */}
<div aria-hidden="true" className="absolute inset-0 -z-[1] size-full overflow-hidden pointer-events-none"> <div aria-hidden="true" className="absolute inset-0 -z-[1] size-full overflow-hidden pointer-events-none">
<div className={cn('absolute inset-y-0 left-4 w-px bg-linear-to-b from-transparent via-border to-border md:left-8', borderLine)} /> <div className="absolute inset-y-0 left-4 w-px bg-linear-to-b from-transparent via-foreground/10 to-foreground/10 md:left-8" />
<div className={cn('absolute inset-y-0 right-4 w-px bg-linear-to-b from-transparent via-border to-border md:right-8', borderLine)} /> <div className="absolute inset-y-0 right-4 w-px bg-linear-to-b from-transparent via-foreground/10 to-foreground/10 md:right-8" />
<div className={cn('absolute inset-y-0 left-8 w-px bg-linear-to-b from-transparent via-border/50 to-border/50 md:left-12', borderLineFaint)} /> <div className="absolute inset-y-0 left-8 w-px bg-linear-to-b from-transparent via-foreground/5 to-foreground/5 md:left-12" />
<div className={cn('absolute inset-y-0 right-8 w-px bg-linear-to-b from-transparent via-border/50 to-border/50 md:right-12', borderLineFaint)} /> <div className="absolute inset-y-0 right-8 w-px bg-linear-to-b from-transparent via-foreground/5 to-foreground/5 md:right-12" />
</div> </div>
<div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-28 px-6 text-center"> <div className="relative flex flex-col items-center justify-center gap-5 pt-32 pb-28 px-6 text-center">
{richText && ( {richText && (
<RichText <RichText
className={cn( className="fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-100 duration-500 ease-out max-w-[36.5rem] text-center text-foreground"
'fade-in slide-in-from-bottom-10 animate-in fill-mode-backwards delay-100 duration-500 ease-out',
'max-w-[36.5rem] text-center',
richTextColor
)}
data={richText} data={richText}
enableGutter={false} enableGutter={false}
/> />
@ -202,12 +196,8 @@ function HeroSection({ isDark, richText, links }: HeroProps & { isDark: boolean
className={cn( className={cn(
'flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-all', 'flex items-center gap-2 rounded-full px-5 py-2.5 text-sm font-medium transition-all',
i === 0 i === 0
? isDark ? 'bg-foreground text-background hover:bg-foreground/90'
? 'bg-white text-black hover:bg-white/90' : 'border border-foreground/25 text-foreground hover:bg-foreground/8'
: 'bg-black text-white hover:bg-black/85'
: isDark
? 'border border-white/25 text-white hover:bg-white/8'
: 'border border-black/20 text-black hover:bg-black/5'
)} )}
/> />
))} ))}
@ -236,7 +226,7 @@ export default function HeroPage({ richText, links, children }: HeroPageProps) {
</div> </div>
<div style={{ position: 'relative', zIndex: 1 }}> <div style={{ position: 'relative', zIndex: 1 }}>
<main> <main>
<HeroSection isDark={isDark} richText={richText} links={links} /> <HeroSection richText={richText} links={links} />
{children && ( {children && (
<div id="content"> <div id="content">
{children} {children}
@ -246,4 +236,4 @@ export default function HeroPage({ richText, links, children }: HeroPageProps) {
</div> </div>
</div> </div>
) )
} }