Skip to content
Merged

Api #68

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
184bfde
Simplify DataProvider to be base provider and handle both real and mo…
elysableu Dec 9, 2025
0aec707
Move DataContextType to types directory and added index file to handl…
elysableu Dec 9, 2025
6da92bf
Create ./DataProvider/index.ts to handle exports
elysableu Dec 9, 2025
28ec3c0
Refine dataprovider to be base for seperate providers
elysableu Dec 9, 2025
b007a86
Create seperate provider to handle mock data
elysableu Dec 9, 2025
4bc9b10
Update DataContextType interface
elysableu Dec 9, 2025
864fc5e
Update providers export file
elysableu Dec 9, 2025
0d30e2d
Add missed typing
elysableu Dec 10, 2025
df4ddbe
Created seperate provider to handle data coming in from sg_backend
elysableu Dec 10, 2025
9813efc
Add AppDataProvider to handle which provider is being used
elysableu Dec 10, 2025
f9e1d3a
Update layout to use new provider flow
elysableu Dec 10, 2025
7ebb0b6
Add extractAll helper method
elysableu Dec 10, 2025
b2751c8
Update endpoints
elysableu Dec 10, 2025
1a5782b
Add fetchUserCharacters to api fetch methods
elysableu Dec 10, 2025
7e46266
Update test userId
elysableu Dec 10, 2025
0f5e763
Add call to fetchUserCharacters and set context state with characters…
elysableu Dec 10, 2025
2b3a72f
Fix typo in endpoints
elysableu Dec 10, 2025
e81e02f
Fix linter errors
elysableu Dec 10, 2025
9556775
Fix image element error
elysableu Dec 10, 2025
c152758
Add .waitForURL to fix test error
elysableu Dec 11, 2025
f4c6faf
Add wait times to playwright tests
elysableu Dec 11, 2025
5b29971
USE_MOCK_DATA active
elysableu Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions e2e/helper/positioning.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Locator } from '@playwright/test'

