feat: makingsoftware.com pixel-perfect portal transformation

This commit is contained in:
Khoa.vo 2026-04-19 10:16:32 +07:00
commit 60b2db9381
31 changed files with 4743 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
README.md Normal file
View file

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

25
SECURITY.md Normal file
View file

@ -0,0 +1,25 @@
# Security & Anti-DDoS Guide
To fully protect your Synology NAS from DDoS attacks and hide your home IP address, you must use a reverse proxy service like **Cloudflare**. Frontend code alone cannot hide your server's IP.
## Step 1: Create a Cloudflare Account
1. Go to [Cloudflare.com](https://www.cloudflare.com/) and sign up.
2. Click **Add a Site** and enter your domain (e.g., `khoavo.i234.me`).
## Step 2: Update DNS Records
1. Cloudflare will scan your existing DNS records.
2. Ensure your `A` records (pointing to your home IP) are set to **Proxied** (Orange Cloud icon).
* **Orange Cloud**: Traffic goes through Cloudflare -> Your NAS. (IP Hidden, DDoS Protected)
* **Grey Cloud**: Traffic goes directly to your NAS. (IP Exposed, No Protection)
## Step 3: Configure SSL/TLS
1. Go to the **SSL/TLS** tab in Cloudflare.
2. Set the mode to **Full (Strict)** if your NAS has a valid certificate, or **Flexible** if it doesn't.
## Step 4: Firewall Rules (Optional but Recommended)
1. Go to **Security > WAF**.
2. Create a rule to **Block** traffic from countries you don't expect visitors from.
3. Enable **Bot Fight Mode** to block automated attacks.
## Why this is necessary?
When you host a website on your NAS, your domain `khoavo.i234.me` translates directly to your home IP address. Anyone on the internet can see this IP. By using Cloudflare as a "middleman", visitors only see Cloudflare's IP, keeping your home network safe.

29
eslint.config.js Normal file
View file

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

18
index.html Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VNDANGKHOA</title>
<meta http-equiv="Referrer-Policy" content="no-referrer" />
<meta http-equiv="X-Content-Type-Options" content="nosniff" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3771
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "vndangkhoa",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.23.24",
"lucide-react": "^0.555.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.1",
"vite": "^5.4.21"
}
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

1
public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

122
src/App.jsx Normal file
View file

