Enhance glass cards UI
Add interactive glass card component with hover glow and new mouse tracking hook; update avatar asset usage and index page to integrate vertical bento grid with dynamic glass effects. X-Lovable-Edit-ID: edt-8015897d-3f4c-4771-add5-3c9a859c0017
This commit is contained in:
commit
03591b0d41
6 changed files with 285 additions and 79 deletions
BIN
src/assets/avatar.png
Normal file
BIN
src/assets/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
38
src/components/GlassCard.tsx
Normal file
38
src/components/GlassCard.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useRef, useState, useCallback, ReactNode } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface GlassCardProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
interactive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlassCard = ({ children, className, interactive = true }: GlassCardProps) => {
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [glowPos, setGlowPos] = useState({ x: 0, y: 0 });
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!cardRef.current || !interactive) return;
|
||||||
|
const rect = cardRef.current.getBoundingClientRect();
|
||||||
|
setGlowPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
}, [interactive]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={cardRef}
|
||||||
|
className={cn('glass-card glass-card-hover rounded-2xl relative', className)}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{interactive && isHovered && (
|
||||||
|
<div
|
||||||
|
className="mouse-glow"
|
||||||
|
style={{ left: glowPos.x, top: glowPos.y, opacity: 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative z-10">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/hooks/useMousePosition.ts
Normal file
22
src/hooks/useMousePosition.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useState, useEffect, useCallback, RefObject } from 'react';
|
||||||
|
|
||||||
|
export function useMousePosition(containerRef?: RefObject<HTMLElement>) {
|
||||||
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||||
|
if (containerRef?.current) {
|
||||||
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
|
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
|
||||||
|
} else {
|
||||||
|
setPosition({ x: e.clientX, y: e.clientY });
|
||||||
|
}
|
||||||
|
}, [containerRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const target = containerRef?.current || window;
|
||||||
|
target.addEventListener('mousemove', handleMouseMove as EventListener);
|
||||||
|
return () => target.removeEventListener('mousemove', handleMouseMove as EventListener);
|
||||||
|
}, [handleMouseMove, containerRef]);
|
||||||
|
|
||||||
|
return position;
|
||||||
|
}
|
||||||
184
src/index.css
184
src/index.css
|
|
@ -2,95 +2,52 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
||||||
All colors MUST be HSL.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 220 15% 10%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 30 10% 90%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 220 12% 14%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 30 10% 90%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 220 12% 14%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 30 10% 90%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--primary: 25 95% 55%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 220 15% 8%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
--secondary: 220 12% 18%;
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary-foreground: 30 10% 85%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 220 10% 20%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 220 8% 55%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--accent: 25 90% 50%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--accent-foreground: 220 15% 8%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 220 10% 22%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 220 10% 22%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 25 95% 55%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 1rem;
|
||||||
|
|
||||||
--sidebar-background: 0 0% 98%;
|
--glass-bg: 220 12% 16%;
|
||||||
|
--glass-border: 220 10% 28%;
|
||||||
|
--glass-shine: 25 95% 55%;
|
||||||
|
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-background: 220 15% 10%;
|
||||||
|
--sidebar-foreground: 30 10% 90%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 25 95% 55%;
|
||||||
|
--sidebar-primary-foreground: 220 15% 8%;
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-accent: 220 12% 18%;
|
||||||
|
--sidebar-accent-foreground: 30 10% 90%;
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-border: 220 10% 22%;
|
||||||
|
--sidebar-ring: 25 95% 55%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
||||||
|
|
||||||
--sidebar-border: 220 13% 91%;
|
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: 222.2 84% 4.9%;
|
|
||||||
--foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--card: 222.2 84% 4.9%;
|
|
||||||
--card-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--popover: 222.2 84% 4.9%;
|
|
||||||
--popover-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--primary: 210 40% 98%;
|
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
|
||||||
|
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
|
||||||
--secondary-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
|
||||||
--destructive-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--border: 217.2 32.6% 17.5%;
|
|
||||||
--input: 217.2 32.6% 17.5%;
|
|
||||||
--ring: 212.7 26.8% 83.9%;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +57,85 @@ All colors MUST be HSL.
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-body antialiased;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.glass-card {
|
||||||
|
background: hsl(var(--glass-bg) / 0.6);
|
||||||
|
border: 1px solid hsl(var(--glass-border) / 0.4);
|
||||||
|
backdrop-filter: blur(20px) saturate(1.8);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 1px hsl(0 0% 100% / 0.05),
|
||||||
|
inset 0 -1px 1px hsl(0 0% 0% / 0.1),
|
||||||
|
0 8px 32px hsl(0 0% 0% / 0.3),
|
||||||
|
0 2px 8px hsl(0 0% 0% / 0.2);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
hsl(0 0% 100% / 0.08) 0%,
|
||||||
|
transparent 40%,
|
||||||
|
transparent 60%,
|
||||||
|
hsl(0 0% 100% / 0.03) 100%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card-hover {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
box-shadow 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card-hover:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: hsl(var(--glass-shine) / 0.3);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 1px hsl(0 0% 100% / 0.08),
|
||||||
|
0 12px 40px hsl(0 0% 0% / 0.4),
|
||||||
|
0 4px 12px hsl(var(--primary) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse-glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
hsl(var(--primary) / 0.12) 0%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-arrow {
|
||||||
|
@apply inline-block transition-transform duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-arrow-parent:hover .link-arrow {
|
||||||
|
transform: translate(3px, -3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-gradient {
|
||||||
|
background: linear-gradient(135deg, hsl(var(--primary)), hsl(30 100% 65%));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,111 @@
|
||||||
// Update this page (the content is just a fallback if you fail to update the page)
|
import { useRef } from 'react';
|
||||||
|
import { GlassCard } from '@/components/GlassCard';
|
||||||
|
import { useMousePosition } from '@/hooks/useMousePosition';
|
||||||
|
import avatar from '@/assets/avatar.png';
|
||||||
|
import { Home, MessageCircle, Rss, GitBranch, Search, ExternalLink } from 'lucide-react';
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ icon: Home, label: 'Home', href: 'https://damaj.tech/', desc: 'Main page' },
|
||||||
|
{ icon: MessageCircle, label: 'Who Am I?', href: 'https://damaj.tech/site/whoami', desc: 'About me' },
|
||||||
|
{ icon: Rss, label: 'Blog', href: 'https://damaj.tech/site/blog', desc: 'My writings' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: GitBranch, label: 'Git Server', href: 'https://git.damaj.tech/', desc: 'Self-hosted Forgejo' },
|
||||||
|
{ icon: Search, label: 'SearXNG', href: 'https://searxng.damaj.tech/', desc: 'Private search engine' },
|
||||||
|
];
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mouse = useMousePosition(containerRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
<div
|
||||||
<div className="text-center">
|
ref={containerRef}
|
||||||
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
|
className="min-h-screen bg-background relative overflow-hidden"
|
||||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
>
|
||||||
|
{/* Global mouse glow */}
|
||||||
|
<div
|
||||||
|
className="fixed pointer-events-none z-0"
|
||||||
|
style={{
|
||||||
|
left: mouse.x,
|
||||||
|
top: mouse.y,
|
||||||
|
width: 600,
|
||||||
|
height: 600,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
background: 'radial-gradient(circle, hsl(25 95% 55% / 0.06) 0%, transparent 70%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-2xl mx-auto px-4 py-12 md:py-20 flex flex-col gap-5">
|
||||||
|
{/* Avatar + Intro */}
|
||||||
|
<GlassCard className="p-6 md:p-8">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<img
|
||||||
|
src={avatar}
|
||||||
|
alt="Mohamad's avatar"
|
||||||
|
className="w-20 h-20 md:w-24 md:h-24 rounded-full ring-2 ring-primary/30"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl md:text-3xl font-heading font-bold text-foreground">
|
||||||
|
👋 Hey There!
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground mt-1 font-body">
|
||||||
|
My name is <span className="text-gradient font-semibold">Mohamad</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Welcome to my space. Feel free to look around.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
|
||||||
|
{/* Website Content */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-mono uppercase tracking-widest text-muted-foreground mb-3 px-1">
|
||||||
|
Website Content
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
{links.map((link) => (
|
||||||
|
<a key={link.label} href={link.href} target="_blank" rel="noopener noreferrer">
|
||||||
|
<GlassCard className="p-5 h-full link-arrow-parent cursor-pointer">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<link.icon className="w-5 h-5 text-primary mb-3" />
|
||||||
|
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
||||||
|
</div>
|
||||||
|
<p className="font-heading font-semibold text-foreground">{link.label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{link.desc}</p>
|
||||||
|
</GlassCard>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Self-hosted Services */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-mono uppercase tracking-widest text-muted-foreground mb-3 px-1">
|
||||||
|
Self-hosted Services
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{services.map((svc) => (
|
||||||
|
<a key={svc.label} href={svc.href} target="_blank" rel="noopener noreferrer">
|
||||||
|
<GlassCard className="p-5 h-full link-arrow-parent cursor-pointer">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<svc.icon className="w-5 h-5 text-primary mb-3" />
|
||||||
|
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground link-arrow" />
|
||||||
|
</div>
|
||||||
|
<p className="font-heading font-semibold text-foreground">{svc.label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{svc.desc}</p>
|
||||||
|
</GlassCard>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<p className="text-center text-xs text-muted-foreground font-mono mt-4">
|
||||||
|
damaj.tech
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,17 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
heading: ['"Space Grotesk"', 'sans-serif'],
|
||||||
|
body: ['"Space Grotesk"', 'sans-serif'],
|
||||||
|
mono: ['"JetBrains Mono"', 'monospace'],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
glass: {
|
||||||
|
bg: "hsl(var(--glass-bg) / <alpha-value>)",
|
||||||
|
border: "hsl(var(--glass-border) / <alpha-value>)",
|
||||||
|
shine: "hsl(var(--glass-shine) / <alpha-value>)",
|
||||||
|
},
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
input: "hsl(var(--input))",
|
input: "hsl(var(--input))",
|
||||||
ring: "hsl(var(--ring))",
|
ring: "hsl(var(--ring))",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue