OpenClaw + ClawTeam + Superpowers Git Initialization
@@ -1,169 +0,0 @@
|
||||
# Lô Tô Online — Product Requirements Document (PRD)
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision
|
||||
|
||||
Mang trải nghiệm **Lô Tô truyền thống miền Nam** lên nền tảng Zalo Mini App với phong cách hoài niệm tuổi thơ: giấy cũ ố vàng, rổ tre, túi vải đen đựng số, hạt gạo đặt lên vé. Người chơi tự tay bấm đặt hạt gạo — đúng sai tự chịu, giữ nguyên cảm giác hồi hộp của Lô Tô đời thực.
|
||||
|
||||
**MVP đầu tiên** sẽ chạy trên **web (mobile-first)** để thử nghiệm, sau đó chuyển sang **Zalo Mini App** khi ổn định.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target Users
|
||||
|
||||
| Persona | Mô tả |
|
||||
|---|---|
|
||||
| Người Việt 20–45 tuổi | Lớn lên với Lô Tô, muốn hoài niệm |
|
||||
| Nhóm bạn / gia đình | Chơi giải trí online cùng nhau |
|
||||
| Casual gamer | Thích game đơn giản, may rủi, vui nhộn |
|
||||
|
||||
---
|
||||
|
||||
## 3. Core Game Rules (Lô Tô truyền thống)
|
||||
|
||||
### 3.1 Vé Lô Tô
|
||||
|
||||
- Bảng **3 hàng × 9 cột** = 27 ô
|
||||
- Mỗi hàng có đúng **5 ô số** + 4 ô trống
|
||||
- Số từ **1–90**, mỗi cột chứa số thuộc hàng chục tương ứng (cột 1: 1–9, cột 2: 10–19, …, cột 9: 80–90)
|
||||
- Mỗi vé có **15 số** duy nhất, không trùng
|
||||
|
||||
### 3.2 Lượt chơi
|
||||
|
||||
1. Hệ thống **lắc túi vải** → **bốc 1 viên số** ngẫu nhiên (1–90, không lặp)
|
||||
2. Hiển thị animation bốc số + **đọc số bằng giọng miền Nam**
|
||||
3. Người chơi có **1–5 giây** (cài đặt khi tạo phòng) để **tự bấm đặt hạt gạo** vào ô tương ứng
|
||||
4. Hết thời gian highlight → người hô đọc số tiếp, **không chờ** ai
|
||||
5. Người chơi vẫn có thể **đặt muộn** (theo trí nhớ) bất cứ lúc nào
|
||||
|
||||
### 3.3 Điều kiện "Kinh" (Thắng)
|
||||
|
||||
- Hoàn thành **1 hàng ngang** (5/5 ô số trong hàng đều có hạt gạo)
|
||||
- Hô **"Kinh!"** → hệ thống verify
|
||||
|
||||
### 3.4 Verify & Phạt
|
||||
|
||||
| Trường hợp | Kết quả |
|
||||
|---|---|
|
||||
| Tất cả hạt gạo đúng ô đã bốc | ✅ **Thắng** — nhận tiền thưởng từ tất cả người thua. Nếu có nhiều người cùng Kinh đúng ở một lượt, tiền thưởng được **chia đều** |
|
||||
| Có ≥1 hạt gạo đặt nhầm ô (ô chưa được bốc) | ❌ **Phạt** — đền bằng **mức cược của phòng** cho **mỗi** người chơi còn lại |
|
||||
|
||||
### 3.5 Kết thúc ván
|
||||
|
||||
- Khi có người **kinh hợp lệ** → ván kết thúc (hoặc kết thúc lượt bốc hiện tại để xét những người kinh cùng lúc)
|
||||
- Hoặc khi bốc hết 90 số mà chưa ai kinh → hoà (trả lại cược)
|
||||
|
||||
---
|
||||
|
||||
## 4. Features
|
||||
|
||||
### 4.1 Lobby & Phòng chơi
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Lobby công khai | Danh sách phòng đang chờ, lọc theo mức cược |
|
||||
| Tạo phòng | Chủ phòng cài đặt: mức cược, thời gian highlight (1–5s), số người (2–8) |
|
||||
| Vào phòng | Click vào phòng trong lobby |
|
||||
| Chờ & sẵn sàng | Tất cả sẵn sàng → bắt đầu |
|
||||
| Rời phòng | Rời giữa chừng = bỏ cuộc, mất cược |
|
||||
|
||||
### 4.2 Gameplay
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Lắc túi vải | Animation túi vải đen mềm mại + vật lý khi lắc |
|
||||
| Bốc số | Animation tay rút viên số từ túi ra |
|
||||
| Đọc số | Giọng đọc miền Nam truyền thống |
|
||||
| Đặt hạt gạo | Click vào ô trên vé → hạt gạo rơi xuống (animation) |
|
||||
| Đặt muộn | Vẫn bấm được sau khi hết highlight |
|
||||
| Hô kinh | Nút hô kinh khi nghĩ mình hoàn thành hàng |
|
||||
| Verify | Hệ thống kiểm tra → thắng hoặc phạt |
|
||||
|
||||
### 4.3 Economy (Hệ thống tiền tệ)
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Đơn vị tiền | **"Đồng"** (xu ảo) |
|
||||
| Nhận đồng miễn phí | Sự kiện hàng ngày, nhiệm vụ, hoạt động |
|
||||
| Nạp tiền | Mua đồng bằng tiền thật (IAP / Zalo Pay) |
|
||||
| Cược | Đặt cược khi vào phòng |
|
||||
| Thắng | Nhận cược từ tất cả người chơi thua |
|
||||
| Phạt kinh sai | Trả mức cược × số người chơi còn lại |
|
||||
|
||||
### 4.4 Profile & Xếp hạng
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Đăng nhập Zalo | Dùng Zalo account, lấy avatar + tên |
|
||||
| Profile | Tổng đồng, số ván thắng/thua, tỷ lệ thắng |
|
||||
| Lịch sử chơi | Danh sách ván đã chơi, kết quả, thời gian |
|
||||
| Bảng xếp hạng | Theo tổng đồng kiếm được |
|
||||
|
||||
### 4.5 Social (Trong phòng)
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Chat | Nhắn tin trong phòng chơi |
|
||||
| Reactions / Emoji | Gửi emoji nổi lên màn hình |
|
||||
|
||||
### 4.6 Audio
|
||||
|
||||
| Feature | Chi tiết |
|
||||
|---|---|
|
||||
| Giọng đọc số | Giọng miền Nam, đọc từng số theo kiểu lô tô |
|
||||
| Nhạc nền | Vui tươi, gợi hoài niệm tuổi thơ |
|
||||
| SFX — đặt hạt gạo | Tiếng gạo rơi nhẹ |
|
||||
| SFX — đợi | Tiếng hò reo nhỏ, xì xào khi đợi |
|
||||
| SFX — kinh | La lên, pháo hoa, reo hò |
|
||||
| SFX — kinh sai | Tiếng thở dài, ồ tiếc nuối |
|
||||
|
||||
---
|
||||
|
||||
## 5. Art Direction (Phong cách hoài niệm)
|
||||
|
||||
| Yếu tố | Chi tiết |
|
||||
|---|---|
|
||||
| **Nền** | Giấy cũ ố vàng, texture gỗ mộc |
|
||||
| **Vé lô tô** | Giấy carton cũ, viền tròn, chữ in mờ kiểu cũ |
|
||||
| **Túi số** | Vải đen mềm, animation vật lý khi lắc |
|
||||
| **Viên số** | Bi gỗ tròn, khắc số đỏ |
|
||||
| **Hạt gạo** | Hạt gạo trắng thật, có bóng đổ nhẹ |
|
||||
| **Rổ tre** | Đựng các viên số đã bốc |
|
||||
| **Font** | Handwritten / retro Việt Nam |
|
||||
| **Màu chủ đạo** | Nâu gỗ, vàng ố, đỏ đất, xanh lá cũ |
|
||||
|
||||
---
|
||||
|
||||
## 6. Platform & Technical Constraints
|
||||
|
||||
| Hạng mục | Chi tiết |
|
||||
|---|---|
|
||||
| **MVP platform** | Web (mobile-first), responsive cho điện thoại |
|
||||
| **Production platform** | Zalo Mini App |
|
||||
| **Multiplayer** | Real-time (WebSocket) |
|
||||
| **Min players** | 2 |
|
||||
| **Max players** | 8 |
|
||||
| **Target device** | Mobile only |
|
||||
|
||||
---
|
||||
|
||||
## 7. Out of Scope (v1)
|
||||
|
||||
- Tablet / Desktop optimized layout
|
||||
- Tournament mode
|
||||
- Friend list / invite via Zalo
|
||||
- Spectator mode
|
||||
- AI bot players
|
||||
- Multiple vé per player
|
||||
|
||||
---
|
||||
|
||||
## 8. Success Metrics
|
||||
|
||||
| Metric | Target |
|
||||
|---|---|
|
||||
| Prototype hoàn chỉnh | Full gameplay + multiplayer + animation + audio |
|
||||
| Latency bốc số → hiện | < 200ms |
|
||||
| Animation FPS | ≥ 30fps trên mid-range phone |
|
||||
| Ván chơi trung bình | 3–8 phút |
|
||||
@@ -1,41 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -1 +0,0 @@
|
||||
@AGENTS.md
|
||||
@@ -1,69 +0,0 @@
|
||||
# Nguyen Ngoc Tri Vi (Thomas)
|
||||
|
||||
**Portfolio:** [nguyenngoctrivi.vercel.app](https://nguyenngoctrivi.vercel.app) | **SĐT:** 090 321 5095 | **Email:** nntrivi2001@gmail.com | **LinkedIn:** [linkedin.com/in/nguyen-ngoc-tri-vi](https://linkedin.com/in/nguyen-ngoc-tri-vi)
|
||||
|
||||
---
|
||||
|
||||
## Kinh nghiệm làm việc
|
||||
|
||||
### SmartGift Solution
|
||||
**Kỹ sư AI** | Tháng 03/2026 - Hiện tại
|
||||
|
||||
* Nghiên cứu và triển khai **AI hỗ trợ** nhằm cải thiện hiệu suất của đội ngũ phát triển.
|
||||
* Khám phá và áp dụng các công nghệ phù hợp cho các bài toán kinh doanh, xây dựng các bản **thử nghiệm (POC)** và tài liệu hóa các đề xuất thực tế.
|
||||
|
||||
### Learning Chain Vietnam
|
||||
**Nghiên cứu viên AI Ứng dụng & Cộng tác viên Vận hành** | Tháng 07/2025 - Tháng 03/2026
|
||||
|
||||
* Nghiên cứu và triển khai **tự động hóa giao dịch** và vận hành có hỗ trợ AI.
|
||||
* Khám phá và thực thi công nghệ phù hợp, xây dựng POC và tài liệu hóa các khuyến nghị thực tế.
|
||||
* Thiết kế và giảng dạy chương trình đào tạo **AI ứng dụng**, các buổi hội thảo và tài sản học tập (mẫu n8n).
|
||||
* Hỗ trợ Giám đốc vận hành (COO) trong việc **tối ưu hóa quy trình làm việc**, báo cáo và tiêu chuẩn hóa quy trình liên nhóm.
|
||||
|
||||
### Vườn đậu - Thương hiệu Sữa đậu nành Thảo mộc
|
||||
**Người sáng lập (Founder)** | Tháng 10/2024 - Tháng 04/2025
|
||||
|
||||
* Quản lý toàn bộ quy trình sản xuất và kinh doanh, bao gồm nguồn cung, sản xuất, đóng gói, phân phối và **kiểm soát hàng tồn kho**.
|
||||
* Lập kế hoạch thu mua nguyên liệu, lịch trình sản xuất và lưu kho để **tối ưu hóa chi phí** và duy trì chất lượng sản phẩm ổn định.
|
||||
* Phát triển chiến lược giá và triển khai các kênh bán hàng trực tiếp. Đạt được phản hồi tích cực từ khách hàng với tỷ lệ mua lại trung bình **2.51 lần mỗi khách hàng**.
|
||||
|
||||
### VSTECH Company Limited
|
||||
**Kỹ sư Thị giác Máy tính (Computer Vision Engineer)** | Tháng 08/2023 - Tháng 02/2025
|
||||
|
||||
* **Kiểm tra chất lượng vòi phun (Nozzle Quality Inspection):** Xây dựng cho khách hàng Nhật Bản để triển khai trên dây chuyền sản xuất nhà máy.
|
||||
* **Tích hợp hệ thống thị giác 3D:** Đo chiều cao tấm nền LED Samsung với độ chính xác đạt +-7%.
|
||||
* **Giải pháp OCR:** Đọc mã gói hàng và nhãn văn bản với độ chính xác cao trong môi trường công nghiệp.
|
||||
|
||||
---
|
||||
|
||||
## Học vấn
|
||||
|
||||
### Đại học Công Thương TP.HCM
|
||||
**Kỹ sư Phân tích Dữ liệu & AI** | Tháng 10/2020 - Tháng 10/2024
|
||||
|
||||
* Tốt nghiệp **Thủ khoa** ngành Công nghệ Thông tin.
|
||||
* **GPA:** 3.74/4.0.
|
||||
* **Giải thưởng & Học bổng:**
|
||||
* Giải Nhì - Cuộc thi Sản phẩm và Chủ đề IT Thiết yếu (2024).
|
||||
* Giải Khuyến khích - TOFAS (2023).
|
||||
* Giải Mã nguồn sạch (Clean Code) & Giải Khuyến khích - Tìm kiếm Tài năng & Sản phẩm IT Sáng tạo (2022).
|
||||
* Nhiều học bổng Sinh viên Xuất sắc & Giỏi (2020-2024).
|
||||
|
||||
---
|
||||
|
||||
## Kỹ năng
|
||||
|
||||
| Danh mục | Chi tiết |
|
||||
| :--- | :--- |
|
||||
| **Lập trình & Scripting** | Python, C++, HTML/CSS, JavaScript, PineScript, n8n, Telegram bots |
|
||||
| **Framework AI/ML** | TensorFlow, PyTorch, Scikit-learn, OpenCV |
|
||||
| **Dữ liệu & Đám mây** | SQL Server, Supabase, Firebase, MongoDB |
|
||||
| **Quản lý & Vận hành** | Tối ưu hóa quy trình, phối hợp liên nhóm, tài liệu hóa |
|
||||
| **Ngôn ngữ** | Tiếng Anh (Đọc chuyên ngành, giao tiếp trung cấp), Tiếng Việt (Bản ngữ) |
|
||||
|
||||
---
|
||||
|
||||
## Portfolio & Chứng chỉ
|
||||
|
||||
* Các dự án khác và bản demo GIF có sẵn trên trang **Portfolio**.
|
||||
* Các chứng chỉ chuyên môn được liệt kê chi tiết trên **LinkedIn**.
|
||||
@@ -1,36 +0,0 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -1,18 +0,0 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
turbopack: {
|
||||
root: process.cwd(),
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**',
|
||||
},
|
||||
],
|
||||
// Allow local file system images via next/image
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "my-portfolio",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.38.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"next": "16.2.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "20.19.37",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
|
||||
<defs>
|
||||
<linearGradient id="avatarGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#DC2626;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#F59E0B;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="100" cy="100" r="100" fill="url(#avatarGradient)" />
|
||||
<circle cx="100" cy="100" r="92" fill="white" fill-opacity="0.1" />
|
||||
<text
|
||||
x="100"
|
||||
y="115"
|
||||
font-family="'Segoe UI', Arial, sans-serif"
|
||||
font-size="80"
|
||||
font-weight="bold"
|
||||
fill="white"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
style="text-shadow: 2px 2px 4px rgba(0,0,0,0.3);">
|
||||
TV
|
||||
</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 756 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 391 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@@ -1 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
Before Width: | Height: | Size: 385 B |
@@ -1,174 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { GraduationCapIcon, AwardIcon, LanguagesIcon, MapPinIcon, MailIcon, PhoneIcon } from '@/components/icons/TetIcons';
|
||||
import ScrollReveal from '@/components/animations/ScrollReveal';
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4 sm:px-6 lg:px-8 bg-[var(--tet-cream)]">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<ScrollReveal variant="fadeInUp">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl font-bold text-[var(--tet-red)] mb-4">About Me</h1>
|
||||
<div className="h-1 w-32 bg-[var(--tet-gold)] mx-auto rounded-full" />
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-12 items-start">
|
||||
{/* Avatar Section */}
|
||||
<ScrollReveal variant="fadeInUp" delay={0.1}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative w-80 h-80 rounded-full overflow-hidden border-8 border-[var(--tet-gold)] shadow-2xl mb-6 bg-gradient-to-br from-[var(--tet-red)] to-[var(--tet-gold)] flex items-center justify-center">
|
||||
<span className="text-6xl font-bold text-white">TV</span>
|
||||
</div>
|
||||
|
||||
{/* Quick contact */}
|
||||
<div className="space-y-3 w-full max-w-md">
|
||||
<div className="flex items-center space-x-3 text-gray-700">
|
||||
<MapPinIcon className="w-5 h-5 text-[var(--tet-red)]" />
|
||||
<span>Ho Chi Minh City, Vietnam</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 text-gray-700">
|
||||
<MailIcon className="w-5 h-5 text-[var(--tet-red)]" />
|
||||
<a href="mailto:nntrivi2001@gmail.com" className="hover:text-[var(--tet-red)] transition">
|
||||
nntrivi2001@gmail.com
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 text-gray-700">
|
||||
<PhoneIcon className="w-5 h-5 text-[var(--tet-red)]" />
|
||||
<span>090 321 5095</span>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Bio Content */}
|
||||
<ScrollReveal variant="fadeInUp" delay={0.2}>
|
||||
<div className="space-y-8">
|
||||
{/* Introduction */}
|
||||
<section className="bg-white p-8 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-[var(--tet-red)] mb-4">Who I Am</h2>
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
I'm <strong>Nguyễn Ngọc Trí Vĩ</strong> (Thomas), an AI Engineer with a passion for building intelligent systems
|
||||
that solve real-world problems. I graduated as <span className="text-[var(--tet-gold)] font-semibold">Valedictorian</span> with a GPA of 3.74/4.0
|
||||
from Ho Chi Minh City University of Industry and Trade, majoring in Data Analytics & AI.
|
||||
</p>
|
||||
<p className="text-gray-700 leading-relaxed">
|
||||
My journey spans from computer vision in manufacturing to applied AI research and workflow automation.
|
||||
I believe in the power of AI to transform businesses and improve lives, and I'm committed to pushing
|
||||
the boundaries of what's possible with technology.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Education */}
|
||||
<section className="bg-white p-8 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<GraduationCapIcon className="w-8 h-8 text-[var(--tet-red)]" />
|
||||
<h2 className="text-2xl font-bold text-[var(--tet-red)]">Education</h2>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-[var(--tet-gold)] pl-6 py-2">
|
||||
<h3 className="text-xl font-semibold text-gray-800">
|
||||
Engineer in Data Analytics & AI
|
||||
</h3>
|
||||
<p className="text-[var(--tet-red)] font-medium">Ho Chi Minh City University of Industry and Trade</p>
|
||||
<p className="text-gray-600">Oct 2020 – Oct 2024</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<span className="px-3 py-1 bg-[var(--tet-red)]/10 text-[var(--tet-red)] rounded-full text-sm font-medium">
|
||||
Valedictorian 🎓
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-[var(--tet-gold)]/20 text-amber-700 rounded-full text-sm font-medium">
|
||||
GPA: 3.74 / 4.0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Awards */}
|
||||
<div className="mt-6">
|
||||
<h4 className="font-semibold text-gray-800 mb-3">Awards & Achievements</h4>
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
"🥈 2nd Place – Essential IT Products Competition (2024)",
|
||||
"🥉 Consolation Prize – TOFAS (2023)",
|
||||
"🏆 Clean Code Award – Finding Talents & Innovative IT (2022)",
|
||||
"📜 Multiple scholarships (2020–2024)",
|
||||
].map((award, idx) => (
|
||||
<li key={idx} className="flex items-start space-x-2 text-gray-700">
|
||||
<span className="text-[var(--tet-gold)]">{award}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Skills & Languages */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<section className="bg-white p-6 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<h3 className="text-xl font-bold text-[var(--tet-red)] mb-4">Skills</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 mb-2">Programming & Tools</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Python', 'C++', 'HTML/CSS', 'JavaScript', 'PineScript', 'n8n', 'Telegram Bots'].map(skill => (
|
||||
<span key={skill} className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 mb-2">AI / ML</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['TensorFlow', 'PyTorch', 'Scikit-learn', 'OpenCV'].map(skill => (
|
||||
<span key={skill} className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 mb-2">Data & Cloud</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['SQL Server', 'Supabase', 'Firebase', 'MongoDB'].map(skill => (
|
||||
<span key={skill} className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-white p-6 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<LanguagesIcon className="w-6 h-6 text-[var(--tet-red)]" />
|
||||
<h3 className="text-xl font-bold text-[var(--tet-red)]">Languages</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="font-medium text-gray-800">Vietnamese</span>
|
||||
<span className="text-sm text-gray-600">Native</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full">
|
||||
<div className="h-2 bg-[var(--tet-red)] rounded-full" style={{ width: '100%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="font-medium text-gray-800">English</span>
|
||||
<span className="text-sm text-gray-600">Professional</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full">
|
||||
<div className="h-2 bg-[var(--tet-gold)] rounded-full" style={{ width: '85%' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { MailIcon, PhoneIcon, MapPinIcon, SendIcon, LinkedInIcon, GithubIcon, ArrowRightIcon } from '@/components/icons/TetIcons';
|
||||
import { useState } from 'react';
|
||||
import ScrollReveal from '@/components/animations/ScrollReveal';
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate form submission (in production, connect to backend)
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
setSubmitStatus('success');
|
||||
setIsSubmitting(false);
|
||||
setFormData({ name: '', email: '', subject: '', message: '' });
|
||||
|
||||
setTimeout(() => setSubmitStatus('idle'), 5000);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4 sm:px-6 lg:px-8 bg-[var(--tet-cream)]">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<ScrollReveal variant="fadeInUp">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl font-bold text-[var(--tet-red)] mb-4">Get in Touch</h1>
|
||||
<div className="h-1 w-32 bg-[var(--tet-gold)] mx-auto rounded-full mb-4" />
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
Have a project in mind or want to collaborate? I'd love to hear from you.
|
||||
Don't hesitate to reach out!
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-12">
|
||||
{/* Contact Information */}
|
||||
<ScrollReveal variant="fadeInUp" delay={0.1} className="lg:col-span-1 space-y-6">
|
||||
<div className="bg-white p-8 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-[var(--tet-red)] mb-6">Contact Info</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-[var(--tet-red)]/10 rounded-xl">
|
||||
<MailIcon className="w-6 h-6 text-[var(--tet-red)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">Email</p>
|
||||
<a
|
||||
href="mailto:nntrivi2001@gmail.com"
|
||||
className="text-gray-600 hover:text-[var(--tet-red)] transition-colors"
|
||||
>
|
||||
nntrivi2001@gmail.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-[var(--tet-red)]/10 rounded-xl">
|
||||
<PhoneIcon className="w-6 h-6 text-[var(--tet-red)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">Phone</p>
|
||||
<a
|
||||
href="tel:+84903215095"
|
||||
className="text-gray-600 hover:text-[var(--tet-red)] transition-colors"
|
||||
>
|
||||
090 321 5095
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="p-3 bg-[var(--tet-red)]/10 rounded-xl">
|
||||
<MapPinIcon className="w-6 h-6 text-[var(--tet-red)]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800">Location</p>
|
||||
<p className="text-gray-600">Ho Chi Minh City, Vietnam</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">Connect with me</h3>
|
||||
<div className="flex space-x-4">
|
||||
<a
|
||||
href="https://linkedin.com/in/nguyen-ngoc-tri-vi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-[var(--tet-red)]/10 rounded-xl hover:bg-[var(--tet-red)]/20 transition-colors group"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<LinkedInIcon className="w-6 h-6 text-[var(--tet-red)] group-hover:scale-110 transition-transform" />
|
||||
</a>
|
||||
<a
|
||||
href="https://nguyenngoctrivi.vercel.app/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-3 bg-[var(--tet-gold)]/20 rounded-xl hover:bg-[var(--tet-gold)]/30 transition-colors group"
|
||||
aria-label="Portfolio"
|
||||
>
|
||||
<ArrowRightIcon className="w-6 h-6 text-amber-600 group-hover:scale-110 transition-transform" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decorative element */}
|
||||
<div className="text-center text-6xl hidden lg:block">🧧</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Contact Form */}
|
||||
<ScrollReveal variant="fadeInUp" delay={0.2} className="lg:col-span-2">
|
||||
<div className="bg-white p-8 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-[var(--tet-red)] mb-6">Send a Message</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Your Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[var(--tet-red)] focus:outline-none transition-colors"
|
||||
placeholder="Nguyễn Văn A"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[var(--tet-red)] focus:outline-none transition-colors"
|
||||
placeholder="example@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[var(--tet-red)] focus:outline-none transition-colors"
|
||||
placeholder="What's this about?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Message *
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows={6}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border-2 border-gray-200 focus:border-[var(--tet-red)] focus:outline-none transition-colors resize-none"
|
||||
placeholder="Tell me about your project or opportunity..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
* Required fields
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-[var(--tet-red)] text-white rounded-full font-semibold hover:bg-[var(--tet-red-dark)] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
'Sending...'
|
||||
) : (
|
||||
<>
|
||||
Send Message
|
||||
<SendIcon className="w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Success/Error messages */}
|
||||
{submitStatus === 'success' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-green-50 border-2 border-green-200 rounded-xl text-green-800"
|
||||
>
|
||||
✅ Your message has been sent successfully! I'll get back to you soon.
|
||||
</motion.div>
|
||||
)}
|
||||
{submitStatus === 'error' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="p-4 bg-red-50 border-2 border-red-200 rounded-xl text-red-800"
|
||||
>
|
||||
❌ Something went wrong. Please try again or email me directly.
|
||||
</motion.div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { BriefcaseIcon, CalendarIcon, MapPinIcon } from '@/components/icons/TetIcons';
|
||||
import ScrollReveal from '@/components/animations/ScrollReveal';
|
||||
|
||||
const experiences = [
|
||||
{
|
||||
company: 'SmartGift Solution',
|
||||
role: 'AI Engineer',
|
||||
period: 'Mar 2026 – Present',
|
||||
location: 'Ho Chi Minh City, Vietnam',
|
||||
description: [
|
||||
'Researched and implemented AI-assisted solutions to improve developer team performance',
|
||||
'Explored technologies for business use cases, built POCs, and documented recommendations',
|
||||
],
|
||||
color: 'tet-red',
|
||||
},
|
||||
{
|
||||
company: 'Learning Chain Vietnam',
|
||||
role: 'Applied AI Researcher & Operations Associate',
|
||||
period: 'Jul 2025 – Mar 2026',
|
||||
location: 'Ho Chi Minh City, Vietnam',
|
||||
description: [
|
||||
'Built AI-assisted trading and operations automation systems',
|
||||
'Developed POCs and recommended technologies for business use',
|
||||
'Designed applied AI curricula, workshops, and n8n learning assets',
|
||||
'Supported COO in workflow optimization and process standardization',
|
||||
],
|
||||
color: 'tet-gold',
|
||||
},
|
||||
{
|
||||
company: 'Vườn Đậu – Herbal Soy Milk Brand',
|
||||
role: 'Founder',
|
||||
period: 'Oct 2024 – Apr 2025',
|
||||
location: 'Ho Chi Minh City, Vietnam',
|
||||
description: [
|
||||
'Managed full production and business operations',
|
||||
'Optimized sourcing, production, and inventory systems',
|
||||
'Built pricing strategy and launched direct sales channels',
|
||||
'Achieved strong customer retention (2.51 repeat purchases/customer)',
|
||||
],
|
||||
color: 'tet-red',
|
||||
},
|
||||
{
|
||||
company: 'VSTECH Company Limited',
|
||||
role: 'Computer Vision Engineer',
|
||||
period: 'Aug 2023 – Feb 2025',
|
||||
location: 'Ho Chi Minh City, Vietnam',
|
||||
description: [
|
||||
'Developed nozzle quality inspection system for Japanese factory',
|
||||
'Integrated 3D vision system for Samsung LED measurement (±7% accuracy)',
|
||||
'Built OCR system for package code recognition in industrial environments',
|
||||
],
|
||||
color: 'tet-gold',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ExperiencePage() {
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4 sm:px-6 lg:px-8 bg-[var(--tet-cream)]">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<ScrollReveal variant="fadeInUp">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl font-bold text-[var(--tet-red)] mb-4">Experience</h1>
|
||||
<div className="h-1 w-32 bg-[var(--tet-gold)] mx-auto rounded-full" />
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="relative">
|
||||
{/* Central line */}
|
||||
<div className="absolute left-8 md:left-1/2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-[var(--tet-red)] to-[var(--tet-gold)] transform md:-translate-x-1/2" />
|
||||
|
||||
{experiences.map((exp, index) => {
|
||||
const isEven = index % 2 === 0;
|
||||
const Icon = BriefcaseIcon;
|
||||
|
||||
return (
|
||||
<ScrollReveal key={index} variant={isEven ? "slideInLeft" : "slideInRight"} delay={index * 0.15}>
|
||||
<div className={`relative flex items-center mb-16 ${isEven ? 'md:flex-row' : 'md:flex-row-reverse'}`}>
|
||||
{/* Content Card */}
|
||||
<div className={`ml-20 md:ml-0 md:w-5/12 ${isEven ? 'md:pr-12 md:text-right' : 'md:pl-12'} z-10`}>
|
||||
<div className="bg-white p-6 rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg hover:shadow-xl transition-shadow duration-300 card-hover">
|
||||
<div className="flex items-center gap-2 mb-2 justify-center md:justify-start">
|
||||
<CalendarIcon className="w-4 h-4 text-[var(--tet-red)]" />
|
||||
<span className="text-sm font-medium text-[var(--tet-red)]">{exp.period}</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-1">{exp.role}</h3>
|
||||
<div className="flex items-center gap-2 mb-3 justify-center md:justify-start">
|
||||
<span className="text-lg font-semibold text-[var(--tet-red)]">{exp.company}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4 text-gray-600 text-sm justify-center md:justify-start">
|
||||
<MapPinIcon className="w-4 h-4" />
|
||||
<span>{exp.location}</span>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2 mb-4">
|
||||
{exp.description.map((item, idx) => (
|
||||
<li key={idx} className={`text-gray-700 ${isEven ? 'md:text-right' : 'md:text-left'}`}>
|
||||
• {item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex justify-center md:justify-start">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium bg-${exp.color === 'tet-red' ? '[var(--tet-red)]/10' : '[var(--tet-gold)]/20'} text-${exp.color === 'tet-red' ? '[var(--tet-red)]' : 'amber-700'}`}>
|
||||
{exp.color === 'tet-red' ? '🔴' : '🟡'} {exp.period.split(' – ')[0]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline dot */}
|
||||
<div className="absolute left-8 md:left-1/2 transform -translate-x-1/2">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.3 }}
|
||||
className="w-8 h-8 rounded-full bg-[var(--tet-gold)] border-4 border-[var(--tet-cream)] shadow-lg flex items-center justify-center z-20"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-[var(--tet-dark)]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Empty spacer for opposite side */}
|
||||
<div className="hidden md:block md:w-5/12" />
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,66 +0,0 @@
|
||||
:root {
|
||||
/* Tet Theme Colors */
|
||||
--tet-red: #C41E3A;
|
||||
--tet-red-dark: #8B0000;
|
||||
--tet-gold: #FFD700;
|
||||
--tet-gold-light: #FFC107;
|
||||
--tet-cream: #FFF8E7;
|
||||
--tet-dark: #1A1A1A;
|
||||
--tet-red-gradient: linear-gradient(135deg, #C41E3A 0%, #8B0000 100%);
|
||||
--tet-gold-gradient: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
|
||||
--background: var(--tet-cream);
|
||||
--foreground: var(--tet-dark);
|
||||
}
|
||||
|
||||
/* Custom utility classes */
|
||||
.bg-tet-red { background-color: var(--tet-red); }
|
||||
.bg-tet-gold { background-color: var(--tet-gold); }
|
||||
.text-tet-red { color: var(--tet-red); }
|
||||
.text-tet-gold { color: var(--tet-gold); }
|
||||
.border-tet-red { border-color: var(--tet-red); }
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Custom scrollbar with Tet colors */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--tet-cream);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--tet-red);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--tet-red-dark);
|
||||
}
|
||||
|
||||
/* Golden glow effect */
|
||||
.text-glow {
|
||||
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Red card hover effect */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(196, 30, 58, 0.2);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Navigation from "@/components/layout/Navigation";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import Script from "next/script";
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import FallingPetals from '@/components/animations/FallingPetals';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Trí Vĩ - AI Engineer Portfolio",
|
||||
description: "Portfolio of Nguyễn Ngọc Trí Vĩ - AI Engineer specializing in Computer Vision, Machine Learning, and Applied AI solutions",
|
||||
keywords: ["AI Engineer", "Machine Learning", "Computer Vision", "Portfolio", "Vietnam"],
|
||||
authors: [{ name: "Nguyễn Ngọc Trí Vĩ" }],
|
||||
openGraph: {
|
||||
title: "Trí Vĩ - AI Engineer",
|
||||
description: "Portfolio of an AI Engineer building intelligent systems",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
// Page transition variants
|
||||
const pageVariants = {
|
||||
hidden: { opacity: 0, x: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: {
|
||||
duration: 0.4,
|
||||
ease: [0.25, 0.1, 0.25, 1]
|
||||
}
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.1, 0.25, 1]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const pathname = usePathname();
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col">
|
||||
{/* Tailwind CSS CDN */}
|
||||
<Script
|
||||
src="https://cdn.tailwindcss.com"
|
||||
strategy="beforeInteractive"
|
||||
/>
|
||||
|
||||
<Navigation />
|
||||
<div className="flex-1 pt-16 relative"> {/* Added relative for positioning */}
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
{isClient && (
|
||||
<motion.div
|
||||
key={pathname}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
variants={pageVariants}
|
||||
className="min-h-screen"
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* Falling petals overlay - appears on all pages */}
|
||||
<FallingPetals count={40} />
|
||||
</div>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowRightIcon, SparklesIcon, Code2Icon, BrainIcon } from '@/components/icons/TetIcons';
|
||||
import Link from 'next/link';
|
||||
import ScrollReveal from '@/components/animations/ScrollReveal';
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.2,
|
||||
delayChildren: 0.3,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6 },
|
||||
},
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main className="relative min-h-screen flex items-center justify-center overflow-hidden">
|
||||
{/* Decorative border patterns */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
{/* Top border - traditional cloud pattern */}
|
||||
<div className="absolute top-0 left-0 right-0 h-4 bg-gradient-to-r from-[var(--tet-red)] via-[var(--tet-gold)] to-[var(--tet-red)]" />
|
||||
|
||||
{/* Corner decorations */}
|
||||
<div className="absolute top-4 left-4 w-16 h-16 border-t-4 border-l-4 border-[var(--tet-red)]" />
|
||||
<div className="absolute top-4 right-4 w-16 h-16 border-t-4 border-r-4 border-[var(--tet-red)]" />
|
||||
<div className="absolute bottom-4 left-4 w-16 h-16 border-b-4 border-l-4 border-[var(--tet-red)]" />
|
||||
<div className="absolute bottom-4 right-4 w-16 h-16 border-b-4 border-r-4 border-[var(--tet-red)]" />
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
className="relative z-10 max-w-6xl mx-auto px-6 py-24 text-center"
|
||||
>
|
||||
{/* Greeting in Traditional Vietnamese style */}
|
||||
<motion.div variants={itemVariants} className="mb-6">
|
||||
<span className="inline-block px-6 py-2 bg-[var(--tet-red)] text-white rounded-full text-sm font-semibold tracking-wider uppercase">
|
||||
🎊 Chúc Mừng Năm Mới 🎊
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Test agent completion badge */}
|
||||
<motion.div variants={itemVariants} className="mb-4">
|
||||
<span className="inline-flex items-center gap-2 px-4 py-1.5 bg-green-100 text-green-800 rounded-full text-xs font-medium border border-green-200">
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Test Agent Task Completed
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Name with golden glow */}
|
||||
<motion.h1
|
||||
variants={itemVariants}
|
||||
className="text-5xl md:text-8xl font-bold mb-4 bg-[var(--tet-gold-gradient)] bg-clip-text text-transparent text-glow"
|
||||
>
|
||||
Nguyễn Ngọc Trí Vĩ
|
||||
</motion.h1>
|
||||
|
||||
{/* Role with red accent */}
|
||||
<motion.h2
|
||||
variants={itemVariants}
|
||||
className="text-2xl md:text-4xl font-semibold mb-8 text-[var(--tet-red)]"
|
||||
>
|
||||
AI Engineer
|
||||
</motion.h2>
|
||||
|
||||
{/* Bio */}
|
||||
<motion.p
|
||||
variants={itemVariants}
|
||||
className="text-lg md:text-xl max-w-3xl mx-auto mb-12 leading-relaxed text-gray-700"
|
||||
>
|
||||
Passionate about building intelligent systems that bridge the gap between AI and real-world business problems.
|
||||
Currently researching AI-assisted solutions at <span className="font-bold text-[var(--tet-red)]">SmartGift Solution</span>.
|
||||
</motion.p>
|
||||
|
||||
{/* Quick stats */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-12 max-w-4xl mx-auto"
|
||||
>
|
||||
{[
|
||||
{ icon: BrainIcon, label: 'AI/ML', value: 'Expert' },
|
||||
{ icon: Code2Icon, label: 'Experience', value: '3+ Years' },
|
||||
{ icon: SparklesIcon, label: 'Projects', value: '20+' },
|
||||
{ icon: () => (
|
||||
<span className="text-2xl">🎓</span>
|
||||
), label: 'Education', value: 'Valedictorian' },
|
||||
].map((stat, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="flex flex-col items-center p-6 bg-white/80 backdrop-blur-sm rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg card-hover"
|
||||
>
|
||||
<stat.icon className="w-8 h-8 text-[var(--tet-red)] mb-2" />
|
||||
<span className="text-3xl font-bold text-[var(--tet-gold)]">{stat.value}</span>
|
||||
<span className="text-sm text-gray-600 mt-1">{stat.label}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* CTA buttons */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
||||
>
|
||||
<Link
|
||||
href="/about"
|
||||
className="group inline-flex items-center justify-center px-8 py-4 bg-[var(--tet-red)] text-white rounded-full font-semibold text-lg hover:bg-[var(--tet-red-dark)] transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105"
|
||||
>
|
||||
Learn More
|
||||
<ArrowRightIcon className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/projects"
|
||||
className="inline-flex items-center justify-center px-8 py-4 border-2 border-[var(--tet-red)] text-[var(--tet-red)] rounded-full font-semibold text-lg hover:bg-[var(--tet-red)] hover:text-white transition-all duration-300"
|
||||
>
|
||||
View Projects
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center justify-center px-8 py-4 bg-[var(--tet-gold)] text-[var(--tet-dark)] rounded-full font-semibold text-lg hover:bg-[var(--tet-gold-light)] transition-all duration-300"
|
||||
>
|
||||
Get in Touch
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating lantern decoration */}
|
||||
<motion.div
|
||||
animate={{ y: [0, -20, 0] }}
|
||||
transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}
|
||||
className="absolute bottom-10 left-10 text-4xl hidden lg:block"
|
||||
>
|
||||
🏮
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
animate={{ y: [0, -15, 0] }}
|
||||
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut', delay: 0.5 }}
|
||||
className="absolute bottom-10 right-10 text-4xl hidden lg:block"
|
||||
>
|
||||
🧧
|
||||
</motion.div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { ExternalLinkIcon, GithubIcon, FolderOpenIcon, TagIcon } from '@/components/icons/TetIcons';
|
||||
import ScrollReveal from '@/components/animations/ScrollReveal';
|
||||
|
||||
const projects = [
|
||||
return (
|
||||
<main className="min-h-screen py-20 px-4 sm:px-6 lg:px-8 bg-[var(--tet-cream)]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Page Header */}
|
||||
<ScrollReveal variant="fadeInUp">
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-5xl font-bold text-[var(--tet-red)] mb-4">Projects</h1>
|
||||
<div className="h-1 w-32 bg-[var(--tet-gold)] mx-auto rounded-full mb-4" />
|
||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
A showcase of AI, computer vision, and automation projects that demonstrate my expertise in applied AI and full-stack development.
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{projects.map((project, index) => (
|
||||
<ScrollReveal key={project.id} variant="fadeInUp" delay={index * 0.1}>
|
||||
<motion.div
|
||||
whileHover={{ y: -8 }}
|
||||
className="bg-white rounded-2xl border-2 border-[var(--tet-red)]/20 shadow-lg overflow-hidden card-hover group"
|
||||
>
|
||||
{/* Card Header with decorative pattern */}
|
||||
<div className={`h-3 bg-gradient-to-r from-${project.color === 'tet-red' ? '[var(--tet-red)]' : '[var(--tet-gold)]'} to-${project.color === 'tet-red' ? '[var(--tet-red-dark)]' : '[#FFA500]'}`} />
|
||||
|
||||
<div className="p-6">
|
||||
{/* Icon and year */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className={`p-3 rounded-xl bg-${project.color}/10`}>
|
||||
<FolderOpenIcon className={`w-8 h-8 text-${project.color === 'tet-red' ? '[var(--tet-red)]' : '[var(--tet-gold)]'}`} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-500">{project.year}</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-3 line-clamp-2 min-h-[3.5rem]">
|
||||
{project.title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-600 mb-6 text-sm leading-relaxed line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{project.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-xs"
|
||||
>
|
||||
<TagIcon className="w-3 h-3" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={project.link}
|
||||
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-[var(--tet-red)] text-white rounded-lg text-sm font-medium hover:bg-[var(--tet-red-dark)] transition-colors"
|
||||
>
|
||||
<ExternalLinkIcon className="w-4 h-4" />
|
||||
View
|
||||
</a>
|
||||
<a
|
||||
href={project.github}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<GithubIcon className="w-4 h-4" />
|
||||
Code
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* More projects placeholder */}
|
||||
<ScrollReveal variant="fadeInUp" delay={0.6}>
|
||||
<div className="mt-16 text-center">
|
||||
<p className="text-gray-600 mb-4">
|
||||
Want to see more of my work or discuss a collaboration?
|
||||
</p>
|
||||
<a
|
||||
href="/contact"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-[var(--tet-gold)] text-[var(--tet-dark)] rounded-full font-semibold hover:bg-[var(--tet-gold-light)] transition-all duration-300"
|
||||
>
|
||||
Get in Touch
|
||||
</a>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
interface Petal {
|
||||
id: number;
|
||||
x: number;
|
||||
size: number;
|
||||
rotation: number;
|
||||
duration: number;
|
||||
delay: number;
|
||||
type: 'cherry' | 'plum';
|
||||
}
|
||||
|
||||
interface FallingPetalsProps {
|
||||
count?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export default function FallingPetals({ count = 40, enabled = true }: FallingPetalsProps) {
|
||||
const [petals, setPetals] = useState<Petal[]>([]);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
// Generate initial petals
|
||||
const initialPetals: Petal[] = Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
x: Math.random() * 100,
|
||||
size: 8 + Math.random() * 16,
|
||||
rotation: Math.random() * 360,
|
||||
duration: 8 + Math.random() * 12,
|
||||
delay: Math.random() * 5,
|
||||
type: Math.random() > 0.5 ? 'cherry' : 'plum',
|
||||
}));
|
||||
|
||||
setPetals(initialPetals);
|
||||
|
||||
// Recycle petals: regenerate when they reach bottom
|
||||
const interval = setInterval(() => {
|
||||
setPetals(prev => prev.map(petal => ({
|
||||
...petal,
|
||||
id: petal.id + count, // new id to trigger animation
|
||||
x: Math.random() * 100,
|
||||
rotation: Math.random() * 360,
|
||||
delay: 0,
|
||||
})));
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [count, enabled]);
|
||||
|
||||
const getPetalColor = (type: 'cherry' | 'plum') => {
|
||||
return type === 'cherry'
|
||||
? ['#FFB7C5', '#FF69B4', '#FF1493', '#DB7093'] // Cherry blossom pink
|
||||
: ['#FFD700', '#FFA500', '#FF8C00', '#FFCC00']; // Plum blossom gold
|
||||
};
|
||||
|
||||
const getPetalSVG = (type: 'cherry' | 'plum', color: string) => {
|
||||
// Simple petal shape SVG
|
||||
return (
|
||||
<svg width="100%" height="100%" viewBox="0 0 50 50">
|
||||
<path
|
||||
d="M25,5 C20,15 10,20 10,30 C10,40 25,45 25,45 C25,45 40,40 40,30 C40,20 30,15 25,5 Z"
|
||||
fill={color}
|
||||
opacity="0.8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="fixed inset-0 pointer-events-none z-0 overflow-hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{petals.map((petal) => {
|
||||
const colors = getPetalColor(petal.type);
|
||||
const color = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={petal.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: `${petal.x}%`,
|
||||
width: petal.size,
|
||||
height: petal.size,
|
||||
}}
|
||||
initial={{
|
||||
y: -50,
|
||||
rotate: petal.rotation,
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
y: '110vh',
|
||||
rotate: petal.rotation + 360,
|
||||
opacity: [0, 0.9, 0.9, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: petal.duration,
|
||||
delay: petal.delay,
|
||||
ease: 'linear',
|
||||
opacity: {
|
||||
duration: petal.duration,
|
||||
times: [0, 0.1, 0.9, 1],
|
||||
},
|
||||
}}
|
||||
>
|
||||
{getPetalSVG(petal.type, color)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { motion, useInView, Variant } from 'framer-motion';
|
||||
|
||||
interface ScrollRevealProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
once?: boolean;
|
||||
offset?: number;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
const fadeInUp = {
|
||||
hidden: { opacity: 0, y: 60 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }
|
||||
},
|
||||
};
|
||||
|
||||
const fadeIn = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.6 }
|
||||
},
|
||||
};
|
||||
|
||||
const slideInLeft = {
|
||||
hidden: { opacity: 0, x: -60 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }
|
||||
},
|
||||
};
|
||||
|
||||
const slideInRight = {
|
||||
hidden: { opacity: 0, x: 60 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }
|
||||
},
|
||||
};
|
||||
|
||||
const scaleIn = {
|
||||
hidden: { opacity: 0, scale: 0.9 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.5 }
|
||||
},
|
||||
};
|
||||
|
||||
const variants = {
|
||||
fadeInUp,
|
||||
fadeIn,
|
||||
slideInLeft,
|
||||
slideInRight,
|
||||
scaleIn,
|
||||
};
|
||||
|
||||
export default function ScrollReveal({
|
||||
children,
|
||||
className = '',
|
||||
delay = 0,
|
||||
duration,
|
||||
once = true,
|
||||
offset = 0,
|
||||
amount = 0.3,
|
||||
variant = 'fadeInUp',
|
||||
}: ScrollRevealProps & { variant?: keyof typeof variants }) {
|
||||
const ref = React.useRef(null);
|
||||
const isInView = useInView(ref, {
|
||||
once,
|
||||
amount,
|
||||
margin: `0px ${offset}px 0px ${offset}px`,
|
||||
});
|
||||
|
||||
const selectedVariant = variants[variant];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
className={className}
|
||||
initial="hidden"
|
||||
animate={isInView ? 'visible' : 'hidden'}
|
||||
variants={selectedVariant}
|
||||
transition={{
|
||||
delay,
|
||||
...(duration && { duration }),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface IconProps {
|
||||
className?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const GithubIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 4-3 4-3.5" />
|
||||
<path d="M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" />
|
||||
<circle cx="12" cy="11" r="3" />
|
||||
<path d="M12 16v5" />
|
||||
<path d="M8 16h8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LinkedInIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6Z" />
|
||||
<rect width="4" height="12" x="2" y="9" />
|
||||
<circle cx="4" cy="4" r="2" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MailIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const HeartIcon: React.FC<IconProps> = ({ className = "w-4 h-4", size = 20 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ExternalLinkIcon: React.FC<IconProps> = ({ className = "w-4 h-4", size = 20 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" x2="21" y1="14" y2="3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FolderOpenIcon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 28 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6" />
|
||||
<path d="M4 5v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2Z" />
|
||||
<path d="M4 10h16" />
|
||||
<path d="M4 18h16" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const TagIcon: React.FC<IconProps> = ({ className = "w-3 h-3", size = 16 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M12 2H2l2.5 7.5L7 14l-2.5 2L2 22h20l-2.5-6.5L17 14l-2.5-2-2.5 2.5L22 2H12z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PhoneIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MapPinIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SendIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="m22 2-7 20-4-9-9-4Z" />
|
||||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ArrowRightIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="m12 5 7 7-7 7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const GraduationCapIcon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 28 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z" />
|
||||
<path d="M22 10v8" />
|
||||
<path d="M2 18.08V10" />
|
||||
<path d="M12 22v-9" />
|
||||
<path d="M12 13V3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AwardIcon: React.FC<IconProps> = ({ className = "w-6 h-6", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<circle cx="12" cy="8" r="6" />
|
||||
<path d="M15.4 8.6a2 2 0 1 0-2.8-2.8" />
|
||||
<path d="M8.6 8.6a2 2 0 1 0-2.8 2.8" />
|
||||
<path d="M12 18v2" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M6 12H2" />
|
||||
<path d="M22 12h-4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LanguagesIcon: React.FC<IconProps> = ({ className = "w-6 h-6", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M4 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M8 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M12 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M16 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M20 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M7 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M11 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M4 15a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M8 15a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M12 15a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M16 15a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
<path d="M20 15a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BriefcaseIcon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect width="20" height="14" x="2" y="7" rx="2" ry="2" />
|
||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CalendarIcon: React.FC<IconProps> = ({ className = "w-4 h-4", size = 20 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
||||
<line x1="16" x2="16" y1="2" y2="6" />
|
||||
<line x1="8" x2="8" y1="2" y2="6" />
|
||||
<line x1="3" x2="21" y1="10" y2="10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const HomeIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UserIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const FolderIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.55 6" />
|
||||
<path d="M4 5v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2Z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MailSendIcon: React.FC<IconProps> = ({ className = "w-5 h-5", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const BrainIcon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 32 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15A2.5 2.5 0 0 1 9.5 22A2.5 2.5 0 0 1 7 19.5v-15A2.5 2.5 0 0 1 9.5 2Z" />
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V7a2.5 2.5 0 0 0-2.5-2.5Z" />
|
||||
<path d="M9 8h6" />
|
||||
<path d="M9 12h6" />
|
||||
<path d="M9 16h4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Code2Icon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<polyline points="16 18 22 12 16 6" />
|
||||
<polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SparklesIcon: React.FC<IconProps> = ({ className = "w-8 h-8", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
|
||||
<path d="M5 3v4" />
|
||||
<path d="M19 17v4" />
|
||||
<path d="M3 5h4" />
|
||||
<path d="M17 19h4" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const AwardStarIcon: React.FC<IconProps> = ({ className = "text-2xl", size = 24 }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
</svg>
|
||||
);
|
||||
@@ -1,57 +0,0 @@
|
||||
import { GithubIcon, LinkedInIcon, MailIcon, HeartIcon } from '@/components/icons/TetIcons';
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const socialLinks = [
|
||||
{ href: 'https://github.com', icon: GithubIcon, label: 'GitHub' },
|
||||
{ href: 'https://linkedin.com/in/nguyen-ngoc-tri-vi', icon: LinkedInIcon, label: 'LinkedIn' },
|
||||
{ href: 'mailto:nntrivi2001@gmail.com', icon: MailIcon, label: 'Email' },
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-[var(--tet-red)] text-white py-12 mt-auto">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||
{/* Logo and tagline */}
|
||||
<div className="text-center md:text-left">
|
||||
<h3 className="text-2xl font-bold mb-2 text-[var(--tet-gold)]">
|
||||
Trí Vĩ
|
||||
</h3>
|
||||
<p className="text-sm opacity-90">
|
||||
AI Engineer & Creative Developer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Social links */}
|
||||
<div className="flex space-x-4">
|
||||
{socialLinks.map((social) => {
|
||||
const Icon = social.icon;
|
||||
return (
|
||||
<a
|
||||
key={social.label}
|
||||
href={social.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 bg-white/10 rounded-full hover:bg-white/20 transition-all duration-300 hover:scale-110"
|
||||
aria-label={social.label}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div className="mt-8 pt-8 border-t border-white/20 text-center">
|
||||
<p className="text-sm opacity-80 flex items-center justify-center gap-2">
|
||||
© {currentYear} Nguyễn Ngọc Trí Vĩ. Made with{' '}
|
||||
<HeartIcon className="w-4 h-4 fill-[var(--tet-gold)] text-[var(--tet-gold)]" />
|
||||
and Vietnamese Tet spirit
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { motion } from 'framer-motion';
|
||||
import { HomeIcon, UserIcon, BriefcaseIcon, FolderOpenIcon, MailIcon } from '@/components/icons/TetIcons';
|
||||
|
||||
const navItems = [
|
||||
{ href: '/', label: 'Home', icon: HomeIcon },
|
||||
{ href: '/about', label: 'About', icon: UserIcon },
|
||||
{ href: '/experience', label: 'Experience', icon: BriefcaseIcon },
|
||||
{ href: '/projects', label: 'Projects', icon: FolderOpenIcon },
|
||||
{ href: '/contact', label: 'Contact', icon: MailIcon },
|
||||
];
|
||||
|
||||
export default function Navigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 bg-[var(--tet-cream)]/95 backdrop-blur-md shadow-lg border-b-2 border-[var(--tet-red)]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<motion.div
|
||||
whileHover={{ rotate: 10 }}
|
||||
className="text-2xl font-bold bg-[var(--tet-red-gradient)] bg-clip-text text-transparent"
|
||||
>
|
||||
Trí Vĩ
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex space-x-1">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="relative px-4 py-2 rounded-lg transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Active indicator */}
|
||||
{isActive && (
|
||||
<motion.div
|
||||
layoutId="nav-indicator"
|
||||
className="absolute inset-0 bg-[var(--tet-red)]/10 rounded-lg border-2 border-[var(--tet-red)]"
|
||||
initial={false}
|
||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hover effect */}
|
||||
{!isActive && (
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-[var(--tet-gold)]/10 rounded-lg"
|
||||
initial={{ opacity: 0 }}
|
||||
whileHover={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button - simplified */}
|
||||
<div className="md:hidden">
|
||||
<div className="flex space-x-2">
|
||||
{navItems.slice(0, 3).map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="p-2 rounded-lg hover:bg-[var(--tet-red)]/10 transition"
|
||||
>
|
||||
<item.icon className="w-5 h-5 text-[var(--tet-red)]" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts",
|
||||
".next/dev/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||