@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { links, groups } from './data/links';
import Header from './components/Header';
import DotDivider from './components/DotDivider';
import Section from './components/Section';
import PortalCard from './components/PortalCard';
import { Search } from 'lucide-react';
// Relative assets (ported from generated artifacts)
import featuredPrimary from './assets/illustrations/featured_blueprint_primary_1776568228720.png';
function App() {
const [searchQuery, setSearchQuery] = useState('');
// Enhanced links with categories and colors for the MakingSoftware look
const enhancedLinks = links.map(link => {
let color = '#3147ba'; // Primary Blue
if (link.group === 'entertainment') {
color = '#ec4899';
} else if (link.group === 'dev') {
color = '#10b981';
} else if (link.group === 'rm8pfix-vn') {
color = '#f59e0b';
}
return {
...link,
color
};
});
const filteredLinks = enhancedLinks.filter(link =>
link.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
link.subtitle.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="min-h-screen bg-white">
<Header />
<main>
{groups.map((group, groupIndex) => {
const groupLinks = filteredLinks
.filter(link => link.group === group.id)
.sort((a, b) => a.order - b.order);
if (groupLinks.length === 0) return null;
const isPrimary = group.id === 'primary';
return (
<React.Fragment key={group.id}>
<Section
title={group.title}
description={group.description}
index={groupIndex}
isFeatured={isPrimary && !searchQuery} // Show as featured only if not searching
featuredImage={isPrimary ? featuredPrimary : null}
>
{groupLinks.map((link, linkIndex) => (
<PortalCard
key={link.id}
item={link}
index={linkIndex}
/>
))}
</Section>
{/* Position Search Bar immediately after the Primary Featured section */}
{isPrimary && (
<div className="ms-container pt-0 pb-6 md:pb-8">
<div className="mb-8"><DotDivider /></div>
<div className="relative group max-w-md">
<div className="absolute -left-4 top-1/2 -translate-y-1/2 font-mono text-[10px] text-blue-600 font-bold vertical-text hidden md:block">
SEARCH
</div>
<div className="flex items-center gap-2 border-b-2 border-black pb-2 focus-within:border-blue-600 transition-colors">
<Search className="text-gray-300 group-focus-within:text-blue-600 shrink-0" size={16} />
<input
type="text"
placeholder="FIND SYSTEM ACCESS..."
className="w-full bg-transparent font-pixel text-[13px] md:text-sm focus:outline-none placeholder:text-gray-200"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</div>
)}
{groupIndex < groups.length - 1 && !isPrimary && <div className="mt-4"><DotDivider /></div>}
</React.Fragment>
);
})}
{searchQuery && filteredLinks.length === 0 && (
<div className="text-center py-12 px-4">
<p className="font-pixel text-[10px] md:text-xs text-gray-300 uppercase tracking-widest">[ ERROR: NO ACCESS MATCHED ]</p>
</div>
)}
</main>
<footer className="ms-container border-t border-gray-100 mt-8 md:mt-12 pb-12 flex flex-col md:flex-row justify-between items-center md:items-end gap-8">
<div className="flex items-center gap-2 w-full md:w-auto">
<h2 className="font-pixel text-blue-600 text-xs md:text-sm">VNDANGKHOA_PORTAL</h2>
</div>
<div className="text-center md:text-right w-full md:w-auto">
<p className="font-serif text-[10px] md:text-[11px] font-bold text-gray-900 italic">
"Software engineering is the most important trade of the 21st century."
</p>
<p className="font-mono text-[9px] text-gray-400 mt-2 tracking-widest">
© {new Date().getFullYear()} REPRODUCED PIXEL PERFECT
</p>
</div>
</footer>
</div>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

1
src/assets/react.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,60 @@
import React, { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
const ArticleCard = ({ item, index }) => {
const { title, subtitle, url, category, image } = item;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
// Default color if none provided
const accentColor = item.color || '#3b82f6';
return (
<motion.a
ref={ref}
href={url}
target="_blank"
rel="noopener noreferrer"
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="ms-card group block overflow-hidden bg-white"
>
<div className="aspect-[16/9] overflow-hidden bg-gray-50 mb-6 rounded-2xl relative">
<motion.img
src={image || `https://api.placeholder.com/800/450`}
alt={title}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.8, ease: [0.33, 1, 0.68, 1] }}
className="w-full h-full object-cover"
/>
<div
className="absolute inset-0 opacity-10 group-hover:opacity-20 transition-opacity"
style={{ backgroundColor: accentColor }}
/>
</div>
<div className="px-1">
<div className="flex items-center gap-2 mb-3">
<span
className="mono text-[10px] uppercase font-bold tracking-widest px-2 py-0.5 rounded border"
style={{ color: accentColor, borderColor: `${accentColor}33`, backgroundColor: `${accentColor}11` }}
>
{category || 'Software'}
</span>
<span className="text-[10px] text-gray-400 font-medium">8 min read</span>
</div>
<h3 className="text-2xl font-bold tracking-tight mb-3 group-hover:text-blue-600 transition-colors">
{title}
</h3>
<p className="text-gray-500 text-sm leading-relaxed line-clamp-2">
{subtitle || 'Deep dive into the underlying principles and implementation details of this concept.'}
</p>
</div>
</motion.a>
);
};
export default ArticleCard;

View file

@ -0,0 +1,9 @@
import React from 'react';
const DotDivider = () => {
return (
<div className="dot-divider" aria-hidden="true" />
);
};
export default DotDivider;

24
src/components/Header.jsx Normal file
View file

@ -0,0 +1,24 @@
import React from 'react';
const Header = () => {
return (
<header className="ms-container pt-8 md:pt-12 pb-4 md:pb-8 flex flex-col md:flex-row justify-between items-start md:items-baseline gap-6 md:gap-4">
<div className="flex flex-col w-full md:w-auto">
<h1 className="text-3xl sm:text-4xl md:text-6xl text-blue-600 font-pixel tracking-tighter leading-none">
VNDANGKHOA<span className="text-gray-200">_</span>PORTAL
</h1>
</div>
<div className="flex flex-col text-left md:text-right md:max-w-xs transition-opacity hover:opacity-100 opacity-80">
<p className="font-serif text-[12px] md:text-[13px] leading-relaxed italic text-gray-800">
A reference manual for people who design and build software.
</p>
<p className="font-serif text-[11px] md:text-[12px] font-bold mt-1 text-black uppercase tracking-wider">
Managed by Khoa Vo.
</p>
</div>
</header>
);
};
export default Header;

27
src/components/Hero.jsx Normal file
View file

@ -0,0 +1,27 @@
import React from 'react';
import { motion } from 'framer-motion';
const Hero = () => {
return (
<section className="ms-section pt-24 pb-8">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<span className="mono text-[10px] uppercase font-bold tracking-[0.3em] text-blue-600 mb-2 block">
Central Access Portal
</span>
<h1 className="text-4xl md:text-5xl font-black tracking-tighter mb-4">
VNDANGKHOA<span className="text-gray-300">.</span>PORTAL
</h1>
<p className="text-sm text-gray-500 max-w-xl leading-relaxed">
Quick entry point for all internal systems, media platforms, and utility tools.
Powered by the <span className="ms-highlight font-medium text-black">Making Software</span> design system.
</p>
</motion.div>
</section>
);
};
export default Hero;

View file

@ -0,0 +1,35 @@
import React, { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
import { ExternalLink } from 'lucide-react';
const LinkItem = ({ link, index }) => {
const { title, subtitle, url, icon: Icon } = link;
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
return (
<motion.a
ref={ref}
href={url}
target="_blank"
rel="noopener noreferrer"
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.3, delay: index * 0.05 }}
className="flex items-center justify-between py-2 sm:py-3 px-3 sm:px-4 box-thin invert-hover cursor-pointer group"
>
<div className="flex items-center gap-2 sm:gap-4">
<div className="p-0.5">
<Icon size={16} strokeWidth={2} className="sm:size-[20px]" />
</div>
<div>
<span className="font-semibold text-sm sm:text-lg">{title}</span>
<span className="text-xs sm:text-sm text-gray-500 ml-1 sm:ml-2 group-hover:text-white">{subtitle}</span>
</div>
</div>
<ExternalLink size={14} className="sm:size-[18px] group-hover:text-white" />
</motion.a>
);
};
export default LinkItem;

