mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +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) => {
|
app.get('/:id', middleware, async (c) => {
|
||||||
const [gameId, imageType] = c.req.param("id").split('.');
|
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
|
//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) {
|
if (!gameId || !imageType) {
|
||||||
return c.text("Invalid image parameters", 400)
|
return c.text("Invalid image parameters", 400)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,63 @@
|
|||||||
import { component$ } from "@builder.io/qwik";
|
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$(() => {
|
export default component$(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HomeNavBar />
|
<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 {
|
html.dark *::selection {
|
||||||
background-color: theme("colors.primary.900");
|
background-color: theme("colors.primary.800");
|
||||||
color: theme("colors.primary.500");
|
color: theme("colors.primary.500");
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark *::-moz-selection {
|
html.dark *::-moz-selection {
|
||||||
background-color: theme("colors.primary.900");
|
background-color: theme("colors.primary.800");
|
||||||
color: theme("colors.primary.500");
|
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 { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
||||||
import { Link, useLocation } from "@builder.io/qwik-city";
|
import { cn } from "@/design";
|
||||||
import { buttonVariants, cn } from "@/design";
|
|
||||||
|
|
||||||
const navLinks = [
|
|
||||||
{
|
|
||||||
name: "Changelog",
|
|
||||||
href: "/changelog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Pricing",
|
|
||||||
href: "/pricing"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Login",
|
|
||||||
href: "/login"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export const HomeNavBar = component$(() => {
|
export const HomeNavBar = component$(() => {
|
||||||
const location = useLocation()
|
|
||||||
const hasScrolled = useSignal(false);
|
const hasScrolled = useSignal(false);
|
||||||
|
|
||||||
useOnDocument(
|
useOnDocument(
|
||||||
@@ -29,7 +12,7 @@ export const HomeNavBar = component$(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<div class="w-6 h-6 flex-shrink-0 md:mr-2">
|
||||||
<svg
|
<svg
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
@@ -45,16 +28,13 @@ export const HomeNavBar = component$(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex items-center">
|
<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]" />
|
<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-gray-300/70 dark:hover:bg-gray-700/70" >
|
<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" />
|
<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>
|
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</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" >
|
<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" />
|
<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>
|
<p class="whitespace-nowrap [text-overflow:ellipsis] overflow-hidden max-w-[20ch]">Wanjohi</p>
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export * from "./team-counter"
|
|||||||
export * from "./tooltip"
|
export * from "./tooltip"
|
||||||
export * from "./footer"
|
export * from "./footer"
|
||||||
export * from "./router-head"
|
export * from "./router-head"
|
||||||
|
export * from "./card"
|
||||||
@@ -17,7 +17,7 @@ const transition = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
description: string | string[]
|
description?: string | string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReactTitleSection({ title, description }: Props) {
|
export function ReactTitleSection({ title, description }: Props) {
|
||||||
@@ -59,7 +59,7 @@ export function ReactTitleSection({ title, description }: Props) {
|
|||||||
{title}
|
{title}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
</ReactDisplay>
|
</ReactDisplay>
|
||||||
<motion.p
|
{description && (<motion.p
|
||||||
initial={{
|
initial={{
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
y: 50
|
y: 50
|
||||||
@@ -78,7 +78,7 @@ export function ReactTitleSection({ title, description }: Props) {
|
|||||||
{Array.isArray(description) ? description.map((item, index) => {
|
{Array.isArray(description) ? description.map((item, index) => {
|
||||||
return <span key={`id-${index}`}>{item} <br /> </span>
|
return <span key={`id-${index}`}>{item} <br /> </span>
|
||||||
}) : description}
|
}) : description}
|
||||||
</motion.p>
|
</motion.p>)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user