mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
✨ feat: Game card
This commit is contained in:
@@ -42,9 +42,9 @@ app.notFound((c) => c.json({ message: 'Not Found', ok: false }, 404))
|
||||
|
||||
app.get('/:id', middleware, async (c) => {
|
||||
const [gameId, imageType] = c.req.param("id").split('.');
|
||||
const width = parseInt(c.req.query("width") || "600");
|
||||
const width = parseInt(c.req.query("width") || "460");
|
||||
//We don't even use this, but let us keep it for future use
|
||||
const height = parseInt(c.req.query("height") || "900");
|
||||
const height = parseInt(c.req.query("height") || "215");
|
||||
if (!gameId || !imageType) {
|
||||
return c.text("Invalid image parameters", 400)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,63 @@
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { HomeNavBar } from "@nestri/ui";
|
||||
import { GameCard, HomeNavBar, Card } from "@nestri/ui";
|
||||
|
||||
function getGreeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) return "Good Morning";
|
||||
if (hour < 18) return "Good Afternoon";
|
||||
return "Good Evening";
|
||||
}
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
<HomeNavBar />
|
||||
<section class="flex flex-col gap-4 justify-center pt-20 items-center w-full text-left pb-4">
|
||||
<div class="flex flex-col gap-4 mx-auto max-w-xl w-full">
|
||||
<h1 class="text-5xl font-bold font-title">{getGreeting()}, Wanjohi</h1>
|
||||
<p class="dark:text-gray-50/70 text-gray-950/70 text-xl">What will you play today?</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="flex flex-col gap-4 justify-center pt-10 items-center w-full text-left pb-4">
|
||||
<div class="flex gap-4 mx-auto max-w-xl w-full">
|
||||
{/* <GameCard
|
||||
game={{
|
||||
release_date: 1478710740000,
|
||||
compatibility: 'playable',
|
||||
name: 'World of Tanks Blitz',
|
||||
appid: '444200',
|
||||
teams: 10
|
||||
}}
|
||||
/><GameCard
|
||||
game={{
|
||||
release_date: 1478710740000,
|
||||
compatibility: 'playable',
|
||||
name: 'World of Tanks Blitz',
|
||||
appid: '444200',
|
||||
teams: 10
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
<div class="gap-4 mx-auto max-w-xl w-full grid grid-cols-1 md:grid-cols-2">
|
||||
<Card
|
||||
game={{
|
||||
// release_date: 1478710740000,
|
||||
// compatibility: 'playable',
|
||||
name: 'The Lord of the Rings: Return to Moria™',
|
||||
id: 2933130,
|
||||
// teams: 10
|
||||
}}
|
||||
/><Card
|
||||
game={{
|
||||
// release_date: 1478710740000,
|
||||
// compatibility: 'playable',
|
||||
name: 'Control Ultimate Edition',
|
||||
id: 870780,
|
||||
// teams: 10
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -22,12 +22,12 @@
|
||||
}
|
||||
|
||||
html.dark *::selection {
|
||||
background-color: theme("colors.primary.900");
|
||||
background-color: theme("colors.primary.800");
|
||||
color: theme("colors.primary.500");
|
||||
}
|
||||
|
||||
html.dark *::-moz-selection {
|
||||
background-color: theme("colors.primary.900");
|
||||
background-color: theme("colors.primary.800");
|
||||
color: theme("colors.primary.500");
|
||||
}
|
||||
|
||||
|
||||
107
packages/ui/src/card.tsx
Normal file
107
packages/ui/src/card.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { component$, useSignal, useVisibleTask$, $ } from "@builder.io/qwik";
|
||||
|
||||
type Props = {
|
||||
game: {
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const Card = component$(({ game }: Props) => {
|
||||
const imageUrl = `http://localhost:8787/image/cover/${game.id}.avif`
|
||||
const backgroundColor = useSignal<string | undefined>(undefined);
|
||||
const ringColor = useSignal<string | undefined>(undefined);
|
||||
const imgRef = useSignal<HTMLImageElement>();
|
||||
|
||||
// Function to extract dominant color
|
||||
const extractColor = $((img: HTMLImageElement) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let r = 0, g = 0, b = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
r += data[i];
|
||||
g += data[i + 1];
|
||||
b += data[i + 2];
|
||||
}
|
||||
|
||||
r = Math.floor(r / (data.length / 4));
|
||||
g = Math.floor(g / (data.length / 4));
|
||||
b = Math.floor(b / (data.length / 4));
|
||||
|
||||
return `rgb(${r},${g},${b})`;
|
||||
});
|
||||
|
||||
// Function to darken a color
|
||||
const darkenColor = $((color: string | undefined, amount: number) => {
|
||||
if (!color) return color;
|
||||
|
||||
const rgb = color.match(/\d+/g);
|
||||
if (!rgb || rgb.length !== 3) return color;
|
||||
|
||||
const darkenChannel = (channel: number) => Math.max(0, channel - amount);
|
||||
const r = darkenChannel(parseInt(rgb[0]));
|
||||
const g = darkenChannel(parseInt(rgb[1]));
|
||||
const b = darkenChannel(parseInt(rgb[2]));
|
||||
|
||||
return `rgb(${r},${g},${b})`;
|
||||
});
|
||||
|
||||
useVisibleTask$(async ({ track }) => {
|
||||
track(() => imgRef.value);
|
||||
|
||||
const img = imgRef.value;
|
||||
if (img) {
|
||||
if (img.complete) {
|
||||
const extractedColor = await extractColor(img);
|
||||
backgroundColor.value = extractedColor;
|
||||
ringColor.value = await darkenColor(extractedColor, 30);
|
||||
} else {
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = async () => {
|
||||
const extractedColor = await extractColor(img);
|
||||
backgroundColor.value = extractedColor;
|
||||
ringColor.value = await darkenColor(extractedColor, 30);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: backgroundColor.value,
|
||||
"--tw-ring-color": ringColor.value
|
||||
}}
|
||||
class="bg-gray-200/70 min-w-[250px] backdrop-blur-sm ring-gray-300 select-none w-full group dark:ring-gray-700 ring dark:bg-gray-800/70 group rounded-3xl dark:text-primary-50/70 text-primary-950/70 duration-300 transition-colors flex flex-col">
|
||||
<header class="flex gap-4 justify-between p-4">
|
||||
<div
|
||||
class="flex relative pr-[22px] overflow-hidden text-white overflow-ellipsis whitespace-nowrap" >
|
||||
<h3 class="overflow-hidden overflow-ellipsis whitespace-nowrap">{game.name}</h3>
|
||||
</div>
|
||||
</header>
|
||||
<section class="flex justify-center items-center w-full py-7">
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imageUrl}
|
||||
class="rounded-2xl shadow-2xl shadow-gray-900"
|
||||
width={270}
|
||||
height={215}
|
||||
alt={game.name}
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
@@ -1,24 +1,7 @@
|
||||
import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
||||
import { buttonVariants, cn } from "@/design";
|
||||
|
||||
const navLinks = [
|
||||
{
|
||||
name: "Changelog",
|
||||
href: "/changelog"
|
||||
},
|
||||
{
|
||||
name: "Pricing",
|
||||
href: "/pricing"
|
||||
},
|
||||
{
|
||||
name: "Login",
|
||||
href: "/login"
|
||||
}
|
||||
]
|
||||
import { cn } from "@/design";
|
||||
|
||||
export const HomeNavBar = component$(() => {
|
||||
const location = useLocation()
|
||||
const hasScrolled = useSignal(false);
|
||||
|
||||
useOnDocument(
|
||||
@@ -29,7 +12,7 @@ export const HomeNavBar = component$(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<nav class={cn("sticky justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-800/70 dark:text-gray-200/70 h-[66px] dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<nav class={cn("sticky justify-between top-0 z-50 px-2 sm:px-6 text-xs sm:text-sm leading-[1] text-gray-950/70 dark:text-gray-50/70 h-[66px] dark:bg-gray-950/70 before:backdrop-blur-[15px] before:absolute before:-z-[1] before:top-0 before:left-0 before:w-full before:h-full flex items-center", hasScrolled.value && "shadow-[0_2px_20px_1px] shadow-gray-200 dark:shadow-gray-800")} >
|
||||
<div class="w-6 h-6 flex-shrink-0 md:mr-2">
|
||||
<svg
|
||||
class="h-full w-full"
|
||||
@@ -45,16 +28,13 @@ export const HomeNavBar = component$(() => {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<hr class="w-[1px] h-7 bg-gray-700/50 dark:bg-gray-300/50 mx-3 rotate-[16deg]" />
|
||||
<button class="rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-2 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<hr class="w-[1px] h-7 bg-neutral-700 dark:bg-neutral-300 mx-3 rotate-[16deg]" />
|
||||
<button class="rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-2 select-none cursor-pointer hover:bg-neutral-300/70 dark:hover:bg-neutral-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/the-avengers.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">The Avengers</p>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 21 21"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7.5 8.5l3-3l3 3m-6 5l3 3l3-3" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* <div class="flex items-center mx-auto w-full h-full max-w-xl border-b-2 border-gray-950/50 dark:border-gray-950/50">
|
||||
|
||||
</div> */}
|
||||
<button class="ml-auto rounded-full transition-all flex items-center duration-200 px-3 h-8 gap-1 select-none cursor-pointer hover:bg-gray-300/70 dark:hover:bg-gray-700/70" >
|
||||
<img src="http://localhost:8787/image/avatar/wanjohi.png" height={16} width={16} class="size-4 rounded-full" alt="Avatar" />
|
||||
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">Wanjohi</p>
|
||||
|
||||
@@ -7,4 +7,5 @@ export * from "./game-card"
|
||||
export * from "./team-counter"
|
||||
export * from "./tooltip"
|
||||
export * from "./footer"
|
||||
export * from "./router-head"
|
||||
export * from "./router-head"
|
||||
export * from "./card"
|
||||
@@ -17,7 +17,7 @@ const transition = {
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
description: string | string[]
|
||||
description?: string | string[]
|
||||
}
|
||||
|
||||
export function ReactTitleSection({ title, description }: Props) {
|
||||
@@ -59,7 +59,7 @@ export function ReactTitleSection({ title, description }: Props) {
|
||||
{title}
|
||||
</motion.span>
|
||||
</ReactDisplay>
|
||||
<motion.p
|
||||
{description && (<motion.p
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 50
|
||||
@@ -78,7 +78,7 @@ export function ReactTitleSection({ title, description }: Props) {
|
||||
{Array.isArray(description) ? description.map((item, index) => {
|
||||
return <span key={`id-${index}`}>{item} <br /> </span>
|
||||
}) : description}
|
||||
</motion.p>
|
||||
</motion.p>)}
|
||||
</div>
|
||||
</header>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user