Added vue frontend project, fixed swagger path
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { BarChart3, Calendar, PieChart, TrendingUp } from 'lucide-vue-next';
|
||||
|
||||
const categoryData = [
|
||||
{ name: 'Food', value: 850, color: '#f97316' },
|
||||
{ name: 'Transport', value: 420, color: '#ef4444' },
|
||||
{ name: 'Shopping', value: 680, color: '#8b5cf6' },
|
||||
{ name: 'Bills', value: 1250, color: '#3b82f6' },
|
||||
{ name: 'Others', value: 320, color: '#6366f1' },
|
||||
];
|
||||
|
||||
const monthlyData = [
|
||||
{ month: 'Oct', income: 7200, expenses: 4800 },
|
||||
{ month: 'Nov', income: 8100, expenses: 5200 },
|
||||
{ month: 'Dec', income: 7800, expenses: 4500 },
|
||||
{ month: 'Jan', income: 8400, expenses: 5100 },
|
||||
{ month: 'Feb', income: 7900, expenses: 4900 },
|
||||
{ month: 'Mar', income: 8240, expenses: 3120 },
|
||||
];
|
||||
|
||||
const totalExpenses = categoryData.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
const donutGradient = (() => {
|
||||
let cursor = 0;
|
||||
const segments = categoryData.map((item) => {
|
||||
const start = cursor;
|
||||
const end = cursor + (item.value / totalExpenses) * 100;
|
||||
cursor = end;
|
||||
return `${item.color} ${start}% ${end}%`;
|
||||
});
|
||||
return `conic-gradient(${segments.join(', ')})`;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4">
|
||||
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-emerald-500/10">
|
||||
<TrendingUp class="h-[18px] w-[18px] text-emerald-400" :stroke-width="2" />
|
||||
</div>
|
||||
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Income</p>
|
||||
<p class="text-[20px] font-bold text-white">$8,107</p>
|
||||
<p class="mt-1 text-[11px] font-medium text-emerald-400">+5.2% growth</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4">
|
||||
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-rose-500/10">
|
||||
<BarChart3 class="h-[18px] w-[18px] text-rose-400" :stroke-width="2" />
|
||||
</div>
|
||||
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Expenses</p>
|
||||
<p class="text-[20px] font-bold text-white">$4,603</p>
|
||||
<p class="mt-1 text-[11px] font-medium text-rose-400">-12% decrease</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-purple-500/10">
|
||||
<PieChart class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||
</div>
|
||||
<h3 class="text-[15px] font-semibold text-white">Spending by Category</h3>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex h-48 items-center justify-center">
|
||||
<div class="relative h-40 w-40 rounded-full" :style="{ background: donutGradient }">
|
||||
<div class="absolute inset-[22%] rounded-full bg-[#16161F]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<div v-for="category in categoryData" :key="category.name" class="flex items-center gap-3">
|
||||
<div class="h-3 w-3 flex-shrink-0 rounded-full" :style="{ backgroundColor: category.color }" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<span class="text-[13px] font-medium text-white">{{ category.name }}</span>
|
||||
<span class="text-[12px] font-semibold text-zinc-400">${{ category.value }}</span>
|
||||
</div>
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-white/[0.05]">
|
||||
<div
|
||||
class="h-full rounded-full"
|
||||
:style="{
|
||||
width: `${((category.value / totalExpenses) * 100).toFixed(1)}%`,
|
||||
backgroundColor: category.color,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="w-10 flex-shrink-0 text-right text-[12px] font-medium text-zinc-600">
|
||||
{{ ((category.value / totalExpenses) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-blue-500/10">
|
||||
<Calendar class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
|
||||
</div>
|
||||
<h3 class="text-[15px] font-semibold text-white">Last 6 Months</h3>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div v-for="month in monthlyData" :key="month.month" class="flex items-center gap-3 rounded-[12px] bg-white/[0.02] p-3">
|
||||
<div class="w-12 text-center">
|
||||
<p class="text-[13px] font-semibold text-white">{{ month.month }}</p>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<span class="text-[11px] text-zinc-500">Income</span>
|
||||
<span class="text-[12px] font-semibold text-emerald-400">${{ month.income }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[11px] text-zinc-500">Expenses</span>
|
||||
<span class="text-[12px] font-semibold text-rose-400">${{ month.expenses }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-[14px] font-bold text-white">${{ month.income - month.expenses }}</p>
|
||||
<p class="text-[11px] text-zinc-600">{{ (((month.income - month.expenses) / month.income) * 100).toFixed(0) }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-[20px] bg-gradient-to-br from-purple-600 via-purple-500 to-blue-600 p-6 shadow-[0_8px_32px_rgba(139,92,246,0.3)]">
|
||||
<div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-blue-400 opacity-20 blur-3xl" />
|
||||
<div class="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-purple-400 opacity-20 blur-3xl" />
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<p class="mb-2 text-[13px] font-medium text-purple-100/70">Total Balance</p>
|
||||
<h2 class="text-[40px] font-bold leading-none tracking-tight text-white">$24,850</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 rounded-full border border-white/10 bg-white/15 px-3 py-1.5 backdrop-blur-sm">
|
||||
<TrendingUp class="h-3.5 w-3.5 text-emerald-300" :stroke-width="2.5" />
|
||||
<span class="text-[13px] font-semibold text-white">+12.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-[16px] border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
|
||||
<div class="mb-2 flex items-center gap-1.5">
|
||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-400/20">
|
||||
<ArrowUpRight class="h-3.5 w-3.5 text-emerald-400" :stroke-width="2.5" />
|
||||
</div>
|
||||
<p class="text-[12px] font-medium text-white/70">Income</p>
|
||||
</div>
|
||||
<p class="text-[20px] font-bold text-white">$8,240</p>
|
||||
</div>
|
||||
<div class="rounded-[16px] border border-white/10 bg-white/10 p-4 backdrop-blur-sm">
|
||||
<div class="mb-2 flex items-center gap-1.5">
|
||||
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-rose-400/20">
|
||||
<ArrowDownRight class="h-3.5 w-3.5 text-rose-400" :stroke-width="2.5" />
|
||||
</div>
|
||||
<p class="text-[12px] font-medium text-white/70">Expenses</p>
|
||||
</div>
|
||||
<p class="text-[20px] font-bold text-white">$3,120</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Briefcase,
|
||||
Car,
|
||||
ChevronRight,
|
||||
Coffee,
|
||||
Film,
|
||||
Gift,
|
||||
Heart,
|
||||
Home,
|
||||
ShoppingBag,
|
||||
Utensils,
|
||||
Zap,
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
const categories = [
|
||||
{ name: 'Food & Dining', icon: Utensils, color: 'orange', spent: 850, budget: 1200 },
|
||||
{ name: 'Shopping', icon: ShoppingBag, color: 'emerald', spent: 680, budget: 800 },
|
||||
{ name: 'Transport', icon: Car, color: 'red', spent: 420, budget: 500 },
|
||||
{ name: 'Housing', icon: Home, color: 'indigo', spent: 1250, budget: 1250 },
|
||||
{ name: 'Entertainment', icon: Film, color: 'purple', spent: 215, budget: 300 },
|
||||
{ name: 'Coffee', icon: Coffee, color: 'amber', spent: 145, budget: 150 },
|
||||
{ name: 'Bills & Utilities', icon: Zap, color: 'yellow', spent: 380, budget: 400 },
|
||||
{ name: 'Work', icon: Briefcase, color: 'blue', spent: 120, budget: 200 },
|
||||
{ name: 'Gifts & Donations', icon: Heart, color: 'pink', spent: 95, budget: 150 },
|
||||
{ name: 'Others', icon: Gift, color: 'zinc', spent: 65, budget: 100 },
|
||||
] as const;
|
||||
|
||||
const colorMap = {
|
||||
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400', bar: 'bg-orange-500' },
|
||||
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400', bar: 'bg-emerald-500' },
|
||||
red: { bg: 'bg-red-500/10', text: 'text-red-400', bar: 'bg-red-500' },
|
||||
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400', bar: 'bg-indigo-500' },
|
||||
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400', bar: 'bg-purple-500' },
|
||||
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400', bar: 'bg-amber-500' },
|
||||
yellow: { bg: 'bg-yellow-500/10', text: 'text-yellow-400', bar: 'bg-yellow-500' },
|
||||
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400', bar: 'bg-blue-500' },
|
||||
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400', bar: 'bg-pink-500' },
|
||||
zinc: { bg: 'bg-zinc-500/10', text: 'text-zinc-400', bar: 'bg-zinc-500' },
|
||||
} as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2.5">
|
||||
<div
|
||||
v-for="category in categories"
|
||||
:key="category.name"
|
||||
class="group cursor-pointer rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', colorMap[category.color].bg]">
|
||||
<component :is="category.icon" :class="['h-[19px] w-[19px]', colorMap[category.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ category.name }}</h4>
|
||||
<p class="text-[12px] text-zinc-500">${{ category.spent }} of ${{ category.budget }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p :class="['text-[14px] font-bold', category.spent > category.budget ? 'text-rose-400' : 'text-emerald-400']">
|
||||
{{ category.spent > category.budget ? '+' : '' }}${{ Math.abs(category.budget - category.spent) }}
|
||||
</p>
|
||||
<p class="text-[11px] text-zinc-600">{{ ((category.spent / category.budget) * 100).toFixed(0) }}%</p>
|
||||
</div>
|
||||
|
||||
<ChevronRight class="h-5 w-5 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||
</div>
|
||||
|
||||
<div class="h-2 overflow-hidden rounded-full bg-white/[0.05]">
|
||||
<div
|
||||
:class="[
|
||||
'h-full rounded-full',
|
||||
category.spent > category.budget ? 'bg-rose-500' : colorMap[category.color].bar,
|
||||
]"
|
||||
:style="{ width: `${Math.min((category.spent / category.budget) * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Eye, EyeOff, TrendingUp } from 'lucide-vue-next';
|
||||
|
||||
const isVisible = ref(true);
|
||||
const chartData = [20000, 21500, 20800, 23200, 22500, 24850];
|
||||
|
||||
const polylinePoints = computed(() => {
|
||||
const max = Math.max(...chartData);
|
||||
const min = Math.min(...chartData);
|
||||
const width = 96;
|
||||
const height = 64;
|
||||
|
||||
return chartData
|
||||
.map((value, index) => {
|
||||
const x = (index / (chartData.length - 1)) * width;
|
||||
const y = height - ((value - min) / (max - min || 1)) * (height - 6) - 3;
|
||||
return `${x},${y}`;
|
||||
})
|
||||
.join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-[20px] bg-gradient-to-br from-purple-600 via-purple-500 to-blue-600 p-6 shadow-[0_8px_32px_rgba(139,92,246,0.3)]">
|
||||
<div class="absolute -top-20 -right-20 h-40 w-40 rounded-full bg-blue-400 opacity-20 blur-3xl" />
|
||||
<div class="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-purple-400 opacity-20 blur-3xl" />
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6 flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<p class="text-[13px] font-medium text-purple-100/70">Total Balance</p>
|
||||
<button
|
||||
type="button"
|
||||
@click="isVisible = !isVisible"
|
||||
class="flex h-6 w-6 items-center justify-center rounded-lg bg-white/10 transition-colors hover:bg-white/20"
|
||||
>
|
||||
<Eye v-if="isVisible" class="h-3.5 w-3.5 text-white" :stroke-width="2" />
|
||||
<EyeOff v-else class="h-3.5 w-3.5 text-white" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-1 text-[36px] font-bold leading-none tracking-tight text-white">
|
||||
{{ isVisible ? '$24,850.00' : '••••••' }}
|
||||
</h2>
|
||||
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div class="flex items-center gap-1 rounded-md bg-emerald-400/20 px-2 py-0.5">
|
||||
<TrendingUp class="h-3 w-3 text-emerald-300" :stroke-width="2.5" />
|
||||
<span class="text-[12px] font-semibold text-emerald-300">+12.5%</span>
|
||||
</div>
|
||||
<span class="text-[12px] text-purple-200/60">vs last month</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 h-16 w-24">
|
||||
<svg viewBox="0 0 96 64" class="h-full w-full" fill="none">
|
||||
<polyline :points="polylinePoints" stroke="#ffffff" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
|
||||
<p class="mb-1 text-[11px] font-medium text-white/60">This Month</p>
|
||||
<p class="text-[18px] font-bold text-white">$8,240</p>
|
||||
<p class="mt-0.5 text-[11px] font-medium text-emerald-300">+18% income</p>
|
||||
</div>
|
||||
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
|
||||
<p class="mb-1 text-[11px] font-medium text-white/60">Expenses</p>
|
||||
<p class="text-[18px] font-bold text-white">$3,120</p>
|
||||
<p class="mt-0.5 text-[11px] font-medium text-rose-300">-8% saved</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Bell, Plus, Settings, Wallet as WalletIcon } from 'lucide-vue-next';
|
||||
import FinanceBalanceCard from './FinanceBalanceCard.vue';
|
||||
import TransactionsList from './TransactionsList.vue';
|
||||
import AnalyticsView from './AnalyticsView.vue';
|
||||
import CategoriesView from './CategoriesView.vue';
|
||||
import Navigation from './Navigation.vue';
|
||||
|
||||
type Tab = 'transactions' | 'analytics' | 'categories';
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigate: [screen: string];
|
||||
}>();
|
||||
|
||||
const activeTab = ref<Tab>('transactions');
|
||||
|
||||
const tabs: Array<{ id: Tab; label: string }> = [
|
||||
{ id: 'transactions', label: 'Transactions' },
|
||||
{ id: 'analytics', label: 'Analytics' },
|
||||
{ id: 'categories', label: 'Categories' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#0A0A0F] dark">
|
||||
<div class="mx-auto flex min-h-screen max-w-md flex-col relative">
|
||||
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||
<WalletIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Family budget</p>
|
||||
<h1 class="text-[17px] font-semibold tracking-tight text-white">Finance</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]">
|
||||
<Bell class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||
@click="emit('navigate', 'settings')"
|
||||
>
|
||||
<Settings class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto px-5 pb-24">
|
||||
<div class="mb-5">
|
||||
<FinanceBalanceCard />
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<div class="flex items-center gap-2 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-1.5">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
type="button"
|
||||
@click="activeTab = tab.id"
|
||||
class="relative flex-1 rounded-[12px] px-4 py-2.5 text-[13px] font-semibold transition-colors"
|
||||
>
|
||||
<div
|
||||
v-if="activeTab === tab.id"
|
||||
class="absolute inset-0 rounded-[12px] border border-purple-500/20 bg-gradient-to-br from-purple-500/15 to-blue-500/15"
|
||||
/>
|
||||
<span :class="['relative z-10', activeTab === tab.id ? 'text-purple-400' : 'text-zinc-500']">
|
||||
{{ tab.label }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransactionsList v-if="activeTab === 'transactions'" />
|
||||
<AnalyticsView v-else-if="activeTab === 'analytics'" />
|
||||
<CategoriesView v-else />
|
||||
</main>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="group fixed bottom-24 right-6 z-10 flex h-14 w-14 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-600 shadow-[0_8px_32px_rgba(139,92,246,0.4)] transition-all hover:shadow-[0_12px_40px_rgba(139,92,246,0.6)] active:scale-95"
|
||||
>
|
||||
<Plus class="h-6 w-6 text-white transition-transform duration-300 group-hover:rotate-90" :stroke-width="2.5" />
|
||||
</button>
|
||||
|
||||
<Navigation active-screen="finance" @navigate="emit('navigate', $event)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { Bell, Settings, User } from 'lucide-vue-next';
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigate: [screen: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||
<User class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Good evening</p>
|
||||
<h1 class="text-[17px] font-semibold tracking-tight text-white">Anderson Family</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]">
|
||||
<Bell class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||
@click="emit('navigate', 'settings')"
|
||||
>
|
||||
<Settings class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -1,93 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import viteLogo from '../assets/vite.svg'
|
||||
import heroImg from '../assets/hero.png'
|
||||
import vueLogo from '../assets/vue.svg'
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="center">
|
||||
<div class="hero">
|
||||
<img :src="heroImg" class="base" width="170" height="179" alt="" />
|
||||
<img :src="vueLogo" class="framework" alt="Vue logo" />
|
||||
<img :src="viteLogo" class="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
|
||||
</div>
|
||||
<button class="counter" @click="count++">Count is {{ count }}</button>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
|
||||
<section id="next-steps">
|
||||
<div id="docs">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#documentation-icon"></use>
|
||||
</svg>
|
||||
<h2>Documentation</h2>
|
||||
<p>Your questions, answered</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vite.dev/" target="_blank">
|
||||
<img class="logo" :src="viteLogo" alt="" />
|
||||
Explore Vite
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img class="button-icon" :src="vueLogo" alt="" />
|
||||
Learn more
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="social">
|
||||
<svg class="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#social-icon"></use>
|
||||
</svg>
|
||||
<h2>Connect with us</h2>
|
||||
<p>Join the Vite community</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#github-icon"></use>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vite.dev/" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#discord-icon"></use>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/vite_js" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#x-icon"></use>
|
||||
</svg>
|
||||
X.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
||||
<svg class="button-icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#bluesky-icon"></use>
|
||||
</svg>
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import {Calendar, Heart, Home, Sparkles, Wallet} from 'lucide-vue-next';
|
||||
|
||||
interface NavItem {
|
||||
icon: unknown;
|
||||
label: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
activeScreen: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigate: [screen: string];
|
||||
}>();
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ icon: Calendar, label: 'Calendar', id: 'calendar' },
|
||||
{ icon: Wallet, label: 'Finance', id: 'finance' },
|
||||
{ icon: Home, label: 'Home', id: 'home' },
|
||||
{ icon: Heart, label: 'Votes', id: 'votes' },
|
||||
{ icon: Sparkles, label: 'Intimacy', id: 'intimacy' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="pointer-events-none fixed bottom-0 left-0 right-0 mx-auto max-w-md px-5 pb-5">
|
||||
<div class="pointer-events-auto rounded-[20px] border border-white/[0.08] bg-[#1A1A24]/95 px-2 py-3 shadow-[0_8px_32px_rgba(0,0,0,0.6)] backdrop-blur-xl">
|
||||
<div class="flex items-center justify-between">
|
||||
<button
|
||||
v-for="item in navItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
@click="emit('navigate', item.id)"
|
||||
:class="[
|
||||
'relative flex flex-col items-center gap-1.5 py-2 transition-all',
|
||||
item.id === 'home' ? 'px-5' : 'px-3',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="props.activeScreen === item.id"
|
||||
class="absolute inset-0 rounded-[14px] border border-purple-500/20 bg-gradient-to-br from-purple-500/15 to-blue-500/15"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="item.id === 'home'"
|
||||
:class="[
|
||||
'relative z-10 flex h-12 w-12 items-center justify-center rounded-full transition-all',
|
||||
props.activeScreen === item.id
|
||||
? 'bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/30'
|
||||
: 'bg-zinc-800',
|
||||
]"
|
||||
>
|
||||
<component
|
||||
:is="item.icon"
|
||||
:class="[
|
||||
'h-6 w-6',
|
||||
props.activeScreen === item.id ? 'text-white' : 'text-zinc-400',
|
||||
]"
|
||||
:stroke-width="props.activeScreen === item.id ? 2.5 : 2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<component
|
||||
:is="item.icon"
|
||||
:class="[
|
||||
'relative z-10 h-5 w-5 transition-colors',
|
||||
props.activeScreen === item.id ? 'text-purple-400' : 'text-zinc-500',
|
||||
]"
|
||||
:stroke-width="props.activeScreen === item.id ? 2.5 : 2"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
'relative z-10 text-[10px] font-medium transition-colors',
|
||||
props.activeScreen === item.id ? 'text-purple-400' : 'text-zinc-600',
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { Activity, CreditCard, Popcorn, ShoppingCart, Users } from 'lucide-vue-next';
|
||||
|
||||
const activities = [
|
||||
{
|
||||
icon: ShoppingCart,
|
||||
title: 'Grocery Shopping',
|
||||
subtitle: 'Sarah • $124.50',
|
||||
time: '2h',
|
||||
bgColor: 'bg-emerald-500/10',
|
||||
textColor: 'text-emerald-400',
|
||||
},
|
||||
{
|
||||
icon: Popcorn,
|
||||
title: 'Movie Vote',
|
||||
subtitle: '3 votes for "Dune 2"',
|
||||
time: '5h',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
textColor: 'text-purple-400',
|
||||
},
|
||||
{
|
||||
icon: CreditCard,
|
||||
title: 'Subscription',
|
||||
subtitle: 'Netflix • $15.99',
|
||||
time: '1d',
|
||||
bgColor: 'bg-rose-500/10',
|
||||
textColor: 'text-rose-400',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'New Event',
|
||||
subtitle: 'Family BBQ Saturday',
|
||||
time: '2d',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
textColor: 'text-blue-400',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-blue-500/10">
|
||||
<Activity class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
|
||||
</div>
|
||||
<h3 class="text-[15px] font-semibold text-white">Activity</h3>
|
||||
</div>
|
||||
<button class="text-[12px] font-medium text-purple-400 transition-colors hover:text-purple-300">
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="activity in activities"
|
||||
:key="activity.title"
|
||||
class="group flex cursor-pointer items-center gap-3 rounded-[12px] p-3 transition-all hover:bg-white/[0.03]"
|
||||
>
|
||||
<div :class="['flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[12px] transition-transform group-hover:scale-110', activity.bgColor]">
|
||||
<component :is="activity.icon" :class="['h-[18px] w-[18px]', activity.textColor]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="mb-0.5 text-[14px] font-medium text-white">{{ activity.title }}</p>
|
||||
<p class="truncate text-[12px] text-zinc-500">{{ activity.subtitle }}</p>
|
||||
</div>
|
||||
<span class="flex-shrink-0 text-[12px] font-medium text-zinc-600">{{ activity.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, type Component } from 'vue'
|
||||
import {
|
||||
Bell,
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
Crown,
|
||||
Eye,
|
||||
Film,
|
||||
Heart,
|
||||
LayoutGrid,
|
||||
Lock,
|
||||
LogOut,
|
||||
Palette,
|
||||
X,
|
||||
Settings as SettingsIcon,
|
||||
Shield,
|
||||
Users,
|
||||
Utensils,
|
||||
Wallet,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
interface ModuleItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: Component;
|
||||
color: keyof typeof moduleColorMap;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface SettingItemData {
|
||||
icon: Component;
|
||||
color: keyof typeof itemColorMap;
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
onNavigate?: (screen: string) => void;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
navigate: [screen: string];
|
||||
}>()
|
||||
|
||||
const itemColorMap = {
|
||||
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
||||
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
||||
cyan: { bg: 'bg-cyan-500/10', text: 'text-cyan-400' },
|
||||
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||
zinc: { bg: 'bg-zinc-500/10', text: 'text-zinc-400' },
|
||||
} as const
|
||||
|
||||
const moduleColorMap = {
|
||||
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
||||
} as const
|
||||
|
||||
const familyItems: SettingItemData[] = [
|
||||
{
|
||||
icon: Users,
|
||||
color: 'blue',
|
||||
label: 'Family Members',
|
||||
description: 'Manage family access',
|
||||
badge: '4',
|
||||
},
|
||||
{
|
||||
icon: Crown,
|
||||
color: 'amber',
|
||||
label: 'Roles & Permissions',
|
||||
description: 'Control what members can do',
|
||||
},
|
||||
]
|
||||
|
||||
const customizationItems: SettingItemData[] = [
|
||||
{
|
||||
icon: LayoutGrid,
|
||||
color: 'purple',
|
||||
label: 'Dashboard Widgets',
|
||||
description: 'Customize your home screen',
|
||||
},
|
||||
{
|
||||
icon: Palette,
|
||||
color: 'pink',
|
||||
label: 'Theme & Appearance',
|
||||
description: 'Dark mode, colors, fonts',
|
||||
},
|
||||
]
|
||||
|
||||
const privacyItems: SettingItemData[] = [
|
||||
{
|
||||
icon: Lock,
|
||||
color: 'emerald',
|
||||
label: 'Privacy Settings',
|
||||
description: 'Control data sharing',
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
color: 'indigo',
|
||||
label: 'Visibility Controls',
|
||||
description: 'Who can see what',
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
color: 'cyan',
|
||||
label: 'Security',
|
||||
description: 'Password, 2FA, biometrics',
|
||||
},
|
||||
]
|
||||
|
||||
const advancedItems: SettingItemData[] = [
|
||||
{
|
||||
icon: Bell,
|
||||
color: 'orange',
|
||||
label: 'Notifications',
|
||||
description: 'Push, email, SMS settings',
|
||||
},
|
||||
{
|
||||
icon: SettingsIcon,
|
||||
color: 'zinc',
|
||||
label: 'App Preferences',
|
||||
description: 'Language, timezone, units',
|
||||
},
|
||||
]
|
||||
|
||||
const modules = reactive<ModuleItem[]>([
|
||||
{ id: 'finance', name: 'Finance Tracking', icon: Wallet, color: 'purple', enabled: true },
|
||||
{ id: 'calendar', name: 'Family Calendar', icon: Calendar, color: 'blue', enabled: true },
|
||||
{ id: 'food', name: 'Food Voting', icon: Utensils, color: 'orange', enabled: true },
|
||||
{ id: 'movies', name: 'Movie Voting', icon: Film, color: 'pink', enabled: true },
|
||||
{ id: 'activities', name: 'Activity Feed', icon: Heart, color: 'red', enabled: true },
|
||||
])
|
||||
|
||||
function toggleModule(id: string) {
|
||||
const moduleItem = modules.find((item) => item.id === id)
|
||||
if (moduleItem) {
|
||||
moduleItem.enabled = !moduleItem.enabled
|
||||
}
|
||||
}
|
||||
|
||||
function navigate(screen: string) {
|
||||
emit('navigate', screen)
|
||||
props.onNavigate?.(screen)
|
||||
}
|
||||
|
||||
function animationDelay(index: number, base = 0.1) {
|
||||
return { animationDelay: `${base + index * 0.05}s` }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-[#0A0A0F] dark">
|
||||
<div class="relative mx-auto flex min-h-screen max-w-md flex-col">
|
||||
<header class="flex items-center justify-between px-5 pt-6 pb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
|
||||
<SettingsIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Manage your family hub</p>
|
||||
<h1 class="text-[17px] font-semibold tracking-tight text-white">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-[14px] border border-white/5 bg-[#1A1A24] transition-colors hover:bg-[#222230]"
|
||||
@click="navigate('close-settings')"
|
||||
>
|
||||
<X class="h-[18px] w-[18px] text-zinc-400" :stroke-width="2" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto px-5 pb-28">
|
||||
<div class="space-y-5">
|
||||
<section class="screen-enter relative overflow-hidden rounded-[20px] border border-purple-500/20 bg-gradient-to-br from-purple-600/20 via-purple-500/10 to-blue-600/20 p-5" :style="animationDelay(0)">
|
||||
<div class="absolute -top-10 -right-10 h-32 w-32 rounded-full bg-purple-500 opacity-10 blur-3xl" />
|
||||
<div class="relative z-10 flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-600 text-[22px] font-bold text-white">
|
||||
JS
|
||||
</div>
|
||||
<div class="absolute -right-1 -bottom-1 flex h-6 w-6 items-center justify-center rounded-full border-2 border-[#0A0A0F] bg-emerald-500">
|
||||
<Crown class="h-3 w-3 text-white" :stroke-width="2.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="mb-0.5 text-[18px] font-bold text-white">John Smith</h2>
|
||||
<p class="mb-1 text-[13px] font-medium text-purple-300">Family Admin</p>
|
||||
<p class="text-[12px] text-zinc-500">john.smith@email.com</p>
|
||||
</div>
|
||||
|
||||
<button type="button" class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-white/10 transition-colors hover:bg-white/20">
|
||||
<ChevronRight class="h-[18px] w-[18px] text-white" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen-enter" :style="animationDelay(1)">
|
||||
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Family
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="item in familyItems"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||
</div>
|
||||
<div v-if="item.badge" class="rounded-full border border-purple-500/20 bg-purple-500/15 px-2.5 py-1">
|
||||
<span class="text-[12px] font-semibold text-purple-400">{{ item.badge }}</span>
|
||||
</div>
|
||||
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen-enter" :style="animationDelay(2)">
|
||||
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Customization
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="item in customizationItems"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||
</div>
|
||||
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen-enter" :style="animationDelay(3)">
|
||||
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Active Modules
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(module, index) in modules"
|
||||
:key="module.id"
|
||||
class="module-enter flex items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4"
|
||||
:style="animationDelay(index, 0.25)"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px]', moduleColorMap[module.color].bg]">
|
||||
<component :is="module.icon" :class="['h-[19px] w-[19px]', moduleColorMap[module.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-[14px] font-semibold text-white">{{ module.name }}</p>
|
||||
<p :class="['text-[12px]', module.enabled ? 'text-emerald-400' : 'text-zinc-600']">
|
||||
{{ module.enabled ? 'Active' : 'Disabled' }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
:aria-pressed="module.enabled"
|
||||
:class="[
|
||||
'relative h-7 w-12 rounded-full transition-all',
|
||||
module.enabled ? 'bg-gradient-to-r from-purple-500 to-blue-600' : 'bg-zinc-800',
|
||||
]"
|
||||
@click="toggleModule(module.id)"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'absolute top-1 left-1 h-5 w-5 rounded-full bg-white shadow-lg transition-transform duration-300',
|
||||
module.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen-enter" :style="animationDelay(4)">
|
||||
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Privacy & Security
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="item in privacyItems"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||
</div>
|
||||
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="screen-enter" :style="animationDelay(5)">
|
||||
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Advanced
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
v-for="item in advancedItems"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', itemColorMap[item.color].bg]">
|
||||
<component :is="item.icon" :class="['h-[19px] w-[19px]', itemColorMap[item.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ item.label }}</p>
|
||||
<p class="text-[12px] text-zinc-500">{{ item.description }}</p>
|
||||
</div>
|
||||
<ChevronRight class="h-5 w-5 flex-shrink-0 text-zinc-700 transition-colors group-hover:text-zinc-500" :stroke-width="2" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="screen-enter group flex w-full items-center justify-center gap-2 rounded-[16px] border border-red-500/20 bg-red-500/10 p-4 transition-all hover:border-red-500/30 hover:bg-red-500/20"
|
||||
:style="animationDelay(6)"
|
||||
>
|
||||
<LogOut class="h-[18px] w-[18px] text-red-400 transition-colors group-hover:text-red-300" :stroke-width="2" />
|
||||
<span class="text-[14px] font-semibold text-red-400 transition-colors group-hover:text-red-300">
|
||||
Sign Out
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="py-4 text-center">
|
||||
<p class="text-[12px] text-zinc-700">Family OS v2.4.0</p>
|
||||
<p class="mt-1 text-[11px] text-zinc-800">© 2026 Family Hub Inc.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.screen-enter,
|
||||
.module-enter {
|
||||
animation: screen-enter 0.45s ease-out both;
|
||||
}
|
||||
|
||||
@keyframes screen-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Clock, Heart, MapPin, Sparkles, Star, X } from 'lucide-vue-next';
|
||||
|
||||
interface SwipeItem {
|
||||
id: number;
|
||||
type: 'food' | 'movie';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
rating: number;
|
||||
location?: string;
|
||||
duration?: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
const cards: SwipeItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'food',
|
||||
title: 'Italian Restaurant',
|
||||
subtitle: 'Authentic pasta & pizza',
|
||||
rating: 4.8,
|
||||
location: '2.3 km away',
|
||||
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&q=80',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'movie',
|
||||
title: 'Dune: Part Two',
|
||||
subtitle: 'Sci-Fi Epic',
|
||||
rating: 4.9,
|
||||
duration: '2h 46m',
|
||||
image: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'food',
|
||||
title: 'Sushi House',
|
||||
subtitle: 'Fresh Japanese cuisine',
|
||||
rating: 4.7,
|
||||
location: '1.8 km away',
|
||||
image: 'https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=800&q=80',
|
||||
},
|
||||
];
|
||||
|
||||
const currentIndex = ref(0);
|
||||
const swipeDirection = ref<'left' | 'right' | null>(null);
|
||||
const pointerStartX = ref<number | null>(null);
|
||||
|
||||
const currentCard = computed(() => cards[currentIndex.value]);
|
||||
|
||||
function nextCard() {
|
||||
currentIndex.value = (currentIndex.value + 1) % cards.length;
|
||||
}
|
||||
|
||||
function handleSwipe(direction: 'left' | 'right') {
|
||||
swipeDirection.value = direction;
|
||||
window.setTimeout(() => {
|
||||
nextCard();
|
||||
swipeDirection.value = null;
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent) {
|
||||
pointerStartX.value = event.clientX;
|
||||
}
|
||||
|
||||
function onPointerUp(event: PointerEvent) {
|
||||
if (pointerStartX.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = event.clientX - pointerStartX.value;
|
||||
pointerStartX.value = null;
|
||||
|
||||
if (deltaX > 60) {
|
||||
handleSwipe('right');
|
||||
} else if (deltaX < -60) {
|
||||
handleSwipe('left');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] border border-purple-500/20 bg-gradient-to-br from-purple-500/20 to-blue-500/20">
|
||||
<Sparkles class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-[15px] font-semibold text-white">Vote Now</h3>
|
||||
<p class="text-[11px] text-zinc-500">Swipe to decide</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
v-for="(_, i) in cards"
|
||||
:key="i"
|
||||
:class="[
|
||||
'h-1.5 rounded-full transition-all',
|
||||
i === currentIndex ? 'w-6 bg-gradient-to-r from-purple-500 to-blue-500' : 'w-1.5 bg-zinc-700',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-4 h-[360px]">
|
||||
<div
|
||||
class="absolute inset-0 cursor-grab overflow-hidden rounded-[18px] shadow-xl active:cursor-grabbing"
|
||||
:class="swipeDirection ? 'transition-all duration-200 ease-out opacity-0 scale-95' : ''"
|
||||
@pointerdown="onPointerDown"
|
||||
@pointerup="onPointerUp"
|
||||
>
|
||||
<img :src="currentCard.image" :alt="currentCard.title" class="h-full w-full object-cover" />
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
|
||||
|
||||
<div class="absolute top-4 right-4 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/60 px-3 py-1.5 backdrop-blur-md">
|
||||
<Star class="h-3.5 w-3.5 fill-amber-400 text-amber-400" :stroke-width="2" />
|
||||
<span class="text-[13px] font-bold text-white">{{ currentCard.rating }}</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 right-0 p-5">
|
||||
<div class="mb-3">
|
||||
<span class="mb-3 inline-flex items-center gap-1.5 rounded-lg border border-purple-500/20 bg-purple-500/20 px-2.5 py-1.5 text-[11px] font-semibold text-purple-300 backdrop-blur-sm">
|
||||
{{ currentCard.type === 'food' ? 'Restaurant' : 'Movie' }}
|
||||
</span>
|
||||
<h4 class="mb-1 text-[24px] font-bold leading-tight text-white">{{ currentCard.title }}</h4>
|
||||
<p class="mb-3 text-[14px] text-zinc-300">{{ currentCard.subtitle }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-[12px] text-zinc-400">
|
||||
<div v-if="currentCard.location" class="flex items-center gap-1.5">
|
||||
<MapPin class="h-3.5 w-3.5" :stroke-width="2" />
|
||||
<span class="font-medium">{{ currentCard.location }}</span>
|
||||
</div>
|
||||
<div v-if="currentCard.duration" class="flex items-center gap-1.5">
|
||||
<Clock class="h-3.5 w-3.5" :stroke-width="2" />
|
||||
<span class="font-medium">{{ currentCard.duration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="swipeDirection" class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
|
||||
<div
|
||||
:class="[
|
||||
'flex h-24 w-24 items-center justify-center rounded-full border-[3px] backdrop-blur-sm shadow-2xl',
|
||||
swipeDirection === 'left' ? 'border-rose-500 bg-rose-500/20' : 'border-emerald-500 bg-emerald-500/20',
|
||||
]"
|
||||
>
|
||||
<X v-if="swipeDirection === 'left'" class="h-12 w-12 text-rose-500" :stroke-width="3" />
|
||||
<Heart
|
||||
v-else
|
||||
class="h-12 w-12 fill-emerald-500 text-emerald-500"
|
||||
:stroke-width="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSwipe('left')"
|
||||
class="group flex h-14 w-14 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.06] transition-all hover:border-rose-500/30 hover:bg-rose-500/10 active:scale-95"
|
||||
>
|
||||
<X class="h-6 w-6 text-zinc-400 transition-colors group-hover:text-rose-400" :stroke-width="2.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleSwipe('right')"
|
||||
class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500 shadow-[0_8px_24px_rgba(139,92,246,0.35)] transition-all hover:from-purple-600 hover:to-blue-600 hover:shadow-[0_12px_32px_rgba(139,92,246,0.5)] active:scale-95"
|
||||
>
|
||||
<Heart class="h-7 w-7 text-white" :stroke-width="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { Calendar, Clock, MapPin } from 'lucide-vue-next';
|
||||
|
||||
const todayEvents = [
|
||||
{
|
||||
time: '6:30 PM',
|
||||
title: 'Family Dinner',
|
||||
location: 'Home',
|
||||
colorFrom: '#a855f7',
|
||||
colorTo: '#9333ea',
|
||||
},
|
||||
{
|
||||
time: '8:00 PM',
|
||||
title: 'Movie Night',
|
||||
location: 'Living Room',
|
||||
colorFrom: '#3b82f6',
|
||||
colorTo: '#2563eb',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-purple-500/10">
|
||||
<Calendar class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-[15px] font-semibold text-white">Today</h3>
|
||||
<p class="text-[12px] text-zinc-500">Wednesday, April 2</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-[12px] font-medium text-zinc-600">{{ todayEvents.length }} events</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<div
|
||||
v-for="event in todayEvents"
|
||||
:key="event.title"
|
||||
class="group flex cursor-pointer items-center gap-3 rounded-[14px] border border-white/[0.04] bg-white/[0.02] p-3.5 transition-all hover:border-white/[0.08] hover:bg-white/[0.05]"
|
||||
>
|
||||
<div class="flex min-w-[52px] flex-col items-center justify-center rounded-[10px] border border-white/[0.06] bg-white/[0.04] py-2 px-2.5">
|
||||
<Clock class="mb-1 h-3.5 w-3.5 text-zinc-500" :stroke-width="2" />
|
||||
<span class="text-[13px] font-semibold leading-none text-white">{{ event.time }}</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ event.title }}</h4>
|
||||
<div class="flex items-center gap-1 text-zinc-500">
|
||||
<MapPin class="h-3 w-3" :stroke-width="2" />
|
||||
<span class="text-[12px]">{{ event.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="h-12 w-1.5 rounded-full opacity-60 transition-opacity group-hover:opacity-100"
|
||||
:style="{ background: `linear-gradient(to bottom, ${event.colorFrom}, ${event.colorTo})` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,181 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Car,
|
||||
Coffee,
|
||||
Film,
|
||||
Heart,
|
||||
Home,
|
||||
ShoppingBag,
|
||||
TrendingUp,
|
||||
Utensils,
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
amount: number;
|
||||
type: 'income' | 'expense';
|
||||
icon: unknown;
|
||||
color: keyof typeof colorMap;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface TransactionGroup {
|
||||
date: string;
|
||||
total: number;
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
|
||||
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
|
||||
amber: { bg: 'bg-amber-500/10', text: 'text-amber-400' },
|
||||
blue: { bg: 'bg-blue-500/10', text: 'text-blue-400' },
|
||||
red: { bg: 'bg-red-500/10', text: 'text-red-400' },
|
||||
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
|
||||
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400' },
|
||||
indigo: { bg: 'bg-indigo-500/10', text: 'text-indigo-400' },
|
||||
} as const;
|
||||
|
||||
const transactionGroups: TransactionGroup[] = [
|
||||
{
|
||||
date: 'Today',
|
||||
total: -245.5,
|
||||
transactions: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Whole Foods Market',
|
||||
category: 'Groceries',
|
||||
amount: -124.5,
|
||||
type: 'expense',
|
||||
icon: ShoppingBag,
|
||||
color: 'emerald',
|
||||
time: '2:30 PM',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Uber Eats',
|
||||
category: 'Food & Dining',
|
||||
amount: -45,
|
||||
type: 'expense',
|
||||
icon: Utensils,
|
||||
color: 'orange',
|
||||
time: '12:15 PM',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Starbucks',
|
||||
category: 'Coffee',
|
||||
amount: -12.5,
|
||||
type: 'expense',
|
||||
icon: Coffee,
|
||||
color: 'amber',
|
||||
time: '9:00 AM',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Freelance Payment',
|
||||
category: 'Income',
|
||||
amount: 850,
|
||||
type: 'income',
|
||||
icon: TrendingUp,
|
||||
color: 'blue',
|
||||
time: '8:00 AM',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: 'Yesterday',
|
||||
total: -89.99,
|
||||
transactions: [
|
||||
{
|
||||
id: '5',
|
||||
title: 'Shell Gas Station',
|
||||
category: 'Transport',
|
||||
amount: -65,
|
||||
type: 'expense',
|
||||
icon: Car,
|
||||
color: 'red',
|
||||
time: '6:45 PM',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Netflix Subscription',
|
||||
category: 'Entertainment',
|
||||
amount: -15.99,
|
||||
type: 'expense',
|
||||
icon: Film,
|
||||
color: 'purple',
|
||||
time: '12:00 PM',
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Charity Donation',
|
||||
category: 'Donation',
|
||||
amount: -25,
|
||||
type: 'expense',
|
||||
icon: Heart,
|
||||
color: 'pink',
|
||||
time: '10:30 AM',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
date: 'Apr 1',
|
||||
total: -1250,
|
||||
transactions: [
|
||||
{
|
||||
id: '8',
|
||||
title: 'Rent Payment',
|
||||
category: 'Housing',
|
||||
amount: -1250,
|
||||
type: 'expense',
|
||||
icon: Home,
|
||||
color: 'indigo',
|
||||
time: '9:00 AM',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div v-for="group in transactionGroups" :key="group.date">
|
||||
<div class="mb-3 flex items-center justify-between px-1">
|
||||
<h3 class="text-[14px] font-semibold text-white">{{ group.date }}</h3>
|
||||
<span :class="['text-[13px] font-semibold', group.total >= 0 ? 'text-emerald-400' : 'text-zinc-400']">
|
||||
{{ group.total >= 0 ? '+' : '' }}${{ Math.abs(group.total).toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="transaction in group.transactions"
|
||||
:key="transaction.id"
|
||||
class="group flex cursor-pointer items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-3.5 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
|
||||
>
|
||||
<div :class="['flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] transition-transform group-hover:scale-110', colorMap[transaction.color].bg]">
|
||||
<component :is="transaction.icon" :class="['h-[19px] w-[19px]', colorMap[transaction.color].text]" :stroke-width="2" />
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="mb-0.5 truncate text-[14px] font-semibold text-white">{{ transaction.title }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[12px] text-zinc-500">{{ transaction.category }}</span>
|
||||
<span class="text-zinc-700">•</span>
|
||||
<span class="text-[12px] text-zinc-600">{{ transaction.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p :class="['text-[15px] font-bold', transaction.type === 'income' ? 'text-emerald-400' : 'text-white']">
|
||||
{{ transaction.type === 'income' ? '+' : '-' }}${{ Math.abs(transaction.amount).toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user