View file

@ -0,0 +1,47 @@
import React from 'react';
import { motion } from 'framer-motion';
const PortalCard = ({ item, index }) => {
const { title, subtitle, url, color } = item;
return (
<motion.a
href={url}
target="_blank"
rel="noopener noreferrer"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
className="tech-label group block relative"
>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="font-mono text-[10px] text-gray-300 font-bold uppercase tracking-tight">
[ PORT_ACCESS ]
</span>
<div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: color }}
/>
</div>
<h3 className="font-mono text-[13px] font-extrabold text-black flex items-center gap-1 group-hover:text-blue-600 transition-colors">
<span className="opacity-40 group-hover:opacity-100 transition-opacity"></span> {title.toUpperCase()}
</h3>
<p className="font-serif text-[11px] italic text-gray-400 leading-tight">
{subtitle}
</p>
</div>
{/* Technical bracket decorations */}
<div className="absolute top-0 right-0 p-1 opacity-20 group-hover:opacity-100 transition-all">
<div className="font-mono text-[8px] vertical-text">
EST. 2026
</div>
</div>
</motion.a>
);
};
export default PortalCard;

View file

@ -0,0 +1,62 @@
import React, { useRef } from 'react';
import { motion, useInView } from 'framer-motion';
const Section = ({ title, description, children, index, isFeatured, featuredImage }) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
// Format index as 001, 002, etc.
const figNumber = String(index + 1).padStart(3, '0');
return (
<section className={`ms-container relative py-4 md:py-8 flex flex-col ${isFeatured ? 'md:flex-col lg:flex-row' : 'md:flex-row'} gap-6 md:gap-12`}>
{/* Sidebar Illustration Info */}
<div className={`${isFeatured ? 'lg:w-1/2' : 'md:w-1/3'} flex group`}>
<div className="relative w-full">
{/* Vertical Label - Desktop Only */}
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5 }}
className="hidden md:block vertical-text absolute -left-8 top-0 text-[10px] font-mono text-gray-300 tracking-widest uppercase font-bold"
>
FIG. {figNumber}
</motion.div>
<div className="md:pr-4">
<h2 className={`${isFeatured ? 'text-2xl md:text-3xl' : 'text-xl md:text-sm'} font-pixel text-blue-600 mb-6 md:mb-8 tracking-wider uppercase`}>
{isFeatured ? `FEATURED: ${title}` : title}
</h2>
<p className={`${isFeatured ? 'text-[18px] md:text-[20px] leading-relaxed' : 'text-[15px] md:text-[14px] leading-relaxed'} font-serif text-gray-800 drop-cap opacity-95 group-hover:opacity-100 transition-opacity`}>
{description}
</p>
{isFeatured && featuredImage && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 1, delay: 0.2 }}
className="mt-12 border border-gray-100 p-2 bg-white shadow-2xl shadow-blue-900/5"
>
<img src={featuredImage} alt="Featured Technical Illustration" className="w-full h-auto" />
<div className="mt-4 flex justify-between items-center font-mono text-[9px] text-gray-400 uppercase tracking-widest px-2">
<span>SCALE 1:15</span>
<span>EXPLODED_VIEW_DIG.V1</span>
</div>
</motion.div>
)}
</div>
</div>
</div>
{/* Main Grid Content */}
<div className={`${isFeatured ? 'lg:w-1/2' : 'md:w-2/3'}`}>
<div className={`grid grid-cols-1 ${isFeatured ? 'sm:grid-cols-1 md:grid-cols-2' : 'sm:grid-cols-2'} gap-4`}>
{children}
</div>
</div>
</section>
);
};
export default Section;