export async function isLeftOf(leftElement: Locator, rightElement: Locator) {
await leftElement.waitFor({ state: 'visible', timeout: 15000 })
await rightElement.waitFor({ state: 'visible', timeout: 15000 })

const leftBox = await leftElement.boundingBox()
const rightBox = await rightElement.boundingBox()

Expand Down
4 changes: 4 additions & 0 deletions e2e/tests/layout/header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ test.describe('Homepage', () => {
test('should navigate to the profile page', async ({ page }) => {
await page.getByRole('link', { name: /Profile/i }).click()

await page.waitForURL('/profile')

await page.waitForLoadState('networkidle')

await playwrightExpect(page).toHaveURL('/profile')
await playwrightExpect(page.getByRole('heading', { name: /Profile/i })).toBeVisible()
})
Expand Down
1 change: 1 addition & 0 deletions e2e/tests/profile/profile-viewing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ test.describe('User Profile Navigation', () => {
test.beforeEach(async ({ page }) => {
// TODO: Login as test user
await page.goto('/profile')
await page.waitForLoadState('networkidle')
})

test('profile displays details on left and character roster on right', async ({ page }) => {
Expand Down
11 changes: 10 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import type { NextConfig } from "next"

const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'ui-avatars.com',
port: '',
pathname: '/api/**'
}
]
}
};

export default nextConfig
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"test": "jest",
Expand Down
6 changes: 3 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Metadata } from 'next'
import { beau_rivage, geistSans, geistMono } from './fonts'
import './globals.css'
import Header from '../components/layout/Header/header'
import { DataProvider } from './providers/DataProvider'
import { AppDataProvider } from '@/app/providers'

export const metadata: Metadata = {
title: 'SilverGuild',
Expand All @@ -19,7 +19,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} ${beau_rivage.variable} antialiased`}
>
<DataProvider userId={4}>
<AppDataProvider userId={7}> {/* Hard set for testing purposes */}
<div className="flex flex-col w-full h-screen">
<div className="top-0 left-0 w-full ">
<Header />
Expand All @@ -28,7 +28,7 @@ export default function RootLayout({
{children}
</main>
</div>
</DataProvider>
</AppDataProvider>
</body>
</html>
)
Expand Down
7 changes: 5 additions & 2 deletions src/app/profile/components/CharacterRosterCard.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from 'next/image'
import { Character } from '@/types/character'

interface CharacterRosterCardProps {
Expand All @@ -8,9 +9,11 @@ export default function CharacterRosterCard({ character }: CharacterRosterCardPr
return (
<div className="flex items-center bg-gray-800 m-2 p-4 rounded-lg w-full">
<div className="w-16 h-16 rounded-full mr-2 mb-4 overflow-hidden shrink-0">
<img
src={`https://ui-avatars.com/api/?name=${character.name}&size=64&background=random`}
<Image
src={`https://ui-avatars.com/api/?name=${character.name}&size=64&background=7B9BB3`}
alt={character.name}
width={80}
height={80}
/>
</div>
<div className="flex-1 w-full text-center">
Expand Down
28 changes: 17 additions & 11 deletions src/app/profile/components/ProfileDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from 'next/image'
import { User } from '@/types/user'

interface ProfileDetailsProps {
Expand All @@ -6,21 +7,26 @@ interface ProfileDetailsProps {

export default function ProfileDetails({ user }: ProfileDetailsProps) {
return (
<div data-testid="profile-details" className="flex items-center bg-gray-800 m-2 p-5 rounded-lg w-1/3">
<div data-testid="profile-details" className="flex-column items-center bg-gray-800 m-2 p-5 rounded-lg w-1/3 h-">
<div className="w-16 h-16 rounded-full mr-2 mb-4 overflow-hidden shrink-0">
<img
data-testid="profile-avatar"
src={`https://ui-avatars.com/api/?name=${user.username}&size=64&background=random`}
alt={user.username}
<Image
data-testid="profile-avatar"
src={`https://ui-avatars.com/api/?name=${user.username}&size=64&background=7B9BB3`}
alt={user.username}
width={120}
height={120}
priority
/>
</div>
<dl>
<dt>Username:</dt>
<dd>{user.username}</dd>
<div>
<dl>
<dt>Username:</dt>
<dd>{user.username}</dd>

<dt>Email:</dt>
<dd>{user.email}</dd>
</dl>
<dt>Email:</dt>
<dd>{user.email}</dd>
</dl>
</div>
</div>
)
}
52 changes: 4 additions & 48 deletions src/app/providers/DataProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,13 @@
'use client'

import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { User } from '@/types/user'
import { Character } from '@/types/character'
import { mockUser, mockCharacters } from '@/mocks'
import { fetchUser } from '@/lib/api'

interface DataContextType {
user: User | null
setUser: (user: User | null) => void
characters: Character[]
setCharacters: (characters: Character[]) => void
loading: boolean
}
import { createContext, useContext, ReactNode } from 'react'
import { DataContextType } from '@/types'

const DataContext = createContext<DataContextType | null>(null)

// Toggle this to switch between mock and real data
const USE_MOCK_DATA = true

export function DataProvider({ children, userId }: { children: ReactNode; userId: number }) {
const [user, setUser] = useState<User | null>(USE_MOCK_DATA ? mockUser : null)
const [characters, setCharacters] = useState<Character[]>(USE_MOCK_DATA ? mockCharacters : [])
const [loading, setLoading] = useState<boolean>(false)

useEffect(() => {
if (!USE_MOCK_DATA && !userId) {
return
}

async function loadData() {
try {
setLoading(true)

if (USE_MOCK_DATA) {
setUser(mockUser)
setCharacters(mockCharacters)
} else {
const userData = await fetchUser(userId)
setUser(userData)
}
} catch (error) {
console.error('Failed to load data:', error)
} finally {
setLoading(false)
}
}

loadData()
}, [userId])

export function DataProvider({ children, value }: { children: ReactNode; value: DataContextType }) {
return (
<DataContext.Provider value={{ user, setUser, characters, setCharacters, loading }}>
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
)
Expand Down
26 changes: 26 additions & 0 deletions src/app/providers/MockDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client'

import { useState, ReactNode } from 'react'
import { DataProvider } from '@/app/providers'
import { User, Character } from '@/types'
import {mockUser, mockCharacters } from '@/mocks'

export function MockDataProvider({ children }: { children: ReactNode}) {
const [ user, setUser ] = useState<User | null>(mockUser)
const [ characters, setCharacters ] = useState<Character[]>(mockCharacters)

return (
<DataProvider
value={{
user,
setUser,
characters,
setCharacters,
loading: false,
isMockData:true
}}
>
{children}
</DataProvider>
)
}
55 changes: 55 additions & 0 deletions src/app/providers/RealDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client'

import { useState, ReactNode, useEffect } from 'react'
import { DataProvider } from '@/app/providers'
import { User, Character } from '@/types'
import { fetchUser, fetchUserCharacters } from '@/lib/api'

export function RealDataProvider({ children, userId }: { children: ReactNode, userId?: number}) {
const [ user, setUser ] = useState<User | null>(null)
const [ characters, setCharacters ] = useState<Character[]>([])
const [ loading, setLoading ] = useState(true)

useEffect(() =>{
if(!userId) {
setLoading(false)
return
}

async function loadData() {
try {
setLoading(true)

const [ userData, characterData ] = await Promise.all([
fetchUser(userId!),
fetchUserCharacters(userId!)
])

setUser(userData)
setCharacters(characterData)

} catch (error) {
console.error('Failed to load data:', error)
} finally {
setLoading(false)
}
}

loadData()
}, [userId])

return (
<DataProvider
value={{
user,
setUser,
characters,
setCharacters,
loading,
isMockData:false
}}
>
{children}
</DataProvider>
)
}
20 changes: 20 additions & 0 deletions src/app/providers/helper/AppDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import { ReactNode } from 'react'
import { MockDataProvider, RealDataProvider } from '@/app/providers'

const USE_MOCK_DATA = true

export function AppDataProvider({
children,
userId
}: {
children: ReactNode,
userId?: number
}) {
if(USE_MOCK_DATA) {
return <MockDataProvider>{children}</MockDataProvider>
}

return <RealDataProvider userId={userId}>{children}</RealDataProvider>
}
4 changes: 4 additions & 0 deletions src/app/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { AppDataProvider } from './helper/AppDataProvider'
export { DataProvider, useData } from './DataProvider'
export { RealDataProvider } from './RealDataProvider'
export { MockDataProvider } from './MockDataProvider'
4 changes: 2 additions & 2 deletions src/components/layout/Header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Link from 'next/link'
import Image from 'next/image'

export default function Header() {
return (
<header className="fixed top-0 w-full bg-black shadow-lg z-50">
<div className="flex justify-between items-center my-4">
<div className="basis-2/10 ml-10">
{/* eslint-disable-next-line @next/next/no-img-element */}
<Link href="/">
<img className="size-28" src="/logo.svg" alt="sg_logo" />
<Image className="size-28" src="/logo.svg" alt="sg_logo" width={120} height={120} priority/>
</Link>
</div>
<nav className="basis-3/10 mr-10">
Expand Down
9 changes: 7 additions & 2 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SG_API_BASE_URL, SG_API_ENDPOINTS } from './config'
import { User } from '@/types/user'
import { JsonApiResponse, extractSingle } from './jsonApiClient'
import { User, Character } from '@/types'
import { JsonApiResponse, extractSingle, extractAll } from './jsonApiClient'

export async function apiRequest<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${SG_API_BASE_URL}${endpoint}`, {
Expand All @@ -22,3 +22,8 @@ export async function fetchUser(id: number): Promise<User> {
const json = await apiRequest<JsonApiResponse<User>>(SG_API_ENDPOINTS.userById(id))
return extractSingle<User>(json)
}

export async function fetchUserCharacters(id: number): Promise<Character[]> {
const json = await apiRequest<JsonApiResponse<Character>>(SG_API_ENDPOINTS.charactersByUserId(id))
return extractAll<Character>(json)
}
7 changes: 2 additions & 5 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ export const APP_NAME = 'SilverGuild'

// API Endpoints
export const SG_API_ENDPOINTS = {
users: '/users',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userById: (id: number) => `/users/${id}`,
characters: '/characters',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
characterById: (id: number) => `/characters/$(id)`,
charactersByUserId: (userId: number) => `/users/${userId}/characters`,
characterById: (id: number) => `/characters/${id}`
} as const
11 changes: 11 additions & 0 deletions src/lib/jsonApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ export function extractSingle<T>(response: JsonApiResponse<T>): T & { id: number
const item = response.data[0]
return { id: item.id, ...item.attributes }
}

export function extractAll<T>(response: JsonApiResponse<T>): Array<T & {id: number }> {
if (!response.data || response.data.length == 0) {
return []
}

return response.data.map(item => ({
id: item.id,
...item.attributes
}))
}
12 changes: 12 additions & 0 deletions src/types/dataContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Dispatch, SetStateAction } from 'react'
import { Character, User } from './index'


export interface DataContextType {
user: User | null
setUser: Dispatch<SetStateAction<User | null>>
characters: Character[]
setCharacters: Dispatch<SetStateAction<Character[]>>
loading: boolean,
isMockData: boolean
}
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { Character } from './character'
export type { User } from './user'
export type { DataContextType } from './dataContext'
Loading