dark theme fix, responsive layout, grouped blinking effect
This commit is contained in:
parent
5c177d9d43
commit
8509500680
2 changed files with 112 additions and 107 deletions
175
src/App.jsx
175
src/App.jsx
|
|
@ -24,23 +24,44 @@ function TetrisPiece({ piece, isDark }) {
|
||||||
setShowText(false);
|
setShowText(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const bgColor = piece.label === 'cv' ? piece.color : piece.color;
|
const bgColor = piece.label === 'cv'
|
||||||
|
? piece.color
|
||||||
|
: (isDark ? piece.colorDark : piece.color);
|
||||||
const rowDelay = piece.startY * 4;
|
const rowDelay = piece.startY * 4;
|
||||||
|
const textColor = '#ffffff';
|
||||||
|
const isStatic = piece.label === 'cv' || piece.featured;
|
||||||
|
|
||||||
const style = {
|
const containerStyle = {
|
||||||
gridColumn: `${piece.startX + 1} / span ${piece.w}`,
|
gridColumn: `${piece.startX + 1} / span ${piece.w}`,
|
||||||
gridRow: `${piece.startY + 1} / span ${piece.h}`,
|
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',
|
position: 'relative',
|
||||||
animation: `dropIn 0.5s ease-out forwards, colorBlink 5s ease-in-out ${rowDelay}s infinite`,
|
cursor: piece.link ? 'pointer' : 'default',
|
||||||
animationFillMode: 'forwards, none',
|
animation: isStatic
|
||||||
|
? `dropIn 0.5s ease-out forwards`
|
||||||
|
: `dropIn 0.5s ease-out forwards, blink 3s ease-in-out ${rowDelay}s infinite`,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
};
|
||||||
|
|
||||||
|
const bgStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
backgroundColor: color || bgColor,
|
||||||
|
transition: 'background-color 0.15s ease',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const textStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '8px',
|
||||||
|
left: '8px',
|
||||||
|
fontSize: 'clamp(8px, 1.5vw, 12px)',
|
||||||
|
fontWeight: '500',
|
||||||
|
textTransform: 'lowercase',
|
||||||
|
color: textColor,
|
||||||
|
opacity: alwaysShowText ? 1 : (showText ? 1 : 0),
|
||||||
|
transition: 'opacity 0.15s ease, color 0.15s ease',
|
||||||
|
zIndex: 1,
|
||||||
|
pointerEvents: 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
const className = 'tetris-piece show';
|
const className = 'tetris-piece show';
|
||||||
|
|
@ -48,62 +69,35 @@ function TetrisPiece({ piece, isDark }) {
|
||||||
// CV block shows looping video
|
// CV block shows looping video
|
||||||
if (isCV) {
|
if (isCV) {
|
||||||
return (
|
return (
|
||||||
<a
|
<div style={containerStyle} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
href={piece.link}
|
<a
|
||||||
target="_blank"
|
href={piece.link}
|
||||||
rel="noopener"
|
target="_blank"
|
||||||
className={className}
|
rel="noopener"
|
||||||
style={style}
|
style={{...bgStyle, display: 'flex', alignItems: 'center', justifyContent: 'center'}}
|
||||||
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
|
||||||
</video>
|
autoPlay
|
||||||
<span style={{
|
loop
|
||||||
position: 'absolute',
|
muted
|
||||||
bottom: '8px',
|
playsInline
|
||||||
left: '8px',
|
style={{
|
||||||
fontSize: 'clamp(8px, 1.5vw, 12px)',
|
width: '100%',
|
||||||
fontWeight: '500',
|
height: '100%',
|
||||||
textTransform: 'lowercase',
|
objectFit: 'cover',
|
||||||
color: '#fff',
|
opacity: 1,
|
||||||
opacity: 1,
|
border: 'none',
|
||||||
transition: 'opacity 0.15s ease',
|
outline: 'none',
|
||||||
}}>
|
}}
|
||||||
{piece.label}
|
>
|
||||||
</span>
|
<source src="/cv-video.mp4" type="video/mp4" />
|
||||||
</a>
|
</video>
|
||||||
|
</a>
|
||||||
|
<span style={{...textStyle, color: '#fff'}}>{piece.label}</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = {
|
const wrapperStyle = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -112,29 +106,32 @@ function TetrisPiece({ piece, isDark }) {
|
||||||
|
|
||||||
if (piece.link) {
|
if (piece.link) {
|
||||||
return (
|
return (
|
||||||
<a
|
<div
|
||||||
href={piece.link}
|
style={containerStyle}
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
<div style={wrapperStyle}>
|
<a
|
||||||
<span style={textStyle}>{piece.label}</span>
|
href={piece.link}
|
||||||
</div>
|
target="_blank"
|
||||||
</a>
|
rel="noopener"
|
||||||
|
style={{...bgStyle, display: 'flex', alignItems: 'center', justifyContent: 'center'}}
|
||||||
|
>
|
||||||
|
<div style={wrapperStyle}>
|
||||||
|
<span style={textStyle}>{piece.label}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={className}
|
style={containerStyle}
|
||||||
style={style}
|
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
|
<div style={bgStyle}></div>
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
<span style={{...textStyle, cursor: 'default'}}>{piece.label}</span>
|
<span style={{...textStyle, cursor: 'default'}}>{piece.label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -151,18 +148,18 @@ function App() {
|
||||||
|
|
||||||
const shuffleLayout = () => {
|
const shuffleLayout = () => {
|
||||||
const items = [
|
const items = [
|
||||||
{ w: 4, h: 1, label: 'rm8pfix', link: 'https://rm8pfix.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#00BCD4' },
|
{ w: 4, h: 1, label: 'rm8pfix', link: 'https://rm8pfix.khoavo.myds.me', color: '#E8E8E8', colorDark: '#333333', hoverColor: '#00BCD4' },
|
||||||
{ w: 2, h: 2, label: 'netflix', link: 'https://nf.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FF9800' },
|
{ w: 2, h: 2, label: 'netflix', link: 'https://nf.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', 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: 'portfolio', link: 'https://portfolio.khoavo.myds.me', featured: true, color: '#4A7BC7', colorDark: '#2d4a7c', hoverColor: '#2196F3' },
|
||||||
{ w: 2, h: 3, label: 'cv', link: 'https://cv.khoavo.myds.me', color: '#616161', hoverColor: '#424242' },
|
{ w: 2, h: 3, label: 'cv', link: 'https://cv.khoavo.myds.me', color: '#616161', colorDark: '#424242', hoverColor: '#424242' },
|
||||||
{ w: 2, h: 2, label: 'youtube', link: 'https://ut.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FF5722' },
|
{ w: 2, h: 2, label: 'youtube', link: 'https://ut.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#FF5722' },
|
||||||
{ w: 2, h: 2, label: 'tiktok', link: 'https://tt.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#9C27B0' },
|
{ w: 2, h: 2, label: 'tiktok', link: 'https://tt.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#9C27B0' },
|
||||||
{ w: 3, h: 2, label: 'spotify', link: 'https://sp.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#4CAF50' },
|
{ w: 3, h: 2, label: 'spotify', link: 'https://sp.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#4CAF50' },
|
||||||
{ w: 3, h: 2, label: 'tools', link: 'https://it.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#FFC107' },
|
{ w: 3, h: 2, label: 'tools', link: 'https://it.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#FFC107' },
|
||||||
{ w: 2, h: 2, label: 'save', link: 'https://save.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#E91E63' },
|
{ w: 2, h: 2, label: 'save', link: 'https://save.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#E91E63' },
|
||||||
{ w: 2, h: 2, label: 'free', link: 'https://free.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#00BCD4' },
|
{ w: 2, h: 2, label: 'free', link: 'https://free.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#00BCD4' },
|
||||||
{ w: 2, h: 2, label: 'jpg', link: 'https://jpg.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#673AB7' },
|
{ w: 2, h: 2, label: 'jpg', link: 'https://jpg.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#673AB7' },
|
||||||
{ w: 2, h: 2, label: 'pdf', link: 'https://pdf.khoavo.myds.me', color: '#E8E8E8', hoverColor: '#795548' },
|
{ w: 2, h: 2, label: 'pdf', link: 'https://pdf.khoavo.myds.me', color: '#E8E8E8', colorDark: '#3a3a3a', hoverColor: '#795548' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const random = (s) => {
|
const random = (s) => {
|
||||||
|
|
@ -235,7 +232,7 @@ const layout = shuffleLayout();
|
||||||
const borderColor = isDark ? '#444' : '#000';
|
const borderColor = isDark ? '#444' : '#000';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100vh', background: bg, padding: '0 10px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div style={{ minHeight: '100vh', height: '100%', background: bg, padding: '0 10px', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
<header style={{
|
<header style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|
|
||||||
|
|
@ -60,15 +60,23 @@ a { color: inherit; text-decoration: none; }
|
||||||
.tetris-board {
|
.tetris-board {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(10, 1fr);
|
grid-template-columns: repeat(10, 1fr);
|
||||||
grid-template-rows: repeat(8, 1fr);
|
grid-template-rows: repeat(10, 1fr);
|
||||||
align-content: end;
|
align-content: end;
|
||||||
gap: 0;
|
gap: 1px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
background: var(--bg-board);
|
background: var(--bg-board);
|
||||||
contain: layout;
|
contain: layout;
|
||||||
|
aspect-ratio: 10 / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.tetris-board {
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
gap: 0.5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 400px) {
|
@media (max-width: 400px) {
|
||||||
|
|
@ -98,6 +106,15 @@ a { color: inherit; text-decoration: none; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.header-name {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
@ -135,22 +152,22 @@ body {
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes colorBlink {
|
@keyframes blink {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
filter: brightness(1);
|
opacity: 1;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
filter: brightness(1.25) saturate(1.1);
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
@keyframes colorBlink {
|
@keyframes blink {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
filter: brightness(1);
|
opacity: 1;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
filter: brightness(1.08) saturate(1.02);
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,15 +181,6 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes colorBlink {
|
|
||||||
0%, 100% {
|
|
||||||
filter: brightness(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
filter: brightness(1.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue