feat: implement customizable Bitcoin logo with 3D rotation and interactive controls
All checks were successful
Deploy to Production / deploy (push) Successful in 8s

This commit is contained in:
2026-03-03 09:12:50 +00:00
commit 2b6e9077c7
17 changed files with 2121 additions and 0 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = true

View File

@@ -0,0 +1,19 @@
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build and deploy with Docker Compose
run: |
# Sprzątaj TYLKO swój projekt
docker compose down --remove-orphans || true
docker compose up -d --build

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?

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Stage 1: Build the Vue application
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
docker-compose.yml Normal file
View File

@@ -0,0 +1,13 @@
version: '3'
services:
bitcoin-logo:
build: .
ports:
- "3003:80"
restart: always
networks:
- npm_public
networks:
npm_public:
external: true

16
index.html Normal file
View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/bitcoin-btc-logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>₿ Bitcoin 3D - Premium SVG Animation</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

15
nginx.conf Normal file
View File

@@ -0,0 +1,15 @@
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

1334
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "bitcoin-logo",
"private": true,
"version": "1.0.0",
"description": "Premium 3D SVG rotating Bitcoin logo",
"author": "Grzegorz Kucmierz",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"vite": "^7.3.1"
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
viewBox="0 0 4091.27 4091.73"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1421344023328">
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

317
src/App.vue Normal file
View File

@@ -0,0 +1,317 @@
<script setup>
import { ref } from 'vue';
import BitcoinLogo from './components/BitcoinLogo.vue'
// Modes: 'auto', 'interactive', 'controlled'
const activeMode = ref('interactive');
const activeEasing = ref('linear');
const activeDuration = ref(4);
const activeFriction = ref(0.98);
const activeSymbolColor = ref('#ffffff');
const activeSize = ref(250);
const activeStrokeWidth = ref(5);
const externalRotation = ref(0);
const easingOptions = [
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'cubic-bezier(0.68, -0.55, 0.265, 1.55)' // bouncy
];
const handleRotationUpdate = (val) => {
if (activeMode.value !== 'controlled') {
externalRotation.value = val;
}
};
</script>
<template>
<div class="app-container">
<div class="glass-card">
<header>
<h1>Bitcoin 3D</h1>
<p>Premium 3D Bitcoin logo with multiple rotation modes.</p>
</header>
<main>
<BitcoinLogo
:mode="activeMode"
:easing="activeEasing"
:duration="activeDuration"
:friction="activeFriction"
:symbolColor="activeSymbolColor"
:size="activeSize"
:strokeWidth="activeStrokeWidth"
v-model:rotation="externalRotation"
@update:rotation="handleRotationUpdate"
/>
</main>
<footer>
<div class="global-controls">
<div class="color-control">
<span class="label">SYMBOL COLOR</span>
<input type="color" v-model="activeSymbolColor">
</div>
<div class="size-control">
<span class="label">SIZE</span>
<input type="range" v-model.number="activeSize" min="50" max="400" step="1">
<span class="value">{{ activeSize }}px</span>
</div>
<div class="size-control">
<span class="label">STROKE</span>
<input type="range" v-model.number="activeStrokeWidth" min="0" max="20" step="0.5">
<span class="value">{{ activeStrokeWidth }}px</span>
</div>
</div>
<div class="mode-selector">
<label
v-for="mode in ['auto', 'interactive', 'controlled']"
:key="mode"
:class="{ active: activeMode === mode }"
>
<input type="radio" :value="mode" v-model="activeMode">
{{ mode.toUpperCase() }}
</label>
</div>
<div v-if="activeMode === 'auto'" class="control-panel">
<div class="duration-control">
<span class="label">SPEED</span>
<input type="range" v-model.number="activeDuration" min="0.5" max="10" step="0.1">
<span class="value">{{ activeDuration }}s</span>
</div>
<select v-model="activeEasing" class="easing-select">
<option v-for="option in easingOptions" :key="option" :value="option">
{{ option === 'cubic-bezier(0.68, -0.55, 0.265, 1.55)' ? 'BOUNCY' : option.toUpperCase() }}
</option>
</select>
</div>
<div v-if="activeMode === 'interactive'" class="control-panel">
<div class="duration-control">
<span class="label">INERTIA</span>
<input type="range" v-model.number="activeFriction" min="0.8" max="0.999" step="0.001">
<span class="value">{{ (activeFriction * 100).toFixed(1) }}%</span>
</div>
</div>
<div v-if="activeMode === 'controlled'" class="control-panel">
<input type="range" v-model.number="externalRotation" min="0" max="360" step="1">
<span class="value">{{ externalRotation }}°</span>
</div>
<div class="badges">
<div class="badge">Vue 3</div>
<div class="badge">SVG 3D</div>
</div>
</footer>
</div>
</div>
</template>
<style scoped>
.app-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
main {
margin: 2rem 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
position: relative;
}
header h1 {
margin-top: 0;
}
footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
margin-top: 2rem;
}
.global-controls {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
background: rgba(255, 255, 255, 0.03);
padding: 1rem 2rem;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.05);
margin-bottom: 0.5rem;
}
.color-control {
display: flex;
align-items: center;
gap: 12px;
}
.color-control .label {
font-size: 0.7rem;
font-weight: 800;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.05em;
}
.color-control input[type="color"] {
appearance: none;
-webkit-appearance: none;
border: none;
width: 32px;
height: 32px;
cursor: pointer;
background: none;
padding: 0;
}
.color-control input[type="color"]::-webkit-color-swatch {
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.size-control {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 200px;
}
.size-control .label {
font-size: 0.7rem;
font-weight: 800;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.05em;
}
.size-control input[type="range"] {
flex: 1;
accent-color: #f7931a;
height: 4px;
}
.size-control .value {
font-family: monospace;
min-width: 45px;
font-size: 0.85rem;
color: #f7931a;
}
.mode-selector {
display: flex;
background: rgba(255, 255, 255, 0.05);
padding: 4px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mode-selector label {
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
color: rgba(255, 255, 255, 0.6);
}
.mode-selector label.active {
background: #f7931a;
color: white;
box-shadow: 0 4px 12px rgba(247, 147, 26, 0.3);
}
.mode-selector input {
display: none;
}
.control-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
max-width: 300px;
}
.duration-control {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.duration-control .label {
font-size: 0.7rem;
font-weight: 800;
color: rgba(255, 255, 255, 0.4);
min-width: 50px;
}
.control-panel input[type="range"] {
flex: 1;
accent-color: #f7931a;
height: 4px;
border-radius: 2px;
}
.control-panel .value {
font-family: monospace;
min-width: 45px;
font-size: 0.85rem;
color: #f7931a;
}
.easing-select {
width: 100%;
padding: 8px 12px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
font-weight: 600;
appearance: none;
text-align: center;
}
.easing-select:focus {
outline: none;
border-color: #f7931a;
}
.badges {
display: flex;
gap: 1rem;
}
.badge {
background: rgba(255, 255, 255, 0.1);
padding: 0.5rem 1rem;
border-radius: 99px;
font-size: 0.875rem;
font-weight: 600;
color: #fbbf24;
border: 1px solid rgba(251, 191, 36, 0.2);
}
@media (max-width: 640px) {
h1 { font-size: 2.5rem; }
.glass-card { padding: 2rem; margin: 1rem; }
}
</style>

View File

@@ -0,0 +1,221 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
const FIX_SHOWING_THROUGH = '0.5px';
const props = defineProps({
// Modes: 'auto', 'interactive', 'controlled'
mode: {
type: String,
default: 'interactive'
},
easing: {
type: String,
default: 'linear'
},
duration: {
type: Number,
default: 4
},
friction: {
type: Number,
default: 0.98
},
symbolColor: {
type: String,
default: '#fff'
},
size: {
type: Number,
default: 200
},
strokeWidth: {
type: Number,
default: 5
},
// Used only in 'controlled' mode
rotation: {
type: Number,
default: 0
}
});
const emit = defineEmits(['update:rotation']);
// Internal state for 'interactive' mode
const internalRotation = ref(0);
const velocity = ref(2);
const isDragging = ref(false);
const lastMouseX = ref(0);
// Final rotation value used in CSS
const displayRotation = computed(() => {
if (props.mode === 'controlled') return props.rotation;
return internalRotation.value;
});
const rotationStyle = computed(() => `${displayRotation.value}deg`);
// Drag handlers
const handleStart = (e) => {
if (props.mode !== 'interactive') return;
isDragging.value = true;
lastMouseX.value = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
};
const handleMove = (e) => {
if (!isDragging.value || props.mode !== 'interactive') return;
const currentX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
const deltaX = currentX - lastMouseX.value;
internalRotation.value += deltaX * 0.5;
velocity.value = deltaX * 0.5;
lastMouseX.value = currentX;
emit('update:rotation', internalRotation.value);
};
const handleEnd = () => {
isDragging.value = false;
};
// Animation loop
let rafId = null;
const animate = () => {
if (props.mode === 'interactive' && !isDragging.value) {
internalRotation.value += velocity.value;
velocity.value *= props.friction;
emit('update:rotation', internalRotation.value);
}
rafId = requestAnimationFrame(animate);
};
onMounted(() => {
rafId = requestAnimationFrame(animate);
window.addEventListener('mousemove', handleMove);
window.addEventListener('mouseup', handleEnd);
window.addEventListener('touchmove', handleMove, { passive: false });
window.addEventListener('touchend', handleEnd);
});
onUnmounted(() => {
cancelAnimationFrame(rafId);
window.removeEventListener('mousemove', handleMove);
window.removeEventListener('mouseup', handleEnd);
window.removeEventListener('touchmove', handleMove);
window.removeEventListener('touchend', handleEnd);
});
// Sync internal rotation if external control changes
watch(() => props.rotation, (newVal) => {
if (props.mode === 'controlled') {
internalRotation.value = newVal;
}
});
const coinShape = 'M98.4946664 62.0964188c-6.6779856,26.7864844 -33.808340799999996,43.0882088 -60.5980024,36.4085124 -26.778419200000002,-6.6779856 -43.0801436,-33.8100516 -36.3992252,-60.594336399999996 6.6750528000000005,-26.789417200000003 33.805408,-43.0923636 60.58676,-36.414378 26.787706399999998,6.6779856 43.0884532,33.8129844 36.4097344,60.6006908l0.0004888 -0.0004888z';
const symbolShape = 'M72.0434988 42.8770472c0.9951968,-6.6540344 -4.0707264,-10.2308284 -10.998,-12.6169056l2.247258 -9.013472 -5.4867799999999995 -1.3671735999999999 -2.1876244000000002 8.7761596c-1.4424488,-0.3597568 -2.9237572,-0.6987396 -4.3960228,-1.0347896l2.2035104 -8.8340824 -5.4833584 -1.3671735999999999 -2.24848 9.0105392c-1.1936496,-0.2717728 -2.3660364,-0.5403684 -3.5034739999999998,-0.8233836l0.0063544000000000005 -0.028350399999999998 -7.566379599999999 -1.8894564 -1.4595567999999999 5.8602232c0,0 4.0707264,0.9331192 3.984942,0.9905532 2.2218404,0.5545436 2.623634,2.0253428 2.5569128,3.1911308l-2.5598456 10.268466c0.1529944,0.0388596 0.3514472,0.0950716 0.5704296,0.1830556 -0.1830556,-0.0454584 -0.3778424,-0.0950716 -0.5799612,-0.1434628l-3.5880364 14.384650800000001c-0.2715284,0.6750328 -0.9607364,1.6880707999999998 -2.5141428,1.3033852 0.05499,0.07967439999999999 -3.9878747999999997,-0.9951968 -3.9878747999999997,-0.9951968l-2.7240824 6.280591200000001 7.140146 1.7799652c1.328314,0.3331172 2.6299884,0.6816316 3.9118664,1.009372l-2.270476 9.1168532 5.4804256 1.3671735999999999 2.24848 -9.0200708c1.4971944,0.4064372 2.9501524,0.7813468 4.3725604,1.1347492l-2.2409035999999998 8.9775452 5.4870244 1.3671735999999999 2.2702316 -9.0997452c9.3561208,1.770678 16.391174799999998,1.0567856 19.3523252,-7.4058088 2.3860772,-6.8133832 -0.1187784,-10.743335199999999 -5.0409944,-13.306113600000002 3.5851036,-0.8268051999999999 6.2854792,-3.1847764 7.0054815999999995,-8.0556684l-0.0017108000000000002 -0.001222zm-12.536009199999999 17.5787144c-1.6956471999999998,6.8133832 -13.1672944,3.1302752000000003 -16.886573600000002,2.2066876l3.0129632 -12.078248c3.7190347999999998,0.9284756000000001 15.645754799999999,2.7658748 13.873854799999998,9.8715604zm1.6968692 -17.677452c-1.5468076,6.1974952000000005 -11.0947824,3.04889 -14.192063600000001,2.2768303999999997l2.7316588 -10.9542524c3.0972812000000003,0.7720596 13.071734000000001,2.2130419999999997 11.4608936,8.677422l-0.0004888 0z';
</script>
<template>
<div
class="logo-container"
@mousedown="handleStart"
@touchstart="handleStart"
>
<div
class="coin"
:class="{
'auto-rotate': props.mode === 'auto',
'dragging': isDragging
}"
>
<div class="side heads">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 100">
<path fill="#F7931A" fill-rule="nonzero" :stroke="symbolColor" :stroke-width="strokeWidth" :d="coinShape"></path>
<path :fill="symbolColor" fill-rule="nonzero" :d="symbolShape"></path>
</svg>
</div>
<div class="side tails">
<svg xmlns="http://www.w3.org/2000/svg" class="svg_back" width="100%" height="100%" viewBox="0 0 100 100">
<path fill="#F7931A" fill-rule="nonzero" :stroke="symbolColor" :stroke-width="strokeWidth" :d="coinShape"></path>
<path :fill="symbolColor" fill-rule="nonzero" :d="symbolShape"></path>
</svg>
</div>
</div>
</div>
</template>
<style scoped>
.logo-container {
font-size: v-bind("props.size + 'px'");
width: 1em;
height: 1em;
margin: auto;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background: rgba(255, 0, 0, 0.2); Bounding rect debug */
display: flex;
justify-content: center;
align-items: center;
cursor: v-bind("props.mode === 'interactive' ? (isDragging ? 'grabbing' : 'grab') : 'default'");
user-select: none;
touch-action: v-bind("props.mode === 'interactive' ? 'none' : 'auto'");
}
.coin {
width: 0.1em;
height: 1em;
background: linear-gradient(#faa504, #141001);
transform: rotateY(v-bind(rotationStyle));
transform-style: preserve-3d;
position: relative;
}
.coin.auto-rotate {
animation: rotate v-bind("props.duration + 's'") infinite v-bind("props.easing");
}
.coin .side, .coin:before, .coin:after {
content: "";
position: absolute;
width: 1em;
height: 1em;
overflow: hidden;
border-radius: 50%;
right: calc(-0.4em + v-bind(FIX_SHOWING_THROUGH));
text-align: center;
line-height: 1;
transform: rotateY(-90deg);
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.coin .tails, .coin:after {
left: calc(-0.4em + v-bind(FIX_SHOWING_THROUGH));
transform: rotateY(90deg);
}
.coin:before, .coin:after {
background: linear-gradient(#faa504, #141001);
backface-visibility: hidden;
transform: rotateY(90deg);
}
.coin:after {
transform: rotateY(-90deg);
}
.svg_back {
transform: scaleX(-1);
}
@keyframes rotate {
0% { transform: rotateY(90deg); }
100% { transform: rotateY(450deg); }
}
</style>

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

65
src/style.css Normal file
View File

@@ -0,0 +1,65 @@
:root {
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: dark;
color: rgba(255, 255, 255, 0.95);
background-color: #0f172a;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
/* Center vertically if content is small */
min-width: 320px;
min-height: 100vh;
background: radial-gradient(circle at center, #1e293b 0%, #0f172a 100%);
overflow-y: auto;
/* Ensure vertical scroll is allowed */
}
#app {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
h1 {
font-size: 4rem;
line-height: 1;
font-weight: 800;
margin-bottom: 0.5rem;
background: linear-gradient(to right, #f59e0b, #fbbf24);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
filter: drop-shadow(0 0 10px rgba(245, 158, 11, 0.3));
}
p {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 3rem;
}
.glass-card {
background: rgba(30, 41, 59, 0.5);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24px;
padding: 3rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
display: inline-block;
}

15
vite.config.js Normal file
View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
host: true,
allowedHosts: true,
headers: {
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
}
}
})