View file

@ -0,0 +1,98 @@
import React from 'react';
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { ExternalLink } from 'lucide-react';
const ServiceCard = ({ link, index, size = "default" }) => {
const { title, subtitle, url, icon: Icon, color, shadow } = link;
const sizeClasses = size === "large"
? "w-40 sm:w-48 md:w-56 p-4 sm:p-6 md:p-8"
: "w-28 sm:w-32 md:w-36 p-3 sm:p-4 md:p-5";
const iconSize = size === "large"
? "w-8 h-8 sm:w-9 md:w-10"
: "w-6 h-6 sm:w-7 md:w-8";
const titleSize = size === "large"
? "text-lg sm:text-xl md:text-2xl"
: "text-sm sm:text-base md:text-lg";
// Motion values for tilt effect
const x = useMotionValue(0);
const y = useMotionValue(0);
// Smooth rotation
const rotateX = useTransform(y, [-100, 100], [15, -15]);
const rotateY = useTransform(x, [-100, 100], [-15, 15]);
function handleMouseMove(event) {
const rect = event.currentTarget.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const xPct = mouseX / width - 0.5;
const yPct = mouseY / height - 0.5;
x.set(xPct * 200); // Amplify movement
y.set(yPct * 200);
}
function handleMouseLeave() {
x.set(0);
y.set(0);
}
return (
<motion.div
style={{
perspective: 1000,
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<motion.a
href={url}
target="_blank"
rel="noopener noreferrer"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
className={`relative group block ${sizeClasses} rounded-2xl bg-white/50 dark:bg-slate-900/50 border border-slate-200 dark:border-slate-800 backdrop-blur-xl overflow-hidden hover:border-slate-400 dark:hover:border-slate-600 transition-colors duration-300 ${shadow} hover:shadow-2xl`}
>
{/* Glare Effect */}
<div
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none z-20"
style={{
background: 'radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(255,255,255,0.1) 0%, transparent 80%)'
}}
/>
<div
className={`absolute inset-0 opacity-0 group-hover:opacity-20 transition-opacity duration-500 bg-gradient-to-br ${color}`}
style={{ transform: "translateZ(-20px)" }}
/>
<div className="relative z-10 flex items-start justify-between" style={{ transform: "translateZ(20px)" }}>
<div className={`p-2 sm:p-3 rounded-lg sm:rounded-xl bg-slate-100/50 dark:bg-slate-800/50 text-slate-900 dark:text-white group-hover:scale-110 transition-transform duration-300`}>
<Icon className={`${iconSize} text-slate-900 dark:text-white`} />
</div>
<ExternalLink className="w-4 h-4 sm:w-5 sm:h-5 text-slate-400 dark:text-slate-500 group-hover:text-slate-900 dark:group-hover:text-white transition-colors" />
</div>
<div className="relative z-10 mt-2 sm:mt-3 md:mt-4" style={{ transform: "translateZ(30px)" }}>
<h3 className={`${titleSize} font-bold text-slate-900 dark:text-white group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-slate-900 group-hover:to-slate-600 dark:group-hover:from-white dark:group-hover:to-slate-300 transition-all`}>
{title}
</h3>
<p className="mt-1 text-xs sm:text-sm text-slate-500 dark:text-slate-400 group-hover:text-slate-700 dark:group-hover:text-slate-300 transition-colors">
{subtitle}
</p>
</div>
</motion.a>
</motion.div>
);
};
export default ServiceCard;

View file

@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react';
import { motion, useSpring, useMotionValue } from 'framer-motion';
const Spotlight = () => {
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
// Smooth out the mouse movement
const springConfig = { damping: 25, stiffness: 150 };
const x = useSpring(mouseX, springConfig);
const y = useSpring(mouseY, springConfig);
useEffect(() => {
const handleMouseMove = (e) => {
// Center the spotlight on the cursor
mouseX.set(e.clientX - 250);
mouseY.set(e.clientY - 250);
};
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, [mouseX, mouseY]);
return (
<motion.div
className="pointer-events-none fixed inset-0 z-0 overflow-hidden"
style={{ opacity: 0.6 }}
>
<motion.div
className="absolute w-[500px] h-[500px] rounded-full bg-gradient-to-r from-indigo-500/30 to-purple-500/30 blur-3xl"
style={{
x,
y,
}}
/>
</motion.div>
);
};
export default Spotlight;

148
src/data/links.js Normal file
View file

@ -0,0 +1,148 @@
import { Youtube, Video, FileText, Image, Terminal, Film, Music, Briefcase, Wrench, Monitor, Cpu, HardDrive } from 'lucide-react';
export const links = [
{
id: 1,
title: "Portfolio",
subtitle: "My Work",
url: "https://portfolio.khoavo.myds.me",
icon: Briefcase,
group: "primary",
order: 1
},
{
id: 2,
title: "CV",
subtitle: "Resume",
url: "https://cv.khoavo.myds.me",
icon: FileText,
group: "primary",
order: 2
},
{
id: 3,
title: "YouTube",
subtitle: "No Ads",
url: "https://ut.khoavo.myds.me",
icon: Youtube,
group: "entertainment",
order: 1
},
{
id: 4,
title: "TikTok",
subtitle: "No Ads",
url: "https://tt.khoavo.myds.me",
icon: Video,
group: "entertainment",
order: 2
},
{
id: 5,
title: "Music",
subtitle: "Streaming",
url: "https://music.khoavo.myds.me",
icon: Music,
group: "entertainment",
order: 3
},
{
id: 6,
title: "Spotify",
subtitle: "Streaming",
url: "https://sp.khoavo.myds.me",
icon: Music,
group: "entertainment",
order: 4
},
{
id: 7,
title: "Netflix",
subtitle: "Streaming",
url: "https://nf.khoavo.myds.me",
icon: Film,
group: "entertainment",
order: 5
},
{
id: 8,
title: "RM8PFix",
subtitle: "Fixes & Tools",
url: "https://rm8pfix.khoavo.myds.me",
icon: Wrench,
group: "rm8pfix-vn",
order: 1
},
{
id: 9,
title: "Portal",
subtitle: "Save",
url: "https://save.khoavo.myds.me",
icon: HardDrive,
group: "rm8pfix-vn",
order: 2
},
{
id: 10,
title: "Free",
subtitle: "Free",
url: "https://free.khoavo.myds.me",
icon: Wrench,
group: "rm8pfix-vn",
order: 3
},
{
id: 11,
title: "PDF",
subtitle: "Tools",
url: "https://pdf.khoavo.myds.me",
icon: FileText,
group: "dev",
order: 1
},
{
id: 12,
title: "JPG",
subtitle: "Tools",
url: "https://jpg.khoavo.myds.me",
icon: Image,
group: "dev",
order: 2
},
{
id: 13,
title: "IT Utilities",
subtitle: "Dev Tools",
url: "https://it.khoavo.myds.me",
icon: Terminal,
group: "dev",
order: 3
},
];
export const groups = [
{
id: 'primary',
title: 'PRIMARY',
icon: Briefcase,
description: "Explore the underlying principles of the primary ecosystem. Each component is designed for maximum efficiency and conceptual clarity in your daily workflow."
},
{
id: 'entertainment',
title: 'ENTERTAINMENT',
icon: Monitor,
description: "Digital consumption and leisure systems. A curated selection of platforms for high-fidelity media streaming and interactive entertainment."
},
{
id: 'rm8pfix-vn',
title: 'RM8PFIX-VN',
icon: Wrench,
description: "Specialized system utilities and maintenance protocols for local network environments. Ensuring stability and performance through automated tooling."
},
{
id: 'dev',
title: 'DEV TOOLS',
icon: Cpu,
description: "A comprehensive toolkit for the modern developer. From binary manipulation to layout verification, these utilities accelerate the build process."
},
];

96
src/index.css Normal file
View file

@ -0,0 +1,96 @@
@import url('https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--bg-primary: #ffffff;
--text-primary: #000000;
--text-secondary: #6b7280;
--accent: #3147ba;
--accent-soft: rgba(49, 71, 186, 0.05);
--border-color: #e5e7eb;
}
@layer base {
body {
@apply bg-white text-black antialiased font-serif;
background-image:
linear-gradient(var(--accent-soft) 1px, transparent 1px),
linear-gradient(90deg, var(--accent-soft) 1px, transparent 1px);
background-size: 24px 24px;
}
h1, h2, h3, .font-pixel {
font-family: 'Silkscreen', cursive;
}
p, .font-serif {
font-family: 'PT Serif', serif;
}
code, pre, .font-mono {
font-family: 'IBM Plex Mono', monospace;
}
}
/* Technical Layout Utilities */
.ms-container {
@apply max-w-[1100px] mx-auto px-6 md:px-12 py-8 md:py-12;
}
/* Dropped Cap */
.drop-cap::first-letter {
@apply text-6xl font-bold float-left mr-3 leading-[0.8] mt-2;
font-family: 'PT Serif', serif;
}
/* Wavy Highlight (Refined for Pixel Perfect) */
.ms-highlight {
@apply relative inline-block;
text-decoration: underline wavy var(--accent);
text-underline-offset: 4px;
}
/* Dot Grid Divider */
.dot-divider {
@apply w-full h-8 mb-6 md:mb-8;
background-image: radial-gradient(circle, #d1d5db 1px, transparent 1px);
background-size: 12px 12px;
}
/* Card Styling (Technical Labels) */
.tech-label {
@apply border border-gray-100 bg-white p-4 transition-all duration-300;
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.05);
}
.tech-label:hover {
@apply border-blue-600 shadow-blue-100;
box-shadow: 4px 4px 0px var(--accent-soft);
transform: translate(-2px, -2px);
}
/* Vertical Text Helper */
.vertical-text {
writing-mode: vertical-rl;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: white;
}
::-webkit-scrollbar-thumb {
background: var(--accent-soft);
border: 4px solid white;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}

10
src/main.jsx Normal file
View file

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

28
tailwind.config.js Normal file
View file

@ -0,0 +1,28 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
animation: {
'blob': 'blob 7s infinite',
},
keyframes: {
blob: {
'0%': { transform: 'translate(0px, 0px) scale(1)' },
'33%': { transform: 'translate(30px, -50px) scale(1.1)' },
'66%': { transform: 'translate(-20px, 20px) scale(0.9)' },
'100%': { transform: 'translate(0px, 0px) scale(1)' },
}
}
},
},
plugins: [],
}

11
vite.config.js Normal file
View file

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: './',
build: {
sourcemap: false,
},
})