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
6 changes: 6 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const nextConfig: NextConfig = {
port: "",
pathname: "/**"
},
{
protocol: "http",
hostname: "localhost",
port: "3001",
pathname: "/**"
}
]
},
};
Expand Down
Binary file added public/assets/avatars/profile.JPG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/avatars/profile.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/app/(web)/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { metadata as meta } from './data/meta';
import { AboutTimeline } from './sections/timeline';
import { Footer } from '@/components/ui/footer';
import { AboutMeSection } from './sections/home-aboutme';
import { ContactBar } from './sections/contact-bar';

export const metadata: Metadata = meta

Expand All @@ -19,7 +20,6 @@ export default function HomePage() {
<AboutTimeline />
<ProjectsSection />
</main>

<Footer />
</>
);
Expand Down
34 changes: 34 additions & 0 deletions src/app/(web)/(home)/sections/contact-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";
import { FacebookIcon } from '@/components/icons/facebook';
import { FigmaIcon } from '@/components/icons/figma';
import { GitHubIcon } from '@/components/icons/github';
import { motion } from 'framer-motion';
import Link from 'next/link';

export const ContactBar = () => {

const headlineVariants = {
hidden: { opacity: 0, y: -10 },
visible: { opacity: 1, y: 0, transition: { duration: 0.4, delay: 0.3 } }
};

return (
<motion.div
variants={headlineVariants}
initial="hidden"
animate="visible"
className='flex-col flex absolute max-sm:right-3 right-0 max-sm:top-3 top-1/4'>
<ol className='items-center flex flex-col gap-5'>
<li>
<Link href="https://fb.me/chat.leatsophat" target='_blank'> <FacebookIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://github.com/pphatdev" target='_blank'> <GitHubIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://t.me/SophatLEAT" target='_blank'> <FigmaIcon className='stroke-1 size-6 text-foreground/50 hover:text-foreground' /> </Link>
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Telegram link is using the FigmaIcon; import and use a TelegramIcon component instead to correctly represent the platform.

Suggested change
<Link href="https://t.me/SophatLEAT" target='_blank'> <FigmaIcon className='stroke-1 size-6 text-foreground/50 hover:text-foreground' /> </Link>
<Link href="https://t.me/SophatLEAT" target='_blank'> <TelegramIcon className='stroke-1 size-6 text-foreground/50 hover:text-foreground' /> </Link>

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +29
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

External links opened with target="_blank" should include rel="noopener noreferrer" to prevent reverse tabnabbing vulnerabilities.

Suggested change
<Link href="https://fb.me/chat.leatsophat" target='_blank'> <FacebookIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://github.com/pphatdev" target='_blank'> <GitHubIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://t.me/SophatLEAT" target='_blank'> <FigmaIcon className='stroke-1 size-6 text-foreground/50 hover:text-foreground' /> </Link>
<Link href="https://fb.me/chat.leatsophat" target='_blank' rel="noopener noreferrer"> <FacebookIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://github.com/pphatdev" target='_blank' rel="noopener noreferrer"> <GitHubIcon className='size-6 text-foreground/50 hover:text-foreground' /> </Link>
</li>
<li>
<Link href="https://t.me/SophatLEAT" target='_blank' rel="noopener noreferrer"> <FigmaIcon className='stroke-1 size-6 text-foreground/50 hover:text-foreground' /> </Link>

Copilot uses AI. Check for mistakes.
</li>
</ol>
</motion.div>
);
}
9 changes: 4 additions & 5 deletions src/app/(web)/(home)/sections/hero-section.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"use client";
import React, { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle, useMemo, } from 'react';
import { Announcing } from '@/app/(web)/(home)/sections/announcing';
import { motion, AnimatePresence, useScroll, useMotionValueEvent, type Transition, type VariantLabels, type TargetAndTransition, type Variants, } from 'framer-motion';
import { Profile } from '@/app/(web)/(home)/sections/profile';
import { MagneticArea } from '@/components/magnetic-aria';
import { cn } from '@/lib/utils';
import Image from 'next/image';
import { ThemeToggle } from '@/components/ui/theme-switch';
import TechStack from '@/components/tech-stack';
import { ContactBar } from './contact-bar';

