Landing Page

This commit is contained in:
David Haz
2025-07-08 12:39:14 +03:00
parent fa9392fa47
commit 9ddb731258
41 changed files with 4584 additions and 8 deletions

View File

@@ -0,0 +1,662 @@
.features-section {
position: relative;
margin-top: 12em;
padding: 8rem 2rem 4em;
padding-bottom: 0 !important;
z-index: 22;
user-select: none;
}
.features-container {
max-width: 1200px;
margin: 0 auto;
}
.features-header {
text-align: center;
margin-bottom: 4rem;
}
.features-title {
font-size: 4rem;
font-weight: 600;
letter-spacing: -2px;
color: #fff;
margin-bottom: .2rem;
background: linear-gradient(135deg, #fff 0%, #60fa89 20%, #55f788 40%, #00ff62 60%, #55f799 80%, #fff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
animation: gradientShift 4s ease-in-out infinite;
display: inline-block;
white-space: nowrap;
}
.features-title * {
background: inherit;
background-size: inherit;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: inherit;
}
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
.features-subtitle {
font-size: 1.2rem;
color: #fff;
text-shadow:
0 0 2px rgba(255, 255, 255, 0.1),
0 0 4px rgba(255, 255, 255, 0.3),
0 0 8px rgba(255, 255, 255, 0.4),
0 0 136px rgba(0, 255, 98, 0.9);
font-weight: 400;
margin: 0;
text-align: center;
}
@media (max-width: 768px) {
.features-title {
font-size: 2.5rem;
}
.features-subtitle {
font-size: 1.1rem;
}
.features-header {
margin-bottom: 3rem;
}
}
@media (max-width: 480px) {
.features-title {
font-size: 2rem;
}
.features-subtitle {
font-size: 1rem;
}
}
.bento-grid {
max-width: 1200px;
display: grid;
gap: 1.5em;
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
@media (min-width: 480px) and (max-width: 767px) {
.bento-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.25em;
}
.card1 {
grid-column: 1 / 3;
grid-row: 1 / 2;
}
.card2 {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
.card4 {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 50rem) {
.bento-grid {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, auto);
gap: 1.5em;
}
.card1 {
grid-column: 1 / 3;
grid-row: 1 / 2;
}
.card2 {
grid-column: 3 / 5;
grid-row: 1 / 3;
}
.card4 {
grid-column: 1 / 3;
grid-row: 2 / 3;
}
}
@media (min-width: 768px) and (max-width: 49.99rem) {
.bento-grid {
grid-template-columns: repeat(3, 1fr);
gap: 1.25em;
}
.card1 {
grid-column: 1 / 3;
grid-row: 1 / 2;
}
.card2 {
grid-column: 3 / 4;
grid-row: 1 / 3;
}
.card4 {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
}
.feature-card {
user-select: none;
background: #0e0e0e;
border: 1px solid rgba(148, 184, 154, 0.2);
border-radius: 16px;
padding: 2rem;
text-align: left;
text-decoration: none;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
height: 100%;
min-height: 220px;
--glow-x: 50%;
--glow-y: 50%;
--glow-intensity: 0;
}
.feature-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(163, 148, 184, 0.3), transparent);
transition: opacity 0.3s ease;
opacity: 0;
}
.feature-card::after {
content: '';
position: absolute;
inset: 0;
padding: 1px;
background: radial-gradient(200px circle at var(--glow-x) var(--glow-y),
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
transparent 60%);
border-radius: inherit;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: subtract;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
transition: opacity 0.3s ease;
}
.feature-card:hover {
box-shadow: 0 4px 40px -15px rgba(46, 24, 78, 0.4) !important;
background: #07160a;
}
.feature-card:hover::before {
opacity: 1;
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 2rem;
display: flex;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
}
.feature-card h3 {
font-size: 1rem;
font-weight: 600;
color: #fff;
margin-bottom: 0.5rem;
letter-spacing: -0.01em;
flex-shrink: 0;
position: relative;
z-index: 10;
}
.feature-card h2 {
font-size: 6rem;
position: relative;
top: 22px;
margin: 0;
font-weight: 800;
background: linear-gradient(135deg, #fff 0%, #60fa7f 20%, #55f783 40%, #00ff48 60%, #58f755 80%, #fff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 4s ease-in-out infinite;
z-index: 10;
}
.feature-card p {
color: rgba(161, 148, 184, 0.9);
line-height: 1.4;
font-size: 0.95rem;
text-align: left;
max-width: 100%;
position: relative;
z-index: 10;
}
.particle-container {
position: relative;
overflow: hidden;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(0, 255, 98, 0.8);
border-radius: 50%;
pointer-events: none;
z-index: 100;
box-shadow: 0 0 6px rgba(0, 255, 68, 0.6);
}
.particle::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: rgba(0, 255, 81, 0.3);
border-radius: 50%;
z-index: -1;
}
.feature-card.particle-container {
overflow: hidden;
}
.feature-card.particle-container:hover {
box-shadow: 0 4px 20px rgba(24, 78, 42, 0.4), 0 0 30px rgba(0, 255, 76, 0.2);
background: #07160b;
}
.global-spotlight {
mix-blend-mode: screen;
will-change: transform, opacity;
z-index: 200 !important;
pointer-events: none;
}
.components-gif-wrapper {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 1;
overflow: hidden;
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card:hover .components-gif-wrapper {
opacity: 1;
}
.components-gif {
width: 100%;
height: auto;
opacity: 0.2;
mix-blend-mode: lighten;
display: block;
filter: grayscale(100%);
}
.components-gif-wrapper::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
border-radius: 0 0 8px 8px;
pointer-events: none;
z-index: 2;
}
.components-gif-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
border-radius: 8px 8px 0 0;
pointer-events: none;
z-index: 2;
}
.messages-gif-wrapper {
position: absolute;
mix-blend-mode: lighten;
top: 0.5rem;
right: 1rem;
bottom: 1rem;
width: 50%;
border-radius: 8px;
z-index: 1;
overflow: hidden;
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card:hover .messages-gif-wrapper {
opacity: 1;
}
.messages-gif {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 0;
opacity: 0.3;
display: block;
}
.messages-gif-wrapper::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
border-radius: 0 0 8px 8px;
pointer-events: none;
z-index: 2;
}
.messages-gif-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
border-radius: 8px 8px 0 0;
pointer-events: none;
z-index: 2;
}
.switch-gif-wrapper {
position: absolute;
top: 0;
right: 1rem;
width: 40%;
mix-blend-mode: lighten;
border-radius: 8px;
z-index: 1;
overflow: hidden;
opacity: 0;
transition: opacity 0.3s ease;
}
.feature-card:hover .switch-gif-wrapper {
opacity: 1;
}
.switch-gif {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: 0;
opacity: 0.3;
display: block;
}
.switch-gif-wrapper::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to top, #0e0e0e 0%, transparent 100%);
border-radius: 0 0 8px 8px;
pointer-events: none;
z-index: 2;
}
.switch-gif-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(to bottom, #0e0e0e 0%, transparent 100%);
border-radius: 8px 8px 0 0;
pointer-events: none;
z-index: 2;
}
@media (max-width: 479px) {
.features-section {
padding: 4rem 1rem 2rem;
padding-bottom: 0;
margin-top: 4em;
}
.bento-grid {
gap: 1rem;
max-width: 100%;
}
.feature-card {
min-height: 160px;
padding: 1.25rem;
border-radius: 12px;
}
.feature-icon {
font-size: 1.75rem;
margin-bottom: 0.75rem;
}
.feature-card h2 {
font-size: 4rem;
}
.feature-card h3 {
font-size: 1rem;
margin-bottom: 0.5rem;
}
.feature-card p {
font-size: 0.85rem;
line-height: 1.6;
}
.components-gif-wrapper,
.messages-gif-wrapper,
.switch-gif-wrapper {
display: none !important;
}
}
@media (max-width: 360px) {
.features-section {
padding: 2.5rem 0.5rem 1rem;
}
.bento-grid {
gap: 0.75rem;
}
.feature-card {
min-height: 150px;
padding: 1rem;
border-radius: 10px;
}
.feature-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.feature-card h2 {
font-size: 4rem;
}
.feature-card h3 {
font-size: 0.95rem;
margin-bottom: 0.4rem;
}
.feature-card p {
font-size: 0.8rem;
line-height: 1.7;
}
.components-gif-wrapper,
.messages-gif-wrapper,
.switch-gif-wrapper {
display: none !important;
}
}
@media (min-width: 768px) {
.features-section {
padding: 6rem 2rem 3rem;
margin-top: 6em;
}
.feature-card {
min-height: 200px;
padding: 1.75rem;
}
.feature-icon {
font-size: 2.25rem;
margin-bottom: 1.5rem;
}
.feature-card h3 {
font-size: 1.15rem;
}
.feature-card p {
font-size: 0.92rem;
}
.components-gif-wrapper,
.messages-gif-wrapper,
.switch-gif-wrapper {
display: none !important;
}
}
@media (min-width: 50rem) {
.features-section {
padding: 8rem 2rem 4rem;
padding-bottom: 0;
margin-top: 8em;
}
.feature-card {
min-height: 220px;
padding: 2rem;
}
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.feature-card h3 {
font-size: 2rem;
}
.feature-card p {
font-size: 0.95rem;
}
.components-gif-wrapper,
.messages-gif-wrapper,
.switch-gif-wrapper {
display: block !important;
}
}
@media (max-height: 500px) and (orientation: landscape) {
.features-section {
margin-top: 2em;
padding: 2rem 1rem;
}
.feature-card {
min-height: 140px;
padding: 1rem;
}
.feature-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.feature-card h3 {
font-size: 0.9rem;
margin-bottom: 0.3rem;
}
.feature-card p {
font-size: 0.8rem;
}
}

