diff --git a/app/(content)/(gaming)/games/[gameid]/page.tsx b/app/(content)/(gaming)/games/[gameid]/page.tsx index 74c4ac9dfb8f53a0a7014a474dedca1cfb5a6f30..489cb4920249d8a0883380be24133474b91810bd 100644 --- a/app/(content)/(gaming)/games/[gameid]/page.tsx +++ b/app/(content)/(gaming)/games/[gameid]/page.tsx @@ -1,3 +1,4 @@ +import { Card } from "@/components/ui/card"; import { getGame } from "@/lib/igdb"; import { IGame } from "@/types/igdb-types"; import Image from "next/image"; @@ -6,18 +7,62 @@ import Image from "next/image"; export default async function GameDetail({ params }: { params: { gameid: string } }) { const data: IGame[] = await getGame(parseInt(params.gameid)) + // onvert unix timestamp to date in this format: Feb 25, 2022 + const date = new Date(data[0].first_release_date * 1000).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }) + + const companies = data[0].involved_companies.map((company) => { + // put a comma between each company + if (company !== data[0].involved_companies[0]) { + return `, ${company.company.name}` + } + return company.company.name + }) + return ( - <div> - <h1>Game Detail</h1> - <h1>{data[0].name}</h1> - <Image - src={data[0].cover.url} - alt={data[0].name} - width={264} - height={374} - priority={true} - style={{ width: 'auto', height: 'auto' }} /> - <p>{data[0].summary}</p> + <div className="main-content h-full"> + <Card className="w-full h-full overflow-hidden"> + <div className="h-64 overflow-hidden -top-1/2"> + <div className="aspect-[889/500] relative block group"> + <Image + src={data[0].screenshots[0].url} + alt={data[0].name} + fill + priority + className="object-center" /> + </div> + </div> + <div className="p-6 md:p-12 ss:flex"> + <div className="aspect-[264/374]"> + <Card className="aspect-[264/374] relative block group -mt-36 w-52 flex-shrink-0"> + <Image + src={data[0].cover.url} + alt={data[0].name} + fill + priority + className="object-cover rounded-lg" /> + </Card> + </div> + <div className="ml-6 md:ml-12 grid items-start gap-2"> + <h1>{data[0].name}</h1> + <h1>released on {date} by {companies}</h1> + <h1>{data[0].summary}</h1> + </div> + </div> + <div className="px-6 md:px-12"> + <div className='mt-6 border-b border-gray-400 dark:border-gray-200 ' /> + {/* comments */} + </div> + </Card> + <div> + <Card className="side-content"> + a + </Card> + </div> + </div> ) } \ No newline at end of file diff --git a/app/(content)/(gaming)/games/loading.tsx b/app/(content)/(gaming)/games/loading.tsx index abebd225f3eab27d0100dde9d15d857432af216a..08b23ae0b7f0c7b9f70442e4a7cfd38ced20c335 100644 --- a/app/(content)/(gaming)/games/loading.tsx +++ b/app/(content)/(gaming)/games/loading.tsx @@ -1,8 +1,34 @@ -// root loading component, this renders when any loading happens +import { Card } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; + export default function Loading() { return ( - <div> - <h1>Games Loading...</h1> - </div> + <main className="main-content"> + <div className="flex justify-center"> + <Card className="p-6 w-full"> + <div className="grid grid-cols-1 ss:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-8"> + {Array.from({ length: 18 }, (_, i) => i + 1).map((i) => ( + <Skeleton key={i} className="aspect-[264/374] bg-gray-300" /> + ))} + </div> + </Card> + </div> + <div className="side-content"> + <Card className="p-6 grid items-start gap-2 bg-secondary"> + <Skeleton className="h-6 w-1/4 bg-gray-400 dark:bg-gray-200" /> + + <Skeleton className="h-10 bg-background w-full" /> + + <Skeleton className="h-10 bg-background w-full" /> + + <Skeleton className="h-10 bg-background w-full" /> + + <Skeleton className="h-6 w-1/4 mt-6 bg-gray-400 dark:bg-gray-200" /> + <Skeleton className="h-10 bg-background w-full" /> + + <Skeleton className="h-10 w-full bg-gray-300" /> + </Card> + </div> + </main> ) } \ No newline at end of file diff --git a/app/(content)/(gaming)/games/page.tsx b/app/(content)/(gaming)/games/page.tsx index 81b91728af8c7021d3f6b122f32c00ba5d3b0d9f..f0af8fc9792fa1096349cf9cbe2bc843915f76d8 100644 --- a/app/(content)/(gaming)/games/page.tsx +++ b/app/(content)/(gaming)/games/page.tsx @@ -5,18 +5,16 @@ import ScrollToTop from "@/components/scroll-to-top"; // renders a list of games infinitely export default async function GamesPage() { return ( - <> - <main className="relative lg:gap-10 xl:grid xl:grid-cols-[1fr_240px]"> - <div className="grid"> - <InfiniteScrollGames /> + <main className="main-content"> + <div className="flex justify-center"> + <div className="fixed top-30 z-50"> + <ScrollToTop /> </div> - <div className="hidden xl:block flex-col md:flex"> - <Sort /> - </div> - </main> - <div className="fixed top-6 left-1/2 -ml-7"> - <ScrollToTop /> + <InfiniteScrollGames /> + </div> + <div className="side-content"> + <Sort /> </div> - </> + </main> ) } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index cb7efa4d9556ea875c5bced31c08b5d8a75a3a6e..41fd62b56689cdf2c76f865a41b51691339b618a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -95,6 +95,15 @@ } } +@layer components { + .main-content { + @apply relative md:gap-10 lg:grid lg:grid-cols-[1fr_240px]; + } + .side-content { + @apply hidden lg:block flex-col; + } +} + body { width: 100vw; overflow-x: hidden; diff --git a/app/page.tsx b/app/page.tsx index 1f60c271d256d3d7e6e664594ddd2aff4e741c2b..473b33336e888c8b57607b0ebc04e944d92c4357 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,52 +1,15 @@ -// import { buttonVariants } from '@/components/ui/button' -// import { cn } from '@/lib/utils' -// import Link from 'next/link' - -// export default function Home() { -// return ( -// <> -// <section className="space-y-6 pb-8 pt-6 md:pb-12 md:pt-10 lg:py-32"> -// <div className="container flex max-w-[64rem] flex-col items-center gap-4 text-center"> -// <h1 className="font-heading text-3xl sm:text-5xl md:text-6xl lg:text-7xl"> -// Welcome to GameUnity! -// </h1> -// <p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8"> -// This will be our Home Page and is still WIP -// </p> -// <div className="space-x-4"> -// <Link href="/login" className={cn(buttonVariants({ size: "lg" }))}> -// Log In -// </Link> -// <Link href="/signup" className={cn(buttonVariants({ size: "lg" }))}> -// Sign Up -// </Link> -// </div> -// <div className="flex flex-col space-y-4"> -// <Link href="/games" className={cn(buttonVariants({ size: "lg" }))}> -// Games List Progress -// </Link> -// <Link href="/home" className={cn(buttonVariants({ size: "lg" }))}> -// Home List Progress -// </Link> -// </div> -// </div> -// </section> -// </> -// ) -// } - -import Link from "next/link" -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" -import { SiteFooter } from "@/components/site-footer" import { GameUnityLogo } from "@/components/logo" - - +import { SiteFooter } from "@/components/site-footer" +import { buttonVariants } from "@/components/ui/button" +import { getCurrentUser } from "@/lib/session" +import { cn } from "@/lib/utils" +import Link from "next/link" export default async function IndexPage() { + const user = await getCurrentUser() return ( - <> + <div className="flex flex-col h-screen justify-between"> <section className="space-y-6 pb-8 pt-6 md:pb-12 md:pt-10 lg:py-32"> <div className="container flex max-w-[64rem] flex-col items-center gap-4 text-center"> <div className="flex items-center"> @@ -60,14 +23,15 @@ export default async function IndexPage() { <p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8"> Step into a gaming world beyond imagination. Experience unparalleled features, connect with a vibrant community, and unlock your true gaming potential. Elevate your gameplay, discover new horizons, and make every gaming moment count. Join us and embark on an extraordinary gaming adventure like no other. </p> - <div className="space-x-5"> - <Link href="/login" className={cn(buttonVariants({ size: "lg" }))}> + {!user && <div className="align-middle mb-12"> + <Link href="/login" className={cn(buttonVariants({ size: "lg" }), "mr-6")}> Login </Link> - <Link href="/signup" className={cn(buttonVariants({ size: "lg" }))}> + <span className="text-muted-foreground">or</span> + <Link href="/signup" className={cn(buttonVariants({ size: "lg" }), "ml-6")}> Sign-Up </Link> - </div> + </div>} <Link href="/home" className={cn(buttonVariants({ size: "lg" }))}> Home Feed </Link> @@ -128,6 +92,6 @@ export default async function IndexPage() { </div> </section> <SiteFooter className="border-t" /> - </> + </div> ) } diff --git a/components/game-item.tsx b/components/game-item.tsx index 4fcc90d1e7b61959475fba33103437fdf58d8f2a..32655f8e02097df039dd6d581594e423f3b63085 100644 --- a/components/game-item.tsx +++ b/components/game-item.tsx @@ -5,19 +5,20 @@ import Link from "next/link"; // this is a single game helper-component, only for design purposes export default function Game({ id, name, cover }: { id: number, name: string, cover: { url: string } }) { return ( - <Card> - <Link href={`/games/${id}`}> - <div className="rounded-lg flex items-center justify-center overflow-hidden"> - <Image - src={cover.url} - alt={name} - width={264} - height={374} - priority={true} - style={{ width: '100%', height: '100%' }} /> + <Link href={`/games/${id}`}> + <Card className="aspect-[264/374] relative block group items-center justify-center overflow-hidden"> + <Image + src={cover.url} + alt={name} + fill + priority + className="object-cover rounded-lg" /> + <div className="absolute bottom-0 p-3 group-hover:bg-background w-full"> + <div className="transition-all transform translate-y-6 opacity-0 group-hover:opacity-100 group-hover:translate-y-0"> + <p className="truncate">{name}</p> + </div> </div> - </Link> - <p className="truncate">{name}</p> - </Card> + </Card> + </Link> ) } \ No newline at end of file diff --git a/components/infinity-scroll.tsx b/components/infinity-scroll.tsx index b1651210f22eb1d933213067230fe8e34565582d..c8623c4fa7ffdb6dbfcafb113cf1720b4e10fa6c 100644 --- a/components/infinity-scroll.tsx +++ b/components/infinity-scroll.tsx @@ -2,11 +2,13 @@ import Game from "@/components/game-item"; import { Card } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import { IGame } from "@/types/igdb-types"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useSearchParams } from "next/navigation"; import { Fragment } from "react"; import InfiniteScroll from "react-infinite-scroll-component"; +import { Skeleton } from "./ui/skeleton"; export function InfiniteScrollGames() { const searchParams = useSearchParams() @@ -46,7 +48,7 @@ export function InfiniteScrollGames() { ) return ( - <Card className="p-6"> + <Card className="p-6 w-full"> {status === 'error' ? (<span className="text-center"> @@ -58,26 +60,31 @@ export function InfiniteScrollGames() { dataLength={data?.pages.length * 20} next={fetchNextPage} hasMore={hasNextPage ? true : false} + className="grid grid-cols-1 ss:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-8 items-center" loader={ - <h1 className="text-center pt-6"> - <b>Trying to load more...</b> - </h1> + <> + {Array.from({ length: 8 }, (_, i) => i + 1).map((i) => ( + <Skeleton key={i} className="aspect-[264/374] bg-gray-300 hidden lg:block" /> + ))} + {Array.from({ length: 4 }, (_, i) => i + 1).map((i) => ( + <Skeleton key={i} className="aspect-[264/374] bg-gray-300 lg:hidden" /> + ))} + </> } endMessage={ - <h1 className="text-center pt-6"> - <b>Yay! You have seen it all!</b> - </h1> + <div className="text-center"> + <h1 className="font-bold text-2xl">Yay!<br />You have seen it all!</h1> + + </div> } > - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4 lg:gap-8"> - {data.pages.map((page, i) => ( - <Fragment key={i}> - {page.map((game: IGame) => ( - <Game id={game.id} name={game.name} cover={game.cover} key={game.id} /> - ))} - </Fragment> - ))} - </div> + {data.pages.map((page, i) => ( + <Fragment key={i}> + {page.map((game: IGame) => ( + <Game id={game.id} name={game.name} cover={game.cover} key={game.id} /> + ))} + </Fragment> + ))} </InfiniteScroll> ))} </Card> diff --git a/components/site-footer.tsx b/components/site-footer.tsx index d1d87ab569b91d4e91d015007a0ae8d650cf38b4..1478728bd2cf10e6b73f179cf503aca0be1cba39 100644 --- a/components/site-footer.tsx +++ b/components/site-footer.tsx @@ -8,7 +8,7 @@ export function SiteFooter({ className }: React.HTMLAttributes<HTMLElement>) { <div className="container flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0"> <div className="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0"> <Icons.logoWhite className="h-7 w-7" /> - <span className="pr-6"> + <span className="md:pr-6"> <Icons.logoWhiteName className="" /> </span> <p className="text-white text-center text-sm leading-loose md:text-left"> diff --git a/lib/igdb.ts b/lib/igdb.ts index 93d0d43ff04c4e25e352ebc15503451dd1581f8d..daa3d78b90c2b1686d5a1e6c10dba3804d225cbc 100644 --- a/lib/igdb.ts +++ b/lib/igdb.ts @@ -77,7 +77,11 @@ export async function getGame(id: number): Promise<IGame[]> { 'Client-ID': CLIENT_ID, 'Authorization': `Bearer ${auth.access_token}` }, - body: `fields name, cover.*, summary; where cover != null; where id = ${id};` + body: + `fields *, cover.image_id, screenshots.image_id, involved_companies.company.name; + where total_rating_count > 3 + & cover != null & total_rating != null & rating != null & age_ratings != null + & id = ${id};` }) if (!response.ok) { @@ -87,8 +91,12 @@ export async function getGame(id: number): Promise<IGame[]> { const games = await response.json() as IGame[] games.forEach(game => { - game.cover.url = getImageURL(game.cover.image_id, 'cover_big') - }) + game.cover.url = getImageURL(game.cover.image_id, 'cover_big'); + + game.screenshots.forEach(screenshot => { + screenshot.url = getImageURL(screenshot.image_id, 'screenshot_big'); + }); + }); return games } \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index f4d902d4ae37d56ee6d2f853a6f4fbe2679954a8..fae222d6a7b738e27352d938e19cfa26be2d8748 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,13 +5,21 @@ module.exports = { './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', - ], + ], theme: { + screens: { + "ss": "380px", + "sm": '640px', + "md": '768px', + "lg": '1024px', + "xl": '1280px', + "2xl": '1536px', + }, container: { center: true, padding: "2rem", screens: { - "2xl": "1400px", + "2xl": '1536px', }, }, extend: { diff --git a/types/igdb-types.d.ts b/types/igdb-types.d.ts index 311c0fbaf0ab02a29fc1418b812bc8521a70302c..c18bc051d3f376fbc97c5a8034b74818d99c6d87 100644 --- a/types/igdb-types.d.ts +++ b/types/igdb-types.d.ts @@ -30,7 +30,7 @@ export interface IGame { game_modes: number[]; genres: number[]; hypes: number; - involved_companies: number[]; + involved_companies: IInvolvedCompany[]; keywords: number[]; language_supports: number[]; multiplayer_modes: number[]; @@ -44,7 +44,7 @@ export interface IGame { release_dates: number[]; remakes: number[]; remasters: number[]; - screenshots: number[]; + screenshots: IScreenshots[]; similar_games: number[]; slug: string; standalone_expansions: number[]; @@ -75,6 +75,17 @@ export interface ICover { width: number; } +export interface IScreenshots { + id: number; + alpha_channel: boolean; + animated: boolean; + game: number; + height: number; + image_id: string; + url: string; + width: number; +} + export interface IGenre { id: number; created_at: number; @@ -84,6 +95,22 @@ export interface IGenre { url: string; } +interface IInvolvedCompany { + id: number; + company: { + id: number; + name: string; + }; + created_at: number; + developer: boolean; + game: number; + porting: boolean; + publisher: boolean; + supporting: boolean; + updated_at: number; + checksum: string; +} + export interface IPlatform { id: number; abbreviation: string;