Complete tetris grid layout with animations and mobile support

This commit is contained in:
vndangkhoa 2026-04-29 15:17:07 +07:00
parent 6a37da1ae5
commit 54e5018030
13 changed files with 1347 additions and 940 deletions

View file

@ -5,7 +5,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VNDANGKHOA</title> <title>VNDANGKHOA — PORTAL</title>
<meta name="description" content="VNDANGKHOA, developer and creator. A portal to digital works and utilities." />
<meta http-equiv="Referrer-Policy" content="no-referrer" /> <meta http-equiv="Referrer-Policy" content="no-referrer" />
<meta http-equiv="X-Content-Type-Options" content="nosniff" /> <meta http-equiv="X-Content-Type-Options" content="nosniff" />
</head> </head>

1262
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,6 @@
"globals": "^16.5.0", "globals": "^16.5.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"vite": "^5.4.21" "vite": "^7.0.0"
} }
} }

BIN
public/cv-video.mp4 Normal file

Binary file not shown.

View file

@ -1,127 +1,306 @@
import React, { useState } from 'react'; import React, { useState, useEffect } 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';
// Interactive blueprint components const COLORS_LIGHT = ['#7DD3D8', '#4A7BC7', '#F5A623', '#FFDC00', '#4CAF50', '#9C27B0', '#DC143C'];
import PrimaryBlueprint from './components/illustrations/PrimaryBlueprint'; const COLORS_DARK = ['#5AA0A5', '#3A6294', '#C88820', '#C8B000', '#3E8A40', '#7A2090', '#A01030'];
import MediaBlueprint from './components/illustrations/MediaBlueprint';
import MaintenanceBlueprint from './components/illustrations/MaintenanceBlueprint'; function getRandomColor(isDark) {
import DevToolsBlueprint from './components/illustrations/DevToolsBlueprint'; const colors = isDark ? COLORS_DARK : COLORS_LIGHT;
return colors[Math.floor(Math.random() * colors.length)];
}
function TetrisPiece({ piece, isDark }) {
const [color, setColor] = useState(null);
const [showText, setShowText] = useState(false);
const isCV = piece.label === 'cv';
const isFeatured = piece.featured;
const alwaysShowText = true;
const handleMouseEnter = () => {
if (!isCV) setColor(piece.hoverColor);
setShowText(true);
};
const handleMouseLeave = () => {
if (!isCV) setColor(null);
setShowText(false);
};
const bgColor = piece.label === 'cv' ? piece.color : piece.color;
const rowDelay = piece.startY * 4;
const style = {
gridColumn: `${piece.startX + 1} / span ${piece.w}`,
gridRow: `${piece.startY + 1} / span ${piece.h}`,
backgroundColor: color || bgColor,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
aspectRatio: '1',
transition: 'background 0.15s ease',
cursor: piece.link ? 'pointer' : 'default',
overflow: 'hidden',
position: 'relative',
animation: `dropIn 0.5s ease-out forwards, colorBlink 5s ease-in-out ${rowDelay}s infinite`,
animationFillMode: 'forwards, none',
};
const className = 'tetris-piece show';
// CV block shows looping video
if (isCV) {
return (
<a
href={piece.link}
target="_blank"
rel="noopener"
className={className}
style={style}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<video
autoPlay
loop
muted
playsInline
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: 1,
transition: 'opacity 0.15s ease',
border: 'none',
outline: 'none',
}}
>
<source src="/cv-video.mp4" type="video/mp4" />
</video>
<span style={{
position: 'absolute',
bottom: '8px',
left: '8px',
fontSize: 'clamp(8px, 1.5vw, 12px)',
fontWeight: '500',
textTransform: 'lowercase',
color: '#fff',
opacity: 1,
transition: 'opacity 0.15s ease',
}}>
{piece.label}
</span>
</a>
);
}
// Text only shows on hover, positioned at bottom left
const textStyle = {
fontSize: 'clamp(8px, 1.5vw, 12px)',
fontWeight: '500',
textTransform: 'lowercase',
color: alwaysShowText ? '#fff' : '#000',
opacity: alwaysShowText ? 1 : (showText ? 1 : 0),
transition: 'opacity 0.15s ease',
position: 'absolute',
bottom: '8px',
left: '8px',
};
const wrapperStyle = {
position: 'relative',
width: '100%',
height: '100%',
};
if (piece.link) {
return (
<a
href={piece.link}
target="_blank"
rel="noopener"
className={className}
style={style}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div style={wrapperStyle}>
<span style={textStyle}>{piece.label}</span>
</div>
</a>
);
}
return (
<div
className={className}
style={style}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div style={wrapperStyle}>
<span style={{...textStyle, cursor: 'default'}}>{piece.label}</span>
</div>
</div>
);
}
function App() { function App() {
const [searchQuery, setSearchQuery] = useState(''); const [isDark, setIsDark] = useState(() => {
const hour = new Date().getHours();
// Enhanced links with categories and colors for the MakingSoftware look return hour < 6 || hour >= 18;
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 [seed, setSeed] = useState(() => Math.random());
const filteredLinks = enhancedLinks.filter(link => const shuffleLayout = () => {
link.title.toLowerCase().includes(searchQuery.toLowerCase()) || const items = [
link.subtitle.toLowerCase().includes(searchQuery.toLowerCase()) { w: 4, h: 1, label: 'rm8pfix', link: 'https://rm8pfix.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#00BCD4' },
); { w: 2, h: 2, label: 'netflix', link: 'https://nf.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FF9800' },
{ w: 2, h: 3, label: 'portfolio', link: 'https://portfolio.khoavo.myds.me', featured: true, color: '#4A7BC7', hoverColor: '#2196F3' },
{ w: 2, h: 3, label: 'cv', link: 'https://cv.khoavo.myds.me', color: '#616161', hoverColor: '#424242' },
{ w: 2, h: 2, label: 'youtube', link: 'https://ut.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FF5722' },
{ w: 2, h: 2, label: 'tiktok', link: 'https://tt.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#9C27B0' },
{ w: 3, h: 2, label: 'spotify', link: 'https://sp.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#4CAF50' },
{ w: 3, h: 2, label: 'tools', link: 'https://it.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FFC107' },
{ w: 2, h: 2, label: 'save', link: 'https://save.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#E91E63' },
{ w: 2, h: 2, label: 'free', link: 'https://free.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#00BCD4' },
{ w: 2, h: 2, label: 'jpg', link: 'https://jpg.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#673AB7' },
{ w: 2, h: 2, label: 'pdf', link: 'https://pdf.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#795548' },
];
return ( const random = (s) => {
<div className="min-h-screen bg-white"> const x = Math.sin(s * 9999) * 10000;
<Header /> return x - Math.floor(x);
};
<main> // Sort items by size (larger first) to make placement easier
{groups.map((group, groupIndex) => { const sortedItems = [...items].sort((a, b) => (b.w * b.h) - (a.w * a.h));
const groupLinks = filteredLinks
.filter(link => link.group === group.id)
.sort((a, b) => a.order - b.order);
if (groupLinks.length === 0) return null; // Shuffle the sorted items
for (let i = sortedItems.length - 1; i > 0; i--) {
const isPrimary = group.id === 'primary'; const j = Math.floor(random(seed + i * 100) * (i + 1));
[sortedItems[i], sortedItems[j]] = [sortedItems[j], sortedItems[i]];
return (
<React.Fragment key={group.id}>
<Section
title={group.title}
description={group.description}
index={groupIndex}
isFeatured={isPrimary && !searchQuery}
featuredImage={
group.id === 'primary' ? <PrimaryBlueprint /> :
group.id === 'entertainment' ? <MediaBlueprint /> :
group.id === 'rm8pfix-vn' ? <MaintenanceBlueprint /> :
<DevToolsBlueprint />
} }
const gridRows = 10;
const gridCols = 10;
const grid = Array(gridRows).fill(null).map(() => Array(gridCols).fill(0));
const placed = [];
for (const item of sortedItems) {
let fits = false;
let bestPos = null;
let minOverlap = Infinity;
// Try many random positions
for (let attempt = 0; attempt < 500; attempt++) {
const r1 = random(seed + attempt * 7 + item.label.charCodeAt(0));
const r2 = random(seed + attempt * 13 + item.label.charCodeAt(0));
const row = Math.floor(r1 * (gridRows - item.h + 1));
const col = Math.floor(r2 * (gridCols - item.w + 1));
// Check if this position works
let canPlace = true;
let overlap = 0;
for (let rr = row; rr < row + item.h && canPlace; rr++) {
for (let cc = col; cc < col + item.w && canPlace; cc++) {
if (grid[rr] && grid[rr][cc] === 1) {
canPlace = false;
}
}
}
if (canPlace) {
for (let rr = row; rr < row + item.h; rr++) {
for (let cc = col; cc < col + item.w; cc++) {
grid[rr][cc] = 1;
}
}
placed.push({ ...item, startX: col, startY: row });
fits = true;
break;
}
}
}
return placed;
};
const layout = shuffleLayout();
const sortedLayout = [...layout].sort((a, b) => b.startY - a.startY);
const toggleTheme = () => setIsDark(!isDark);
const refreshLayout = () => setSeed(Math.random());
const bg = isDark ? '#1a1a1a' : '#ffffff';
const headerBg = isDark ? '#1a1a1a' : '#ffffff';
const textColor = isDark ? '#fff' : '#000';
const borderColor = isDark ? '#444' : '#000';
return (
<div style={{ height: '100vh', background: bg, padding: '0 10px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<header style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 12px',
background: headerBg,
flexWrap: 'wrap',
gap: '8px',
flexShrink: 0,
}}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '12px', flexWrap: 'wrap' }}>
<a href="/" style={{ fontSize: '14px', fontWeight: '500', color: textColor }}>Khoa.vo</a>
<span style={{ fontSize: '10px', color: isDark ? '#666' : '#999' }} className="header-tagline">where design meets intelligence</span>
</div>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<button
onClick={refreshLayout}
style={{
background: 'none',
border: `1px solid ${borderColor}`,
padding: '8px 12px',
fontSize: '14px',
fontFamily: 'inherit',
cursor: 'pointer',
color: textColor,
touchAction: 'manipulation',
}}
> >
{groupLinks.map((link, linkIndex) => (
<PortalCard </button>
key={link.id} <button
item={link} onClick={toggleTheme}
index={linkIndex} style={{
/> background: 'none',
border: `1px solid ${borderColor}`,
padding: '8px 12px',
fontSize: '14px',
fontFamily: 'inherit',
cursor: 'pointer',
color: textColor,
touchAction: 'manipulation',
}}
>
{isDark ? '☀' : '☾'}
</button>
</div>
</header>
<main className="tetris-board" key={seed} style={{ background: isDark ? '#222' : '#fff', flex: '1 1 auto', minHeight: 0 }}>
{sortedLayout.map((piece, idx) => (
<TetrisPiece key={piece.label} piece={piece} isDark={isDark} />
))} ))}
</Section>
{/* Position Search Bar immediately after the Primary Featured section */}
{isPrimary && (
<div className="ms-container pt-0 pb-1 md:pb-2">
<div className="mb-1"><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-1"><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> </main>
<footer className="ms-container border-t border-gray-100 mt-2 md:mt-4 pb-12 flex flex-col md:flex-row justify-between items-center md:items-end gap-8"> <footer style={{
<div className="flex items-center gap-2 w-full md:w-auto"> padding: '8px 20px',
<h2 className="font-pixel text-blue-600 text-xs md:text-sm">VNDANGKHOA_PORTAL</h2> fontSize: '10px',
</div> color: isDark ? '#888' : '#666',
width: '100%',
<div className="text-center md:text-right w-full md:w-auto"> maxWidth: '500px',
<p className="font-serif text-[10px] md:text-[11px] font-bold text-gray-900 italic"> margin: '0 auto',
"Software engineering is the most important trade of the 21st century." textAlign: 'center',
</p> flexShrink: 0,
<p className="font-mono text-[9px] text-gray-400 mt-2 tracking-widest"> }}>
© {new Date().getFullYear()} REPRODUCED PIXEL PERFECT <p>© {new Date().getFullYear()} Khoa.vo</p>
</p>
</div>
</footer> </footer>
</div> </div>
); );

View file

@ -0,0 +1,86 @@
import React from 'react';
import { motion } from 'framer-motion';
const FeaturedCard = ({ title, subtitle, url, type, videoSrc, index = 0 }) => {
return (
<motion.a
href={url}
target="_blank"
rel="noopener noreferrer"
initial={{ opacity: 0, y: -30 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{
backgroundColor: 'var(--color-border-default)',
transition: { duration: 0.15 }
}}
whileTap={{ scale: 0.98 }}
viewport={{ once: true }}
transition={{
duration: 0.3,
delay: index * 0.1,
ease: [0.25, 0.46, 0.45, 0.94]
}}
className="block p-3 md:p-4"
style={{
backgroundColor: 'var(--color-surface-base)',
border: '1px solid var(--color-border-default)'
}}
>
<div className="flex flex-col min-h-[100px]">
<div className="flex items-center justify-between mb-2 md:mb-3">
<span
className="font-mono text-[9px] uppercase"
style={{ color: 'var(--color-text-secondary)' }}
>
featured
</span>
<div
className="w-1 h-1"
style={{ backgroundColor: 'var(--color-text-primary)' }}
/>
</div>
{type === 'cv' && videoSrc && (
<div
className="relative w-full h-12 md:h-16 mb-2 overflow-hidden"
style={{ border: '1px solid var(--color-border-default)' }}
>
<video
src={videoSrc}
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover opacity-80"
/>
</div>
)}
<h3
className="font-mono text-sm md:text-base"
style={{ color: 'var(--color-text-primary)' }}
>
{title}
</h3>
<p
className="font-mono text-[9px]"
style={{ color: 'var(--color-text-secondary)' }}
>
{subtitle}
</p>
<div className="mt-auto pt-2">
<span
className="font-mono text-[9px] uppercase"
style={{ color: 'var(--color-text-primary)' }}
>
enter
</span>
</div>
</div>
</motion.a>
);
};
export default FeaturedCard;

View file

@ -2,31 +2,18 @@ import React from 'react';
const Header = () => { const Header = () => {
return ( return (
<div className="border-b border-gray-100"> <header
<header className="ms-container py-6 md:py-10 flex flex-col lg:flex-row justify-between items-start lg:items-end gap-6"> className="fixed top-0 left-0 right-0 z-50 flex justify-between items-center px-6 py-4"
<div className="flex flex-col"> >
<h1 className="text-4xl sm:text-5xl md:text-7xl lg:text-[84px] text-blue-600 font-pixel tracking-[-0.05em] leading-[0.85] uppercase"> <a
VNDANGKHOA<br />PORTAL href="/"
</h1> className="font-mono text-sm"
</div> >
Khoa.vo
<div className="flex flex-col text-left lg:text-right max-w-sm lg:mb-2"> </a>
<p className="font-serif text-[15px] md:text-[17px] leading-snug italic text-gray-900 font-medium"> <div className="flex gap-6">
A reference manual for people who design and build software.
</p>
<p className="font-mono text-[11px] md:text-[12px] font-bold mt-2 text-black uppercase tracking-[0.2em]">
Managed and illustrated by Khoa Vo.
</p>
</div> </div>
</header> </header>
{/* Manual Divider Pattern */}
<div className="ms-container overflow-hidden whitespace-nowrap opacity-20 select-none pb-4" aria-hidden="true">
<div className="text-[10px] font-mono tracking-[0.5em] flex justify-between w-full">
{Array(20).fill('❖ ❖ ❖ ❖ ❖').join(' ')}
</div>
</div>
</div>
); );
}; };

View file

@ -1,46 +1,18 @@
import React from 'react'; import React from 'react';
import { motion } from 'framer-motion';
const PortalCard = ({ item, index }) => { const PortalCard = ({ item, index }) => {
const { title, subtitle, url, color } = item; const { title, subtitle, url } = item;
return ( return (
<motion.a <a
href={url} href={url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
initial={{ opacity: 0, y: 10 }} className="portal-link"
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"> <span className="portal-title">{title}</span>
<div className="flex items-center justify-between"> {subtitle && <span className="portal-subtitle">{subtitle}</span>}
<span className="font-mono text-[11px] md:text-[12px] text-gray-400 font-bold uppercase tracking-[0.1em]"> </a>
[ PORT_ACCESS ]
</span>
<div
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: color }}
/>
</div>
<h3 className="font-mono text-[16px] md:text-[18px] font-extrabold text-black flex items-center gap-2 group-hover:text-blue-600 transition-colors mt-1">
<span className="opacity-40 group-hover:opacity-100 transition-opacity"></span> {title.toUpperCase()}
</h3>
<p className="font-serif text-[13px] md:text-[14px] italic text-gray-500 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>
); );
}; };

View file

@ -1,67 +1,41 @@
import React, { useRef } from 'react'; import React from 'react';
import { motion, useInView } from 'framer-motion'; import { motion } 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');
const isEven = index % 2 === 0;
const Section = ({ title, children }) => {
return ( return (
<div className={isFeatured ? "w-full bg-blue-50/20 border-y-2 border-blue-50/50 py-4 my-8 relative shadow-sm" : "w-full"}>
<section className={`ms-container relative py-8 md:py-16 flex flex-col ${isEven ? 'lg:flex-row' : 'lg:flex-row-reverse'} gap-8 lg:gap-16 items-start`}>
{/* Sidebar Label - Desktop Only */}
<motion.div <motion.div
initial={{ opacity: 0, x: isEven ? -10 : 10 }} initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, x: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
className={`hidden lg:block vertical-text absolute ${isEven ? '-left-8' : '-right-8'} top-0 text-[11px] font-mono text-gray-300 tracking-[0.3em] uppercase font-bold`} transition={{ duration: 0.2, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<section className="px-3 md:px-5 py-4 md:py-5">
<div className="max-w-[1100px] mx-auto">
{title && (
<div className="mb-3 md:mb-4">
<h2
className="font-mono text-[9px] uppercase"
style={{ color: 'var(--color-text-primary)' }}
> >
FIG. {figNumber}
</motion.div>
{/* Content Column (Text & Buttons) */}
<div className="w-full lg:w-[45%] flex flex-col z-10 w-full relative">
<h2 className={`font-pixel text-blue-600 mb-6 tracking-wider uppercase leading-tight ${isFeatured ? 'text-3xl md:text-4xl lg:text-5xl drop-shadow-sm' : 'text-xl md:text-2xl'}`}>
{title} {title}
</h2> </h2>
<motion.div
<div className="manual-text drop-cap mb-8"> className="h-[1px] w-6 mt-1"
{description} style={{ backgroundColor: 'var(--color-text-primary)' }}
initial={{ scaleX: 0 }}
whileInView={{ scaleX: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.15, delay: 0.05 }}
/>
</div> </div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 md:gap-3">
{children} {children}
</div> </div>
</div> </div>
{/* Blueprint Column (Animation) */}
<div className="w-full lg:w-[55%]">
{featuredImage && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-50px" }}
className="blueprint-container"
>
{typeof featuredImage === 'string' ? (
<img src={featuredImage} alt={title} className="w-full h-auto grayscale hover:grayscale-0 transition-all duration-700" />
) : (
<div className="w-full h-auto grayscale hover:grayscale-0 transition-all duration-700">
{featuredImage}
</div>
)}
<div className="mt-3 flex justify-between items-center font-mono text-[9px] text-gray-400 uppercase tracking-widest px-1">
<span>SYSTEM_SCHEMATIC_V.1</span>
<span>ID_{figNumber}</span>
</div>
</motion.div>
)}
</div>
</section> </section>
</div> </motion.div>
); );
}; };

View file

@ -22,37 +22,28 @@ export const links = [
{ {
id: 3, id: 3,
title: "YouTube", title: "YouTube",
subtitle: "No Ads", subtitle: "Ad-free",
url: "https://ut.khoavo.myds.me", url: "https://ut.khoavo.myds.me",
icon: Youtube, icon: Youtube,
group: "entertainment", group: "media",
order: 1 order: 1
}, },
{ {
id: 4, id: 4,
title: "TikTok", title: "TikTok",
subtitle: "No Ads", subtitle: "Ad-free",
url: "https://tt.khoavo.myds.me", url: "https://tt.khoavo.myds.me",
icon: Video, icon: Video,
group: "entertainment", group: "media",
order: 2 order: 2
}, },
{ {
id: 5, id: 5,
title: "Music",
subtitle: "Streaming",
url: "https://music.khoavo.myds.me",
icon: Music,
group: "entertainment",
order: 3
},
{
id: 6,
title: "Spotify", title: "Spotify",
subtitle: "Streaming", subtitle: "Streaming",
url: "https://sp.khoavo.myds.me", url: "https://sp.khoavo.myds.me",
icon: Music, icon: Music,
group: "entertainment", group: "media",
order: 4 order: 4
}, },
{ {
@ -61,16 +52,16 @@ export const links = [
subtitle: "Streaming", subtitle: "Streaming",
url: "https://nf.khoavo.myds.me", url: "https://nf.khoavo.myds.me",
icon: Film, icon: Film,
group: "entertainment", group: "media",
order: 5 order: 5
}, },
{ {
id: 8, id: 8,
title: "RM8PFix", title: "RM8PFix",
subtitle: "Fixes & Tools", subtitle: "Tools",
url: "https://rm8pfix.khoavo.myds.me", url: "https://rm8pfix.khoavo.myds.me",
icon: Wrench, icon: Wrench,
group: "rm8pfix-vn", group: "tools",
order: 1 order: 1
}, },
{ {
@ -79,7 +70,7 @@ export const links = [
subtitle: "Save", subtitle: "Save",
url: "https://save.khoavo.myds.me", url: "https://save.khoavo.myds.me",
icon: HardDrive, icon: HardDrive,
group: "rm8pfix-vn", group: "tools",
order: 2 order: 2
}, },
{ {
@ -88,7 +79,7 @@ export const links = [
subtitle: "Free", subtitle: "Free",
url: "https://free.khoavo.myds.me", url: "https://free.khoavo.myds.me",
icon: Wrench, icon: Wrench,
group: "rm8pfix-vn", group: "tools",
order: 3 order: 3
}, },
{ {
@ -97,8 +88,8 @@ export const links = [
subtitle: "Tools", subtitle: "Tools",
url: "https://pdf.khoavo.myds.me", url: "https://pdf.khoavo.myds.me",
icon: FileText, icon: FileText,
group: "dev", group: "tools",
order: 1 order: 4
}, },
{ {
id: 12, id: 12,
@ -106,43 +97,55 @@ export const links = [
subtitle: "Tools", subtitle: "Tools",
url: "https://jpg.khoavo.myds.me", url: "https://jpg.khoavo.myds.me",
icon: Image, icon: Image,
group: "dev", group: "tools",
order: 2 order: 5
},
{
id: 6,
title: "Netflix",
}, },
{ {
id: 13, id: 7,
title: "RM8PFix",
},
{
id: 8,
title: "Portal",
},
{
id: 9,
title: "Free",
},
{
id: 10,
title: "PDF",
},
{
id: 11,
title: "JPG",
},
{
id: 12,
title: "IT Utilities", title: "IT Utilities",
subtitle: "Dev Tools", subtitle: "Dev Tools",
url: "https://it.khoavo.myds.me", url: "https://it.khoavo.myds.me",
icon: Terminal, icon: Terminal,
group: "dev", group: "tools",
order: 3 order: 6
}, },
]; ];
export const groups = [ export const groups = [
{ {
id: 'primary', id: 'primary',
title: 'PRIMARY', title: 'primary',
icon: Briefcase,
description: "Have you ever wondered how your digital presence is routed across different platforms? The primary resource layer acts as the centralized directory for all professional identities. These components are hard-coded for maximum reliability and persistent access to the core VNDK ecosystem."
}, },
{ {
id: 'entertainment', id: 'media',
title: 'MEDIA_DISTRIBUTION', title: 'media',
icon: Monitor,
description: "Digital consumption and leisure systems operate on a separate high-fidelity distribution plane. By utilizing unauthorized ad-blocking layers and clean streaming protocols, we ensure that media remains an uninterrupted experience within the portal."
}, },
{ {
id: 'rm8pfix-vn', id: 'tools',
title: 'SYSTEM_MAINTENANCE', title: 'tools',
icon: Wrench,
description: "This terminal routes directly to the REDMAGIC hardware modification group. Representing flagship architecture (builds 8, 9, and 10 Pro), these devices feature top-tier Snapdragon processing and kernel-level flexibility, granting enthusiasts absolute administrative control to deeply customize their systems."
},
{
id: 'dev',
title: 'DEVELOPMENT_STACK',
icon: Cpu,
description: "Because our software tools are built on primitive data types, we need a robust stack to handle rasterization, PDF serialization, and IT utility execution. This toolkit provides the necessary abstractions to manipulate system state at the binary level."
}, },
]; ];

View file

@ -1,113 +1,184 @@
@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'); @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root { :root {
--bg-primary: #ffffff; --font-mono: 'IBM Plex Mono', monospace;
--cell-size: 9vw;
/* Light theme (default) */
--bg-cream: #ffffff;
--bg-board: #ffffff;
--text-primary: #000000; --text-primary: #000000;
--text-secondary: #6b7280; --text-secondary: #666666;
--accent: #3147ba; --border-color: #000000;
--accent-soft: rgba(49, 71, 186, 0.05); --cell-default: #c0c0c0;
--border-color: #e5e7eb;
} }
@layer base { [data-theme="dark"] {
body { --bg-cream: #1a1a1a;
@apply bg-white text-black antialiased font-serif; --bg-board: #222222;
background-image: --text-primary: #ffffff;
linear-gradient(var(--accent-soft) 1px, transparent 1px), --text-secondary: #888888;
linear-gradient(90deg, var(--accent-soft) 1px, transparent 1px); --border-color: #444444;
background-size: 24px 24px; --cell-default: #333333;
} }
h1, h2, h3, .font-pixel { * { margin: 0; padding: 0; box-sizing: border-box; }
font-family: 'Silkscreen', cursive;
}
p, .font-serif { html, body {
font-family: 'PT Serif', serif; width: 100%;
} overflow-x: hidden;
}
code, pre, .font-mono { body {
font-family: 'IBM Plex Mono', monospace; font-family: var(--font-mono);
background: var(--bg-cream);
}
a { color: inherit; text-decoration: none; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: var(--bg-cream);
border-bottom: 2px solid var(--border-color);
}
.header-name { font-size: 14px; font-weight: 500; }
.header-nav a { font-size: 10px; color: var(--text-secondary); margin-left: 16px; }
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
padding: 4px 8px;
font-size: 10px;
font-family: inherit;
cursor: pointer;
}
.tetris-board {
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(8, 1fr);
align-content: end;
gap: 0;
width: 100%;
max-width: 500px;
margin: 10px auto;
padding: 0 10px;
background: var(--bg-board);
contain: layout;
}
@media (max-width: 400px) {
.tetris-board {
gap: 0;
padding: 0 4px;
}
.tetris-piece {
border: none !important;
}
.header-tagline {
display: none;
} }
} }
/* Technical Layout Utilities */ @media (max-width: 500px) {
.ms-container { .header-tagline {
@apply max-w-[1100px] mx-auto px-6 md:px-12; display: none;
}
} }
/* Dropped Cap */ @media (max-width: 500px) {
.drop-cap::first-letter { .footer {
@apply text-6xl font-bold float-left mr-3 leading-[0.8] mt-2; width: 100% !important;
font-family: 'PT Serif', serif; max-width: 100%;
padding: 8px 16px;
}
} }
/* Wavy Highlight (Refined for Pixel Perfect) */ html, body {
.ms-highlight { width: 100%;
@apply relative inline-block; overflow-x: hidden;
text-decoration: underline wavy var(--accent); height: 100%;
text-underline-offset: 4px;
} }
/* Dot Grid Divider */ body {
.dot-divider { font-family: var(--font-mono);
@apply w-full h-8 mb-4; background: var(--bg-cream);
background-image: radial-gradient(circle, #e5e7eb 1.5px, transparent 1.5px);
background-size: 16px 16px;
} }
/* Blueprint Image Container */ .tetris-piece {
.blueprint-container { display: block;
@apply relative border border-gray-100 bg-white p-2 shadow-sm transition-all duration-500; width: 100%;
background-image: height: 100%;
linear-gradient(var(--accent-soft) 1px, transparent 1px), transition: background 0.15s ease;
linear-gradient(90deg, var(--accent-soft) 1px, transparent 1px); opacity: 0;
background-size: 16px 16px; animation: dropIn 0.5s ease-out forwards;
animation-fill-mode: forwards;
border: none !important;
outline: none !important;
box-shadow: none !important;
-webkit-border-radius: 0;
border-radius: 0;
} }
.blueprint-container img { .tetris-piece * {
@apply mix-blend-multiply opacity-90; border: none !important;
outline: none !important;
box-shadow: none !important;
} }
.manual-text { .tetris-piece.show {
@apply text-[17px] md:text-[18px] leading-[1.6] text-gray-900; animation: dropIn 0.5s ease-out forwards;
animation-fill-mode: forwards;
} }
/* Card Styling (Technical Labels) */ @keyframes colorBlink {
.tech-label { 0%, 100% {
@apply border-2 border-gray-100 bg-white p-4 md:p-5 transition-all duration-300; filter: brightness(1);
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.05); }
50% {
filter: brightness(1.25) saturate(1.1);
}
} }
.tech-label:hover { @media (max-width: 500px) {
@apply border-blue-600 shadow-blue-100 bg-blue-50/10; @keyframes colorBlink {
box-shadow: 6px 6px 0px var(--accent-soft); 0%, 100% {
transform: translate(-3px, -3px); filter: brightness(1);
}
50% {
filter: brightness(1.08) saturate(1.02);
}
}
} }
/* Vertical Text Helper */ @keyframes dropIn {
.vertical-text { 0% {
writing-mode: vertical-rl; opacity: 0;
}
100% {
opacity: 1;
}
} }
/* Custom Scrollbar */ @keyframes colorBlink {
::-webkit-scrollbar { 0%, 100% {
width: 10px; filter: brightness(1);
}
50% {
filter: brightness(1.3);
}
} }
::-webkit-scrollbar-track { .footer {
background: white; padding: 10px 20px;
} font-size: 10px;
color: var(--text-secondary);
::-webkit-scrollbar-thumb { width: calc(10 * var(--cell-size));
background: var(--accent-soft); margin: 0 auto;
border: 4px solid white; text-align: center;
} margin-top: auto;
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
} }

View file

@ -1,28 +1,11 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",
], ],
theme: { theme: {
extend: { 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: [], plugins: [],
} }

View file

@ -4,7 +4,14 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: './', base: '/',
server: {
host: '127.0.0.1',
port: 5173,
},
preview: {
port: 4173,
},
build: { build: {
sourcemap: false, sourcemap: false,
}, },