interface RotatingTextRef {
next: () => void;
Expand Down Expand Up @@ -445,6 +443,7 @@ const HomeHeroSection: React.FC = () => {
<>
<canvas ref={canvasRef} className="absolute inset-0 z-[-1] w-full overflow-hidden pointer-events-none opacity-80" />
<div className='w-full max-w-6xl sticky flex-grow flex flex-col items-start justify-start mx-auto px-1 md:px-14 md:pt-28 lg:px-4 h-full max-sm:pb-5 pb-14 z-10'>
<ContactBar />
<div className='flex gap-5 max-lg:items-center max-lg:flex-col items-start w-full justify-between max-sm:border max-sm:pb-4 max-sm:mt-1'>
<div className='w-full max-sm:text-center flex flex-col max-w-4xl'>
{/* <Announcing className='w-full max-lg:text-center max-md:mt-10 max-sm:order-1 order-0' /> */}
Expand Down Expand Up @@ -536,9 +535,9 @@ const HomeHeroSection: React.FC = () => {

</div>

<MagneticArea className='max-xs:scale-100 shrink-0 max-lg:order-first max-sm:size-52 my-2 w-80 items-center justify-center'>
<div className='max-xs:scale-100 relative inline-flex shrink-0 max-lg:order-first max-sm:size-52 my-2 w-96 items-center justify-center'>
<Profile />
</MagneticArea>
</div>
</div>
<div className='hidden sm:block w-full'>
<TechStack />
Expand Down
59 changes: 29 additions & 30 deletions src/app/(web)/(home)/sections/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,42 @@
"use client";
import { motion, type Variants } from 'framer-motion';
import { useCallback } from 'react';
import { RainbowGlow } from './rainbow-glow';
import Image from 'next/image';

export const Profile = () => {

const loadImage = (src: string): Promise<HTMLImageElement> => new Promise<HTMLImageElement>(resolve => {
const img = new Image();
img.onload = () => resolve(img);
img.src = src;
});
// const loadImage = (src: string): Promise<HTMLImageElement> => new Promise<HTMLImageElement>(resolve => {
// const img = new Image();
// img.onload = () => resolve(img);
// img.src = src;
// });

const renderCanvas = useCallback((canvas: HTMLCanvasElement | null): void => {
if (!canvas) return;
// const renderCanvas = useCallback((canvas: HTMLCanvasElement | null): void => {
// if (!canvas) return;

const ctx = canvas.getContext('2d');
if (!ctx) return;
// const ctx = canvas.getContext('2d');
// if (!ctx) return;

canvas.width = 500;
canvas.height = 500;
// canvas.width = 500;
// canvas.height = 500;

Promise.all([
loadImage('/assets/gallery/WEBP/IMG_1915.webp'),
loadImage('/assets/masks/mask.webp')
]).then(([img, mask]) => {
const scale = Math.max(canvas.width / img.width, canvas.height / img.height);
const x = (canvas.width - img.width * scale) / 2;
const y = (canvas.height - img.height * scale) / 2;
// Promise.all([
// loadImage('/assets/avatars/profile.JPG'),
// // loadImage('/assets/gallery/WEBP/IMG_1915.webp'),
// loadImage('/assets/masks/mask.webp')
// ]).then(([img, mask]) => {
// const scale = Math.max(canvas.width / img.width, canvas.height / img.height);
// const x = (canvas.width - img.width * scale) / 2;
// const y = (canvas.height - img.height * scale) / 2;

// Draw image with object-cover behavior
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
// // Draw image with object-cover behavior
// ctx.drawImage(img, x, y, img.width * scale, img.height * scale);

// Apply mask
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(mask, 0, 0, 500, 500);
});
}, []);
// // Apply mask
// ctx.globalCompositeOperation = 'destination-in';
// ctx.drawImage(mask, 0, 0, 500, 500);
// });
// }, []);

Comment on lines +8 to 40
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider removing the large block of commented-out canvas rendering code to improve readability and maintainability.

Copilot uses AI. Check for mistakes.
const contentDelay = 0.3;
const bannerVariants: Variants = {
Expand All @@ -48,11 +49,9 @@ export const Profile = () => {
variants={bannerVariants}
initial="hidden"
animate="visible"
className="max-w-sm w-full h-full relative max-sm:border inline-flex mt-2 items-center justify-center"
className="max-w-sm w-full h-full relative max-sm:border inline-flex items-center justify-center xl:p-7"
>
<div className="p-7 h-full w-full">
<canvas ref={renderCanvas} className="h-full w-full bg-center m-1" />
</div>
<Image src={'/assets/avatars/profile.webp'} width={512} height={512} className='w-full lg:rounded-4xl object-cover aspect-square' alt='Profile Image'></Image>
<RainbowGlow className="opacity-20" />
</motion.div>
);
Expand Down
7 changes: 4 additions & 3 deletions src/app/(web)/(home)/sections/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import React from "react";
import ProjectCard from "@/components/ui/project-card";
import { motion } from 'framer-motion';
import { useProjects } from "@/hooks/projects";
import { Badge } from "@/components/ui/badge";

const Projects: React.FC = () => {
const { projects, loading, error } = useProjects();

return (
<div className="w-full max-w-6xl mx-auto max-sm:p-0 px-4 pb-16 grid sm:grid-cols-2 lg:grid-cols-3 gap-7 stick">
<div className="w-full max-w-6xl mx-auto max-sm:p-0 px-4 pb-16 grid sm:grid-cols-2 lg:grid-cols-3 gap-7 sticky">
{loading && <p className="text-center text-foreground/50 col-span-full">Loading projects...</p>}
{error && <p className="text-center text-destructive col-span-full">Error: {error}</p>}

Expand All @@ -36,8 +37,7 @@ export const ProjectsSection = () => {
}}
initial="hidden"
animate="visible" className='mx-auto w-full'>
<div
className="w-full mx-auto max-sm:p-3 max-sm:pb-4 z-[999] p-5 gap-4 sticky shadow-2xl shadow-primary/5 bg-card backdrop-blur-[2px]">
<div className="w-full mx-auto max-sm:p-3 max-sm:pb-4 z-[999] p-5 gap-4 sticky shadow-2xl shadow-primary/5 bg-background backdrop-blur-[2px]">
<motion.div
variants={{
hidden: { opacity: 0, y: -20 },
Expand All @@ -47,6 +47,7 @@ export const ProjectsSection = () => {
animate="visible"
className="w-full py-3 max-sm:px-0 px-4 mb-10 mx-auto text-start max-w-6xl z-50 font-sans">

<Badge variant="outline" className='py-1.5 px-3'>Projects</Badge>
<h1 className="w-full py-3 mx-auto text-start max-w-6xl z-50 max-md:text-3xl text-4xl font-bold font-sans">
Featured <span className="text-left bg-background bg-clip-text bg-no-repeat text-transparent bg-gradient-to-r from-sky-500 via-teal-500 to-green-500 [text-shadow:0_0_rgba(0,0,0,0.1)]"> Projects </span>
</h1>
Expand Down
7 changes: 5 additions & 2 deletions src/app/(web)/(home)/sections/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { motion } from 'framer-motion';
import { cn } from "@/lib/utils";
import { NextJSIcon } from "@/components/icons/nextjs";
import { CompaniesProps } from "@/lib/interfaces/skills";
import { Badge } from "@/components/ui/badge";

export const AboutTimeline = () => {

const experiences: CompaniesProps[] = [
const experiences: CompaniesProps[] = [
{
title: "TURBOTECH CO., LTD",
logo: "assets/brands/org/turbotech.png",
Expand Down Expand Up @@ -78,7 +79,7 @@ export const AboutTimeline = () => {
{ title: "Sass", image: "assets/brands/language/sass.svg" },
{ title: "Bootstrap", image: "assets/brands/language/bootstrap.svg" },
{ title: "React", image: "assets/brands/language/react.svg" },
{ title: "Next.js", icon: <NextJSIcon className="size-6 stroke-1 text-foreground/90"/> },
{ title: "Next.js", icon: <NextJSIcon className="size-6 stroke-1 text-foreground/90" /> },
{ title: "Nuxt.js", image: "assets/brands/language/nuxtjs.svg" },
{ title: "EJS", image: "assets/brands/language/ejs.svg" },
{ title: "PHP", image: "assets/brands/language/php.svg" },
Expand Down Expand Up @@ -116,6 +117,8 @@ export const AboutTimeline = () => {
className='z-50 relative max-sm:py-0 bg-gradient-to-b from-background to-transparent backdrop-blur-[2px] w-full py-10'>

<div className="mx-auto max-w-6xl w-full sm:px-4 sm:my-10">

<Badge variant="outline" className='py-1.5 px-3'>Experience</Badge>
<h1 className="w-full py-3 px-4 bg-background/80 max-sm:border-b sticky z-50 top-0 max-md:text-3xl text-4xl text-start tracking-tighter font-bold font-sans">
Work <span className="text-left bg-clip-text bg-no-repeat text-transparent bg-gradient-to-r from-sky-500 via-teal-500 to-green-500 [text-shadow:0_0_rgba(0,0,0,0.1)]">Experience</span>
</h1>
Expand Down
4 changes: 2 additions & 2 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@
}

.dark {
--background: 0 0% 6%;
--background: 0 0% 5%;
--foreground: 0 0% 95%;

--card: 240 10% 3.9%;
--card: 0 0% 10%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/avatar-circles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const AvatarCircles: React.FC<AvatarCirclesProps> = ({
className="rounded-full object-cover max-sm:rounded-none border-2 border-foreground/10"
src={url.imageUrl}
fill
sizes="40px"
alt={`Avatar ${url?.title ?? ''} ?? ""`}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const ProjectCard: React.FC<{ project: Project, className?: string }> = ({ proje

return (
<div
className={cn("col-span-1 top-5 sticky duration-300 drop-shadow-xl drop-shadow-black/[0.02] overflow-hidden max-sm:p-1 p-0.5 max-sm:rounded-none bg-background group font-sans rounded-4xl mb-4 ring-1 ring-border hover:ring-primary/50 hover:ring-2 transition-all ease-in-out flex flex-col h-full", className)}
className={cn("col-span-1 top-5 sticky duration-300 drop-shadow-xl drop-shadow-black/[0.02] overflow-hidden max-sm:p-1 p-0.5 max-sm:rounded-none bg-card group font-sans rounded-4xl mb-4 ring-1 ring-border hover:ring-primary/50 hover:ring-2 transition-all ease-in-out flex flex-col h-full", className)}
role="article"
tabIndex={-1}>

Expand Down