View File

@@ -0,0 +1,286 @@
<template>
<div class="features-section">
<div class="features-container">
<div class="features-header">
<h3 class="features-title">Zero cost, all the cool.</h3>
<p class="features-subtitle">Everything you need to add flair to your websites</p>
</div>
<GlobalSpotlight v-if="gridRef" :grid-ref="gridRef" :disable-animations="isMobile" />
<div class="bento-grid" ref="gridRef">
<ParticleCard class="feature-card card1" :disable-animations="isMobile">
<div className="messages-gif-wrapper">
<img src="/assets/messages.gif" alt="Messages animation" className="messages-gif" />
</div>
<h2>
<template v-if="isMobile">100</template>
<CountUp v-else :to="100" />%
</h2>
<h3>Free &amp; Open Source</h3>
<p>Loved by developers around the world</p>
</ParticleCard>
<ParticleCard class="feature-card card2" :disable-animations="isMobile">
<div className="components-gif-wrapper">
<img src="/assets/components.gif" alt="Components animation" className="components-gif" />
</div>
<h2>
<template v-if="isMobile">80</template>
<CountUp v-else :to="80" />+
</h2>
<h3>Curated Components</h3>
<p>Growing weekly &amp; only getting better</p>
</ParticleCard>
<ParticleCard class="feature-card card4" :disable-animations="isMobile">
<h2>
Modern
</h2>
<h3>Technologies</h3>
<p>TypeScript + Tailwind, ready to ship</p>
</ParticleCard>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, defineComponent, h } from 'vue'
import { gsap } from 'gsap'
import CountUp from '../../../content/Animations/CountUp/CountUp.vue'
import './FeatureCards.css'
const isMobile = ref(false)
const gridRef = ref<HTMLDivElement | null>(null)
const checkIsMobile = () => {
isMobile.value = window.innerWidth <= 768
}
onMounted(() => {
checkIsMobile()
window.addEventListener('resize', checkIsMobile)
})
onUnmounted(() => {
window.removeEventListener('resize', checkIsMobile)
})
const ParticleCard = defineComponent({
name: 'ParticleCard',
props: {
disableAnimations: {
type: Boolean,
default: false
}
},
setup(props, { slots }) {
const cardRef = ref<HTMLDivElement | null>(null)
const particlesRef = ref<HTMLDivElement[]>([])
const timeoutsRef = ref<number[]>([])
const isHoveredRef = ref(false)
const memoizedParticles = ref<HTMLDivElement[]>([])
const particlesInit = ref(false)
const createParticle = (x: number, y: number): HTMLDivElement => {
const el = document.createElement('div')
el.className = 'particle'
el.style.cssText = `
position:absolute;width:4px;height:4px;border-radius:50%;
background:rgba(132,0,255,1);box-shadow:0 0 6px rgba(132,0,255,.6);
pointer-events:none;z-index:100;left:${x}px;top:${y}px;
`
return el
}
const memoizeParticles = () => {
if (particlesInit.value || !cardRef.value) return
const { width, height } = cardRef.value.getBoundingClientRect()
Array.from({ length: 12 }).forEach(() => {
memoizedParticles.value.push(createParticle(Math.random() * width, Math.random() * height))
})
particlesInit.value = true
}
const clearParticles = () => {
timeoutsRef.value.forEach(clearTimeout)
timeoutsRef.value = []
particlesRef.value.forEach(p =>
gsap.to(p, {
scale: 0,
opacity: 0,
duration: 0.3,
ease: "back.in(1.7)",
onComplete: () => {
if (p.parentNode) {
p.parentNode.removeChild(p)
}
},
})
)
particlesRef.value = []
}
const animateParticles = () => {
if (!cardRef.value || !isHoveredRef.value) return
if (!particlesInit.value) memoizeParticles()
memoizedParticles.value.forEach((particle, i) => {
const id = setTimeout(() => {
if (!isHoveredRef.value || !cardRef.value) return
const clone = particle.cloneNode(true) as HTMLDivElement
cardRef.value.appendChild(clone)
particlesRef.value.push(clone)
gsap.set(clone, { scale: 0, opacity: 0 })
gsap.to(clone, { scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" })
gsap.to(clone, {
x: (Math.random() - 0.5) * 100,
y: (Math.random() - 0.5) * 100,
rotation: Math.random() * 360,
duration: 2 + Math.random() * 2,
ease: "none",
repeat: -1,
yoyo: true,
})
gsap.to(clone, { opacity: 0.3, duration: 1.5, ease: "power2.inOut", repeat: -1, yoyo: true })
}, i * 100)
timeoutsRef.value.push(id)
})
}
const handleMouseEnter = () => {
isHoveredRef.value = true
animateParticles()
}
const handleMouseLeave = () => {
isHoveredRef.value = false
clearParticles()
}
onMounted(() => {
if (props.disableAnimations || !cardRef.value) return
const node = cardRef.value
node.addEventListener('mouseenter', handleMouseEnter)
node.addEventListener('mouseleave', handleMouseLeave)
})
onUnmounted(() => {
if (cardRef.value) {
cardRef.value.removeEventListener('mouseenter', handleMouseEnter)
cardRef.value.removeEventListener('mouseleave', handleMouseLeave)
}
isHoveredRef.value = false
clearParticles()
})
return () => h('div', {
ref: cardRef,
class: 'particle-container',
style: { position: 'relative', overflow: 'hidden' }
}, slots.default?.())
}
})
const GlobalSpotlight = defineComponent({
name: 'GlobalSpotlight',
props: {
gridRef: {
type: Object,
required: true
},
disableAnimations: {
type: Boolean,
default: false
}
},
setup(props) {
const spotlightRef = ref<HTMLDivElement | null>(null)
const isInsideSectionRef = ref(false)
const handleMouseMove = (e: MouseEvent) => {
if (!spotlightRef.value || !props.gridRef.value) return
const section = props.gridRef.value.closest('.features-section')
const rect = section?.getBoundingClientRect()
const inside =
rect &&
e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom
isInsideSectionRef.value = inside
const cards = props.gridRef.value.querySelectorAll('.feature-card')
if (!inside) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
cards.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
return
}
let minDist = Infinity
const prox = 100, fade = 150
cards.forEach((card: HTMLElement) => {
const r = card.getBoundingClientRect()
const cx = r.left + r.width / 2
const cy = r.top + r.height / 2
const d = Math.hypot(e.clientX - cx, e.clientY - cy) - Math.max(r.width, r.height) / 2
const ed = Math.max(0, d)
minDist = Math.min(minDist, ed)
const rx = ((e.clientX - r.left) / r.width) * 100
const ry = ((e.clientY - r.top) / r.height) * 100
let glow = 0
if (ed <= prox) glow = 1
else if (ed <= fade) glow = (fade - ed) / (fade - prox)
card.style.setProperty('--glow-x', `${rx}%`)
card.style.setProperty('--glow-y', `${ry}%`)
card.style.setProperty('--glow-intensity', String(glow))
})
gsap.to(spotlightRef.value, { left: e.clientX, top: e.clientY, duration: 0.1, ease: "power2.out" })
const target = minDist <= prox ? 0.8 : minDist <= fade ? ((fade - minDist) / (fade - prox)) * 0.8 : 0
gsap.to(spotlightRef.value, { opacity: target, duration: target > 0 ? 0.2 : 0.5, ease: "power2.out" })
}
const handleMouseLeave = () => {
isInsideSectionRef.value = false
props.gridRef.value
?.querySelectorAll('.feature-card')
.forEach((card: HTMLElement) => card.style.setProperty('--glow-intensity', '0'))
if (spotlightRef.value) {
gsap.to(spotlightRef.value, { opacity: 0, duration: 0.3, ease: "power2.out" })
}
}
onMounted(() => {
if (props.disableAnimations || !props.gridRef?.value) return
const spotlight = document.createElement('div')
spotlight.className = 'global-spotlight'
spotlight.style.cssText = `
position:fixed;width:800px;height:800px;border-radius:50%;pointer-events:none;
background:radial-gradient(circle,rgba(132,0,255,.15) 0%,rgba(132,0,255,.08) 15%,
rgba(132,0,255,.04) 25%,rgba(132,0,255,.02) 40%,rgba(132,0,255,.01) 65%,transparent 70%);
z-index:200;opacity:0;transform:translate(-50%,-50%);mix-blend-mode:screen;
`
document.body.appendChild(spotlight)
spotlightRef.value = spotlight
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseleave', handleMouseLeave)
})
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseleave', handleMouseLeave)
if (spotlightRef.value?.parentNode) {
spotlightRef.value.parentNode.removeChild(spotlightRef.value)
}
})
return () => null
}
})
</script>