enhanced ui with working metrics

This commit is contained in:
Kumaraguru 2025-01-09 22:12:24 +00:00
parent 10d0daf57f
commit 1146db13dc
No known key found for this signature in database
GPG Key ID: 4E4555A84ECD28F7
8 changed files with 2516 additions and 498 deletions

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "styles/globals.css",
"baseColor": "neutral",
"cssVariables": false,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

6
lib/utils.js Normal file
View File

@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

2264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,15 +9,33 @@
"lint": "next lint"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.1.1"
"@radix-ui/react-avatar": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@supabase/supabase-js": "^2.47.8",
"@tremor/react": "^3.18.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotted-map": "^2.2.3",
"framer-motion": "^11.15.0",
"lucide-react": "^0.468.0",
"next": "15.1.1",
"react": "^18",
"react-dom": "^18",
"recharts": "^2.15.0",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"postcss": "^8",
"tailwindcss": "^3.4.1",
"@eslint/eslintrc": "^3",
"eslint": "^9",
"eslint-config-next": "15.1.1",
"@eslint/eslintrc": "^3"
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
}

View File

@ -1,114 +1,514 @@
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@supabase/supabase-js";
import { motion } from "framer-motion";
import { format, isToday, isYesterday } from "date-fns";
import Image from "next/image";
import { Geist, Geist_Mono } from "next/font/google";
import {
Activity,
Users,
Network,
Database,
Clock,
AlertTriangle,
RotateCw,
Search,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
// Initialize Supabase client
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
const ITEMS_PER_PAGE = 5;
export default function Dashboard() {
const [metrics, setMetrics] = useState([]);
const [activeNodes, setActiveNodes] = useState([]);
const [timeframe, setTimeframe] = useState("7d");
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [componentLoading, setComponentLoading] = useState({
metrics: true,
nodes: true,
versions: true,
peers: true,
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
// Pagination and Search states
const [versionPage, setVersionPage] = useState(1);
const [peerPage, setPeerPage] = useState(1);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
useEffect(() => {
fetchData();
}, [timeframe]);
// Update search results when query changes
useEffect(() => {
if (searchQuery.trim()) {
setIsSearching(true);
const results = activePeerIds.filter(peerId =>
peerId.toLowerCase().includes(searchQuery.toLowerCase())
);
setSearchResults(results);
setPeerPage(1);
setIsSearching(false);
} else {
setSearchResults([]);
}
}, [searchQuery]);
const fetchData = async () => {
setLoading(true);
setError(null);
setComponentLoading({
metrics: true,
nodes: true,
versions: true,
peers: true,
});
export default function Home() {
return (
<div
className={`${geistSans.variable} ${geistMono.variable} grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]`}
>
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
pages/index.js
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
try {
// Fetch metrics
const { data: metricsData, error: metricsError } = await supabase
.from("metrics")
.select("*")
.order("date", { ascending: true })
.limit(timeframe === "7d" ? 7 : timeframe === "30d" ? 30 : 365);
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
if (metricsError) throw metricsError;
setMetrics(metricsData || []);
setComponentLoading(prev => ({ ...prev, metrics: false }));
// Fetch nodes
const { data: nodesData, error: nodesError } = await supabase
.from("node_records")
.select("*")
.order("timestamp", { ascending: false });
if (nodesError) throw nodesError;
setActiveNodes(nodesData || []);
setComponentLoading(prev => ({ ...prev, nodes: false, versions: false, peers: false }));
if (!metricsData?.length && !nodesData?.length) {
throw new Error("No data available");
}
} catch (error) {
console.error("Error fetching data:", error);
setError(error.message);
} finally {
setLoading(false);
}
};
// Pagination helpers
const getPaginatedData = (data, page, itemsPerPage = ITEMS_PER_PAGE) => {
const startIndex = (page - 1) * itemsPerPage;
return data.slice(startIndex, startIndex + itemsPerPage);
};
const getPageCount = (totalItems, itemsPerPage = ITEMS_PER_PAGE) => {
return Math.ceil(totalItems / itemsPerPage);
};
const PaginationControls = ({ currentPage, totalPages, onPageChange, className = "" }) => (
<div className={`flex items-center justify-between ${className}`}>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage <= 1}
className="p-1 text-neutral-400 hover:text-[#7afbaf] disabled:text-neutral-600
hover:bg-neutral-800/50 rounded transition-colors disabled:hover:bg-transparent"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<ChevronLeft className="w-5 h-5" />
</button>
<span className="text-sm text-neutral-400">
Page {currentPage} of {totalPages}
</span>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage >= totalPages}
className="p-1 text-neutral-400 hover:text-[#7afbaf] disabled:text-neutral-600
hover:bg-neutral-800/50 rounded transition-colors disabled:hover:bg-transparent"
>
Read our docs
</a>
<ChevronRight className="w-5 h-5" />
</button>
</div>
);
// Component loading skeleton
const ComponentSkeleton = ({ className = "" }) => (
<div className={`animate-pulse space-y-4 ${className}`}>
<div className="h-6 bg-neutral-800/50 rounded-lg w-1/3" />
<div className="space-y-3">
<div className="h-10 bg-neutral-800/50 rounded-lg" />
<div className="h-10 bg-neutral-800/50 rounded-lg" />
<div className="h-10 bg-neutral-800/50 rounded-lg" />
</div>
</div>
);
const formatLastUpdated = (timestamp) => {
if (!timestamp) return "N/A";
const date = new Date(timestamp);
const time = format(date, "HH:mm");
let dateText;
if (isToday(date)) {
dateText = "Today";
} else if (isYesterday(date)) {
dateText = "Yesterday";
} else {
dateText = format(date, "dd.MM.yyyy");
}
return { time, dateText };
};
// Calculate statistics
const currentActiveNodes = activeNodes.length;
const averagePeerCount = activeNodes.length
? (activeNodes.reduce((acc, node) => acc + node.peer_count, 0) / activeNodes.length).toFixed(1)
: 0;
const activePeerIds = [...new Set(activeNodes.map((node) => node.peer_id))];
const totalNodes = metrics.reduce((acc, day) => acc + day.new_records_count, 0);
const versionDistribution = activeNodes.reduce((acc, node) => {
acc[node.version] = (acc[node.version] || 0) + 1;
return acc;
}, {});
const lastUpdated = formatLastUpdated(activeNodes[0]?.timestamp);
// Get paginated data
const versionEntries = Object.entries(versionDistribution);
const paginatedVersions = getPaginatedData(versionEntries, versionPage);
const displayPeerIds = searchQuery ? searchResults : activePeerIds;
const paginatedPeerIds = getPaginatedData(displayPeerIds, peerPage);
// Calculate total pages
const totalVersionPages = getPageCount(versionEntries.length);
const totalPeerPages = getPageCount(displayPeerIds.length);
return (
<div className="min-h-screen bg-black text-white overflow-x-hidden">
{/* Header */}
<motion.header
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="sticky top-0 z-50 backdrop-blur-xl bg-black/50 border-b border-neutral-800"
>
<div className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<img src="/logo.svg" alt="Codex" className="w-10 h-10" />
<h1 className="text-lg sm:text-xl font-bold">Codex Metrics</h1>
</div>
<div className="flex items-center gap-3">
<select
value={timeframe}
onChange={(e) => setTimeframe(e.target.value)}
className="bg-neutral-900 border border-neutral-800 rounded-lg px-3 py-2 text-sm font-medium
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer outline-none"
>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="1y">Last Year</option>
</select>
<button
onClick={fetchData}
disabled={loading}
className="p-2 text-neutral-400 hover:text-[#7afbaf] disabled:text-neutral-600
bg-neutral-900 border border-neutral-800 rounded-lg
hover:border-neutral-700 disabled:border-neutral-800 disabled:hover:border-neutral-800
focus:border-[#7afbaf] focus:ring-1 focus:ring-[#7afbaf]
transition-colors cursor-pointer disabled:cursor-not-allowed outline-none"
>
<RotateCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
<span className="sr-only">Refresh data</span>
</button>
</div>
</div>
</motion.header>
<main className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 sm:py-6">
{error ? (
<ErrorState message={error} />
) : (
<div className="space-y-4 sm:space-y-6">
{/* Top Section: Stats + Graph */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Left Column - Stats Cards */}
<div className="grid grid-cols-2 lg:grid-cols-1 lg:grid-rows-4 gap-4 lg:h-[450px]">
{[
{
title: "Active Nodes",
value: currentActiveNodes,
Icon: Users,
delay: 0,
isLoading: componentLoading.nodes,
},
{
title: "Average Peer Count",
value: averagePeerCount,
Icon: Network,
delay: 0.1,
isLoading: componentLoading.nodes,
},
{
title: "Total Nodes",
value: totalNodes,
Icon: Database,
delay: 0.2,
isLoading: componentLoading.metrics,
},
{
title: "Last Updated",
value: lastUpdated,
Icon: Clock,
delay: 0.3,
isLoading: componentLoading.nodes,
},
].map((stat) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: stat.delay }}
className="bg-neutral-900 p-4 sm:p-5 rounded-xl hover:bg-neutral-900/80
transition-colors border border-neutral-800 hover:border-neutral-700
flex flex-col justify-between h-full"
>
<h3 className="text-neutral-400 text-sm font-medium flex items-center gap-2 mb-3">
<stat.Icon className="w-4 h-4 opacity-60" />
{stat.title}
</h3>
<div className="mt-auto">
{stat.isLoading ? (
<div className="h-8 bg-neutral-800/50 rounded animate-pulse" />
) : stat.title === "Last Updated" ? (
<div className="flex items-baseline gap-2">
<span className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{lastUpdated.time}
</span>
<span className="text-sm font-medium text-[#7afbaf] opacity-70">
{lastUpdated.dateText}
</span>
</div>
) : (
<p className="text-lg sm:text-xl lg:text-2xl font-bold text-[#7afbaf] tracking-tight">
{stat.value}
</p>
)}
</div>
</motion.div>
))}
</div>
{/* Right Column - Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl h-[350px] lg:h-[450px] border border-neutral-800
hover:border-neutral-700 transition-colors"
>
<h3 className="text-neutral-400 mb-4 font-medium flex items-center gap-2">
<Activity className="w-5 h-5 opacity-60" />
Active Nodes Over Time
</h3>
{componentLoading.metrics ? (
<div className="h-[calc(100%-2rem)] flex items-center justify-center">
<ComponentSkeleton />
</div>
) : metrics.length === 0 ? (
<div className="h-[calc(100%-2rem)] flex items-center justify-center">
<p className="text-neutral-400">No data available for the selected timeframe</p>
</div>
) : (
<div className="h-[calc(100%-2rem)]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={metrics}>
<CartesianGrid
strokeDasharray="3 3"
stroke="#333"
vertical={false}
/>
<XAxis
dataKey="date"
stroke="#666"
tickFormatter={(date) => format(new Date(date), "MMM d")}
fontSize={12}
tickMargin={10}
/>
<YAxis
stroke="#666"
fontSize={12}
tickMargin={10}
axisLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: "#1a1a1a",
border: "1px solid #333",
borderRadius: "8px",
fontFamily: "Inter",
fontSize: "12px",
padding: "12px",
}}
cursor={{ stroke: "#666" }}
/>
<Line
type="monotone"
dataKey="new_records_count"
stroke="#7afbaf"
strokeWidth={2}
dot={false}
activeDot={{ r: 6, fill: "#7afbaf" }}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</motion.div>
</div>
{/* Bottom Section: Version Distribution + Active Peers */}
<div className="grid gap-4 sm:gap-6 lg:grid-cols-2">
{/* Version Distribution */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
>
<h3 className="text-neutral-400 mb-4 sm:mb-6 font-medium flex items-center gap-2">
<Database className="w-5 h-5 opacity-60" />
Version Distribution
</h3>
{componentLoading.versions ? (
<ComponentSkeleton />
) : Object.keys(versionDistribution).length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No version data available</p>
</div>
) : (
<>
<div className="space-y-4 overflow-y-auto flex-1 pr-2 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedVersions.map(([version, count]) => (
<div key={version}>
<div className="flex justify-between mb-2">
<span className="font-medium text-sm sm:text-base">{version}</span>
<span className="font-medium text-sm sm:text-base text-[#7afbaf]">
{count}
</span>
</div>
<div className="w-full bg-neutral-800 rounded-full h-2 overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(count / currentActiveNodes) * 100}%` }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="bg-[#7afbaf] h-2 rounded-full"
/>
</div>
</div>
))}
</div>
<PaginationControls
currentPage={versionPage}
totalPages={totalVersionPages}
onPageChange={setVersionPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
{/* Active Peer IDs List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="bg-neutral-900 p-4 sm:p-6 rounded-xl border border-neutral-800
hover:border-neutral-700 transition-colors h-[300px] lg:h-[350px] flex flex-col"
>
<div className="flex flex-col gap-4 sm:gap-6">
<h3 className="text-neutral-400 font-medium flex items-center gap-2">
<Network className="w-5 h-5 opacity-60" />
Active Peer IDs
</h3>
<div className="relative">
<input
type="text"
placeholder="Search peer IDs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-neutral-800 border border-neutral-700 rounded-lg px-4 py-2
text-sm placeholder-neutral-500 focus:border-[#7afbaf] focus:ring-1
focus:ring-[#7afbaf] transition-colors outline-none"
/>
<Search className="w-4 h-4 text-neutral-500 absolute right-3 top-1/2 -translate-y-1/2" />
</div>
</div>
{componentLoading.peers ? (
<ComponentSkeleton className="mt-4" />
) : activePeerIds.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No active peers available</p>
</div>
) : isSearching ? (
<div className="flex items-center justify-center flex-1">
<RotateCw className="w-6 h-6 text-neutral-400 animate-spin" />
</div>
) : searchQuery && searchResults.length === 0 ? (
<div className="flex items-center justify-center flex-1">
<p className="text-neutral-400">No matching peer IDs found</p>
</div>
) : (
<>
<div className="space-y-2 overflow-y-auto flex-1 pr-2 mt-4 scrollbar-thin
scrollbar-thumb-neutral-700 scrollbar-track-neutral-800">
{paginatedPeerIds.map((peerId, index) => (
<motion.div
key={peerId}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 * (index % 5) }}
className="bg-neutral-800 p-3 rounded-lg text-xs sm:text-sm font-medium
break-all hover:bg-neutral-700/50 transition-colors"
>
{peerId}
</motion.div>
))}
</div>
<PaginationControls
currentPage={peerPage}
totalPages={totalPeerPages}
onPageChange={setPeerPage}
className="mt-4 pt-4 border-t border-neutral-800"
/>
</>
)}
</motion.div>
</div>
</div>
)}
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

11
public/logo.svg Normal file
View File

@ -0,0 +1,11 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_291_2793)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7001 32.7386C19.7705 32.7792 19.8502 32.8001 19.9306 32.8001C20.0111 32.8001 20.188 32.7226 20.188 32.7226L30.85 26.581C30.8555 26.5783 30.8632 26.5744 30.8706 26.5702C30.942 26.5296 31.0002 26.4707 31.0406 26.4008C31.0819 26.3303 31.1036 26.25 31.1036 26.1685C31.1036 26.1588 31.1032 26.1499 31.1028 26.1432V13.8597C31.1036 13.8486 31.1036 13.8387 31.1036 13.8337L31.1036 13.8324C31.1036 13.7507 31.0818 13.671 31.0413 13.6009C31.0008 13.5302 30.9421 13.4718 30.872 13.4313C30.8644 13.4269 30.8564 13.4225 30.8484 13.4186L20.1868 7.27759C20.179 7.27231 20.1714 7.2677 20.165 7.26392L20.1644 7.26357C20.0937 7.22258 20.0136 7.20154 19.9325 7.20154H19.9306C19.8495 7.20154 19.77 7.22298 19.701 7.26236C19.692 7.26736 19.6837 7.2726 19.6762 7.27766L9.01204 13.4202C9.00655 13.423 8.99886 13.4269 8.99144 13.4311C8.92049 13.4717 8.86173 13.5311 8.82123 13.6011C8.78067 13.6712 8.75879 13.751 8.75879 13.8328C8.75879 13.8425 8.75919 13.8514 8.75956 13.858V26.1419C8.75876 26.153 8.75878 26.1629 8.75879 26.1679L8.75879 26.1693C8.75879 26.2509 8.78058 26.3312 8.82193 26.4019C8.86238 26.4712 8.92065 26.5297 8.98909 26.5693L8.99047 26.5701L8.99187 26.5709C9.00023 26.5755 9.00781 26.5793 9.01357 26.5822L19.6768 32.7242C19.6818 32.7275 19.6906 32.7333 19.7001 32.7386ZM30.8023 26.4503C30.7969 26.4534 30.7908 26.4564 30.7847 26.4595C30.788 26.4578 30.7915 26.4561 30.7946 26.4545C30.7973 26.4531 30.7999 26.4517 30.8023 26.4503ZM30.9649 26.1472C30.9651 26.1495 30.9652 26.152 30.9653 26.1545C30.9655 26.159 30.9657 26.1637 30.9657 26.1685C30.9657 26.167 30.9657 26.1655 30.9656 26.164C30.9655 26.1581 30.9652 26.1524 30.9649 26.1472ZM20.3983 26.9772L24.3592 29.2549L20.3951 31.538L20.3983 26.9772ZM15.5034 29.2548L19.4645 26.977L19.4676 31.538L15.5034 29.2548ZM25.7528 23.8924L29.7137 26.1701L25.7497 28.4532L25.7528 23.8924ZM20.3984 25.3628V20.8084L24.3514 23.0856L20.3984 25.3628ZM15.0439 22.2784V17.724L18.9969 20.0012L15.0439 22.2784ZM20.3984 19.1932V14.6389L24.3514 16.916L20.3984 19.1932ZM19.4649 13.0247L15.5038 10.7468L19.468 8.46363L19.4649 13.0247ZM10.1491 26.1701L14.1131 28.4532L14.11 23.8922L10.1491 26.1701ZM24.8194 23.8928L24.8225 28.4527L20.8658 26.1705L24.8194 23.8928ZM15.0438 23.8928L15.0406 28.4527L18.9974 26.1705L15.0438 23.8928ZM26.2198 23.0856L30.178 20.8025V25.3687L26.2198 23.0856ZM9.68483 20.8025V25.3687L13.643 23.0856L9.68483 20.8025ZM15.5111 23.085L19.4644 25.3624V20.8077L15.5111 23.085ZM29.7125 20.0004L25.7529 17.7227V22.2782L29.7125 20.0004ZM10.1503 20.0008L14.1099 22.2786V17.7231L10.1503 20.0008ZM24.8189 17.7233V22.278L20.8656 20.0006L24.8189 17.7233ZM30.178 14.6329V19.1992L26.2198 16.916L30.178 14.6329ZM13.643 16.916L9.68483 19.1992V14.6329L13.643 16.916ZM19.4644 14.6385V19.1928L15.5114 16.9157L19.4644 14.6385ZM29.7141 13.8315L25.7497 11.5482L25.7528 16.1095L29.7141 13.8315ZM14.1135 11.5484L14.1104 16.1095L10.1491 13.8315L14.1135 11.5484ZM24.8225 11.549L24.8194 16.1089L20.8658 13.8312L24.8225 11.549ZM18.9974 13.8312L15.0438 16.1089L15.0406 11.549L18.9974 13.8312ZM24.3596 10.7467L20.3951 8.46362L20.3983 13.0247L24.3596 10.7467Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_291_2793">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -2,20 +2,54 @@
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--background: 0 0% 0%;
--foreground: 0 0% 100%;
--accent: 154 98% 73%;
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
font-family: "Inter", sans-serif;
font-optical-sizing: auto;
font-style: normal;
}
}
.font-thin {
font-weight: 100;
}
.font-extralight {
font-weight: 200;
}
.font-light {
font-weight: 300;
}
.font-normal {
font-weight: 400;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.font-extrabold {
font-weight: 800;
}
.font-black {
font-weight: 900;
}

View File

@ -7,11 +7,13 @@ export default {
],
theme: {
extend: {
fontFamily: {
sans: ["Inter", "sans-serif"],
},
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
accent: "#7afbaf",
},
}
},
plugins: [],
};