feat: Game card

This commit is contained in:
Wanjohi
2024-09-01 00:34:50 +03:00
parent 1f9d3be2a6
commit 8b42688bd4
7 changed files with 174 additions and 33 deletions

View File

@@ -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)
}

View File

@@ -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>
</>
)
})

View File

@@ -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
View 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>
)
});

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>