diff --git a/src/lib/api/adapters/job.adapter.ts b/src/lib/api/adapters/job.adapter.ts index a9407a7d..90f025d8 100644 --- a/src/lib/api/adapters/job.adapter.ts +++ b/src/lib/api/adapters/job.adapter.ts @@ -42,6 +42,19 @@ export interface JobSkillPayload { action_id?: number } +/** + * Payload for creating/updating a job accessory + */ +export interface JobAccessoryPayload { + name_en: string + name_jp?: string + granblue_id: string + rarity?: number + release_date?: string + accessory_type: number // 1 = Shield, 2 = Manatura + job_id?: string +} + /** * Payload for updating a job entity */ @@ -123,15 +136,25 @@ export class JobAdapter extends BaseAdapter { * Gets all accessories available for a specific job * Only returns data if the job supports accessories */ - async getAccessories(jobId: string): Promise { - const response = await this.request<{ accessories: JobAccessory[] }>( - `/jobs/${jobId}/accessories`, - { - method: 'GET', - cacheTTL: 300000 // Cache for 5 minutes - } - ) - return response.accessories + async getAccessoriesForJob(jobId: string): Promise { + return this.request(`/jobs/${jobId}/accessories`, { + method: 'GET', + cacheTTL: 300000 // Cache for 5 minutes + }) + } + + /** + * Creates a new job entity (database admin function) + * @param data The job data + */ + async createJob(data: JobUpdatePayload): Promise { + const response = await this.request('/jobs', { + method: 'POST', + body: data + }) + // Clear jobs cache to reflect the new job + this.clearCache('/jobs') + return response } /** @@ -327,12 +350,78 @@ export class JobAdapter extends BaseAdapter { this.clearCache(`/parties/${partyId}`) } + // ============================================ + // Job Accessory Methods (Database Management) + // ============================================ + + /** + * Gets all job accessories + * @param accessoryType Optional filter by type (1=Shield, 2=Manatura) + */ + async getAllAccessories(accessoryType?: number): Promise { + const params = accessoryType ? `?accessory_type=${accessoryType}` : '' + return this.request(`/job_accessories${params}`, { + method: 'GET', + cacheTTL: 300000 // Cache for 5 minutes + }) + } + + /** + * Gets a single job accessory by ID or granblue_id + */ + async getAccessoryById(id: string): Promise { + return this.request(`/job_accessories/${id}`, { + method: 'GET', + cacheTTL: 300000 // Cache for 5 minutes + }) + } + + /** + * Creates a new job accessory + * @param data The accessory data + */ + async createAccessory(data: JobAccessoryPayload): Promise { + const response = await this.request('/job_accessories', { + method: 'POST', + body: data + }) + this.clearCache('/job_accessories') + return response + } + + /** + * Updates an existing job accessory + * @param id The accessory's ID or granblue_id + * @param data The updated accessory data + */ + async updateAccessory(id: string, data: Partial): Promise { + const response = await this.request(`/job_accessories/${id}`, { + method: 'PUT', + body: data + }) + this.clearCache('/job_accessories') + this.clearCache(`/job_accessories/${id}`) + return response + } + + /** + * Deletes a job accessory + * @param id The accessory's ID or granblue_id + */ + async deleteAccessory(id: string): Promise { + await this.request(`/job_accessories/${id}`, { + method: 'DELETE' + }) + this.clearCache('/job_accessories') + } + /** * Clears the cache for job-related data */ clearJobCache() { this.clearCache('/jobs') this.clearCache('/search/job_skills') + this.clearCache('/job_accessories') } } diff --git a/src/lib/api/adapters/search.adapter.ts b/src/lib/api/adapters/search.adapter.ts index a24a0628..d7a4e557 100644 --- a/src/lib/api/adapters/search.adapter.ts +++ b/src/lib/api/adapters/search.adapter.ts @@ -377,6 +377,67 @@ export class SearchAdapter extends BaseAdapter { }) } + /** + * Searches for jobs with specific filters + * + * @param params - Search parameters with job-specific filters + * @returns Promise resolving to job search results + */ + async searchJobs(params: SearchParams = {}): Promise { + const body: any = { + locale: params.locale || 'en', + page: params.page || 1 + } + + if (params.per) { + body.per = params.per + } + + if (params.query) { + body.query = params.query + } + + if (params.sortBy) { + body.sort = params.sortBy + } + if (params.sortOrder) { + body.order = params.sortOrder + } + + // Build job-specific filters + if (params.filters) { + const filters: any = {} + + if (params.filters.row?.length) { + filters.row = params.filters.row + } + if (params.filters.proficiency?.length) { + filters.proficiency = params.filters.proficiency + } + if (params.filters.masterLevel !== undefined) { + filters.masterLevel = params.filters.masterLevel + } + if (params.filters.ultimateMastery !== undefined) { + filters.ultimateMastery = params.filters.ultimateMastery + } + if (params.filters.accessory !== undefined) { + filters.accessory = params.filters.accessory + } + + if (Object.keys(filters).length > 0) { + body.filters = filters + } + } + + return this.request('/search/jobs', { + method: 'POST', + body: { search: body }, + credentials: 'omit', + cacheTTL: params.query ? 300000 : 0, + headers: params.per ? { 'X-Per-Page': String(params.per) } : undefined + }) + } + /** * Clears all cached search results * Useful when entity data has been updated diff --git a/src/lib/api/adapters/types.ts b/src/lib/api/adapters/types.ts index 2dc76909..3caf9f08 100644 --- a/src/lib/api/adapters/types.ts +++ b/src/lib/api/adapters/types.ts @@ -191,6 +191,23 @@ export interface SearchFilters { /** Filter special characters */ special?: boolean | undefined + // Job-specific filters + + /** Filter by job row (1-5, ex, ex2, o1) */ + row?: string[] | undefined + + /** Filter by proficiency (for jobs - matches either proficiency1 or proficiency2) */ + proficiency?: number[] | undefined + + /** Filter jobs with master level */ + masterLevel?: boolean | undefined + + /** Filter jobs with ultimate mastery */ + ultimateMastery?: boolean | undefined + + /** Filter jobs that support accessories */ + accessory?: boolean | undefined + /** Custom filters for specific use cases */ [key: string]: any } diff --git a/src/lib/api/queries/job.queries.ts b/src/lib/api/queries/job.queries.ts index 16e988eb..5a826e5b 100644 --- a/src/lib/api/queries/job.queries.ts +++ b/src/lib/api/queries/job.queries.ts @@ -140,15 +140,15 @@ export const jobQueries = { }), /** - * Job accessories query options + * Job accessories query options (for a specific job) * * @param jobId - Job ID to fetch accessories for * @returns Query options for fetching job accessories */ - accessories: (jobId: string) => + accessoriesForJob: (jobId: string) => queryOptions({ queryKey: ['jobs', jobId, 'accessories'] as const, - queryFn: () => jobAdapter.getAccessories(jobId), + queryFn: () => jobAdapter.getAccessoriesForJob(jobId), enabled: !!jobId, staleTime: 1000 * 60 * 30, // 30 minutes gcTime: 1000 * 60 * 60 // 1 hour @@ -165,6 +165,39 @@ export const jobQueries = { queryFn: () => jobAdapter.getAllSkills(), staleTime: 1000 * 60 * 30, // 30 minutes gcTime: 1000 * 60 * 60 // 1 hour + }), + + // ============================================ + // Job Accessory Database Queries + // ============================================ + + /** + * All job accessories list query options + * + * @param accessoryType - Optional filter by type (1=Shield, 2=Manatura) + * @returns Query options for fetching all job accessories + */ + accessoriesList: (accessoryType?: number) => + queryOptions({ + queryKey: ['jobAccessories', { accessoryType }] as const, + queryFn: () => jobAdapter.getAllAccessories(accessoryType), + staleTime: 1000 * 60 * 30, // 30 minutes + gcTime: 1000 * 60 * 60 // 1 hour + }), + + /** + * Single job accessory query options + * + * @param id - Accessory ID or granblue_id + * @returns Query options for fetching a single accessory + */ + accessoryById: (id: string) => + queryOptions({ + queryKey: ['jobAccessories', id] as const, + queryFn: () => jobAdapter.getAccessoryById(id), + enabled: !!id, + staleTime: 1000 * 60 * 30, // 30 minutes + gcTime: 1000 * 60 * 60 // 1 hour }) } @@ -193,6 +226,15 @@ export const jobKeys = { empSkills: (jobId: string) => [...jobKeys.all, jobId, 'emp_skills'] as const, skillsSearch: (jobId: string, params?: Omit) => [...jobKeys.skills(jobId), 'search', params] as const, - accessories: (jobId: string) => [...jobKeys.all, jobId, 'accessories'] as const, + accessoriesForJob: (jobId: string) => [...jobKeys.all, jobId, 'accessories'] as const, allSkills: () => [...jobKeys.all, 'skills', 'all'] as const } + +/** + * Job accessory query key helpers for cache invalidation + */ +export const jobAccessoryKeys = { + all: ['jobAccessories'] as const, + lists: (accessoryType?: number) => [...jobAccessoryKeys.all, { accessoryType }] as const, + detail: (id: string) => [...jobAccessoryKeys.all, id] as const +} diff --git a/src/lib/components/Navigation.svelte b/src/lib/components/Navigation.svelte index 76ca7e1d..9609d4d5 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/lib/components/Navigation.svelte @@ -80,6 +80,7 @@ if (path.startsWith(databaseCharactersHref)) return 'character' if (path.startsWith(databaseWeaponsHref)) return 'weapon' if (path.startsWith(databaseSummonsHref)) return 'summon' + if (path.startsWith(databaseJobsHref)) return 'job' if (path.startsWith(databaseRaidsHref) || path.startsWith(databaseRaidGroupsHref)) return 'raid' return null }) @@ -92,9 +93,11 @@ ? 'weapon' : currentDatabaseEntity === 'summon' ? 'summon' - : currentDatabaseEntity === 'raid' - ? 'raid' - : null + : currentDatabaseEntity === 'job' + ? 'job' + : currentDatabaseEntity === 'raid' + ? 'raid' + : null ) const databaseNewHref = $derived( currentDatabaseEntity === 'character' @@ -373,6 +376,13 @@ New raid group + {:else if currentDatabaseEntity === 'job'} + + New job + + + New job accessory + {:else} {#if databaseNewHref} diff --git a/src/lib/components/database/DatabaseGridWithProvider.svelte b/src/lib/components/database/DatabaseGridWithProvider.svelte index 2f7a3a13..22e54886 100644 --- a/src/lib/components/database/DatabaseGridWithProvider.svelte +++ b/src/lib/components/database/DatabaseGridWithProvider.svelte @@ -28,7 +28,7 @@ import type { Snippet } from 'svelte' interface Props { - resource: 'weapons' | 'characters' | 'summons' + resource: 'weapons' | 'characters' | 'summons' | 'jobs' columns: IColumn[] pageSize?: number leftActions?: Snippet @@ -45,9 +45,14 @@ // Derive entity type from resource const entityType = $derived( - resource === 'characters' ? 'character' : resource === 'summons' ? 'summon' : 'weapon' + resource === 'characters' ? 'character' : + resource === 'summons' ? 'summon' : + resource === 'jobs' ? 'job' : 'weapon' ) + // Jobs don't use the standard CollectionFilters component + const supportsCollectionFilters = $derived(resource !== 'jobs') + // Fetch weapon series list for URL slug mapping (only for weapons) const weaponSeriesQuery = createQuery(() => queryOptions({ @@ -403,29 +408,31 @@ {@render headerActions()} {/if} - + {#if supportsCollectionFilters} + + {/if} - {#if showFilters} + {#if showFilters && supportsCollectionFilters}
+ + + +
+ {#if row.masterLevel} + Master + {/if} + {#if row.ultimateMastery} + Ultimate + {/if} + {#if row.accessory} + Accessory + {/if} +
+ + diff --git a/src/lib/components/database/cells/JobIconCell.svelte b/src/lib/components/database/cells/JobIconCell.svelte new file mode 100644 index 00000000..984c259b --- /dev/null +++ b/src/lib/components/database/cells/JobIconCell.svelte @@ -0,0 +1,29 @@ + + + + +
+ {row.name?.en +
+ + diff --git a/src/lib/components/database/cells/JobProficienciesCell.svelte b/src/lib/components/database/cells/JobProficienciesCell.svelte new file mode 100644 index 00000000..d069dc43 --- /dev/null +++ b/src/lib/components/database/cells/JobProficienciesCell.svelte @@ -0,0 +1,26 @@ + + + + +
+ {#if row.proficiency?.[0]} + + {/if} + {#if row.proficiency?.[1]} + + {/if} +
+ + diff --git a/src/lib/components/database/cells/JobTierCell.svelte b/src/lib/components/database/cells/JobTierCell.svelte new file mode 100644 index 00000000..86527ae6 --- /dev/null +++ b/src/lib/components/database/cells/JobTierCell.svelte @@ -0,0 +1,33 @@ + + + + +
+ {tierName} +
+ + diff --git a/src/lib/providers/DatabaseProvider.ts b/src/lib/providers/DatabaseProvider.ts index 07bec809..afef9f55 100644 --- a/src/lib/providers/DatabaseProvider.ts +++ b/src/lib/providers/DatabaseProvider.ts @@ -4,7 +4,7 @@ import type { SearchParams } from '$lib/api/adapters/search.adapter' import type { SearchFilters } from '$lib/api/adapters/types' interface DatabaseProviderOptions { - resource: 'weapons' | 'characters' | 'summons' + resource: 'weapons' | 'characters' | 'summons' | 'jobs' pageSize?: number } @@ -19,7 +19,7 @@ interface APIResponse { } export class DatabaseProvider extends RestDataProvider { - private resource: 'weapons' | 'characters' | 'summons' + private resource: 'weapons' | 'characters' | 'summons' | 'jobs' private pageSize: number private currentPage: number = 1 private totalCount: number = 0 @@ -72,6 +72,9 @@ export class DatabaseProvider extends RestDataProvider { case 'summons': result = await searchAdapter.searchSummons(searchParams) break + case 'jobs': + result = await searchAdapter.searchJobs(searchParams) + break default: throw new Error(`Unknown resource type: ${this.resource}`) } diff --git a/src/lib/types/JobAccessory.d.ts b/src/lib/types/JobAccessory.d.ts index 09a462fc..dffd504f 100644 --- a/src/lib/types/JobAccessory.d.ts +++ b/src/lib/types/JobAccessory.d.ts @@ -1,11 +1,2 @@ -export interface JobAccessory { - id: string - granblue_id: string - job: Job - name: { - [key: string]: string - en: string - ja: string - } - rarity: number -} +// Re-export from entities for backwards compatibility +export type { JobAccessory } from './api/entities' diff --git a/src/lib/types/api/entities.ts b/src/lib/types/api/entities.ts index b5371d91..c3e5827f 100644 --- a/src/lib/types/api/entities.ts +++ b/src/lib/types/api/entities.ts @@ -202,12 +202,15 @@ export interface JobSkill { actionId?: number // Unique game ID } -// JobAccessory entity +// JobAccessory entity from JobAccessoryBlueprint export interface JobAccessory { id: string name: LocalizedName - slug: string granblueId: string + rarity: number + releaseDate?: string + accessoryType: number // 1 = Shield, 2 = Manatura + job?: Job // Associated job (optional, included when available) } // Raid entity from RaidBlueprint diff --git a/src/lib/utils/filterParams.ts b/src/lib/utils/filterParams.ts index ea3730f3..ea4a5e3c 100644 --- a/src/lib/utils/filterParams.ts +++ b/src/lib/utils/filterParams.ts @@ -181,7 +181,7 @@ function parseParamArray( */ export function parseFiltersFromUrl( searchParams: URLSearchParams, - entityType: 'character' | 'weapon' | 'summon', + entityType: 'character' | 'weapon' | 'summon' | 'job', weaponSeriesList?: WeaponSeries[] ): ParsedFilters { const element = parseParamArray(searchParams, 'element', PARAM_TO_ELEMENT) @@ -252,7 +252,7 @@ export function buildUrlFromFilters( filters: CollectionFilterState, searchQuery: string, page: number, - entityType: 'character' | 'weapon' | 'summon', + entityType: 'character' | 'weapon' | 'summon' | 'job', weaponSeriesList?: WeaponSeries[] ): URLSearchParams { const params = new URLSearchParams() diff --git a/src/lib/utils/jobAccessoryUtils.ts b/src/lib/utils/jobAccessoryUtils.ts new file mode 100644 index 00000000..9a89b576 --- /dev/null +++ b/src/lib/utils/jobAccessoryUtils.ts @@ -0,0 +1,50 @@ +/** + * Job Accessory Utilities + * + * Helper functions for working with job accessories (Shields and Manatura). + */ + +/** + * Accessory type constants + */ +export const ACCESSORY_TYPES = { + SHIELD: 1, + MANATURA: 2 +} as const + +export type AccessoryType = (typeof ACCESSORY_TYPES)[keyof typeof ACCESSORY_TYPES] + +/** + * Gets the display name for an accessory type + */ +export function getAccessoryTypeName(type: number): string { + switch (type) { + case ACCESSORY_TYPES.SHIELD: + return 'Shield' + case ACCESSORY_TYPES.MANATURA: + return 'Manatura' + default: + return 'Unknown' + } +} + +/** + * Gets options for accessory type filter/select + */ +export function getAccessoryTypeOptions(): Array<{ value: number; label: string }> { + return [ + { value: ACCESSORY_TYPES.SHIELD, label: 'Shield' }, + { value: ACCESSORY_TYPES.MANATURA, label: 'Manatura' } + ] +} + +/** + * Gets the image URL for a job accessory + * @param granblueId The accessory's granblue_id + * @param accessoryType The type of accessory (1=Shield, 2=Manatura) + */ +export function getJobAccessoryImageUrl(granblueId: string, accessoryType: number): string { + // Different asset paths based on accessory type + const folder = accessoryType === ACCESSORY_TYPES.SHIELD ? 'shield' : 'manatura' + return `/images/job-accessories/${folder}/${granblueId}.png` +} diff --git a/src/lib/utils/jobUtils.ts b/src/lib/utils/jobUtils.ts index f8353d6d..95896364 100644 --- a/src/lib/utils/jobUtils.ts +++ b/src/lib/utils/jobUtils.ts @@ -290,3 +290,34 @@ export function validateSkillConfiguration( errors } } + +/** + * Proficiency options for job forms + */ +export const PROFICIENCIES = [ + { value: 0, label: 'None' }, + { value: 1, label: 'Sabre' }, + { value: 2, label: 'Dagger' }, + { value: 3, label: 'Axe' }, + { value: 4, label: 'Spear' }, + { value: 5, label: 'Bow' }, + { value: 6, label: 'Staff' }, + { value: 7, label: 'Melee' }, + { value: 8, label: 'Harp' }, + { value: 9, label: 'Gun' }, + { value: 10, label: 'Katana' } +] as const + +/** + * Row options for job forms + */ +export const ROWS = [ + { value: '1', label: 'Class I' }, + { value: '2', label: 'Class II' }, + { value: '3', label: 'Class III' }, + { value: '4', label: 'Class IV' }, + { value: '5', label: 'Class V' }, + { value: 'ex', label: 'EX' }, + { value: 'ex2', label: 'EX II' }, + { value: 'o1', label: 'Origin I' } +] as const diff --git a/src/lib/utils/listNavigation.ts b/src/lib/utils/listNavigation.ts index 582aac0e..a72219b7 100644 --- a/src/lib/utils/listNavigation.ts +++ b/src/lib/utils/listNavigation.ts @@ -8,13 +8,13 @@ const STORAGE_KEY = 'database_list_url' interface StoredListUrl { url: string - resource: 'characters' | 'weapons' | 'summons' + resource: 'characters' | 'weapons' | 'summons' | 'jobs' } /** * Store the current list URL before navigating to a detail page */ -export function storeListUrl(url: string, resource: 'characters' | 'weapons' | 'summons'): void { +export function storeListUrl(url: string, resource: 'characters' | 'weapons' | 'summons' | 'jobs'): void { try { const data: StoredListUrl = { url, resource } sessionStorage.setItem(STORAGE_KEY, JSON.stringify(data)) diff --git a/src/routes/(app)/database/job-accessories/[granblueId]/+page.server.ts b/src/routes/(app)/database/job-accessories/[granblueId]/+page.server.ts new file mode 100644 index 00000000..ea4143c3 --- /dev/null +++ b/src/routes/(app)/database/job-accessories/[granblueId]/+page.server.ts @@ -0,0 +1,29 @@ +import type { PageServerLoad } from './$types' +import { jobAdapter } from '$lib/api/adapters/job.adapter' +import { error } from '@sveltejs/kit' + +export const load: PageServerLoad = async ({ params, parent }) => { + try { + // Get parent data to access role + const parentData = await parent() + + const accessory = await jobAdapter.getAccessoryById(params.granblueId) + + if (!accessory) { + throw error(404, 'Job accessory not found') + } + + return { + accessory, + role: parentData.role + } + } catch (err) { + console.error('Failed to load job accessory:', err) + + if (err instanceof Error && 'status' in err && err.status === 404) { + throw error(404, 'Job accessory not found') + } + + throw error(500, 'Failed to load job accessory') + } +} diff --git a/src/routes/(app)/database/job-accessories/[granblueId]/+page.svelte b/src/routes/(app)/database/job-accessories/[granblueId]/+page.svelte new file mode 100644 index 00000000..06919e65 --- /dev/null +++ b/src/routes/(app)/database/job-accessories/[granblueId]/+page.svelte @@ -0,0 +1,222 @@ + + + + + + +
+ {#if accessory} +
+
+ +

{accessory.name.en}

+ {#if accessory.name.ja} +

{accessory.name.ja}

+ {/if} +
+ {#if canEdit && editUrl} + + {/if} +
+ +
+ + + {accessory.name.en} + + + {accessory.name.ja ?? '—'} + + + {accessory.granblueId} + + + + + + + {getAccessoryTypeName(accessory.accessoryType)} + + + + {accessory.rarity ?? '—'} + + + {accessory.releaseDate ?? '—'} + + + + + + {#if accessory.job} + + {accessory.job.name.en} + + {:else} + — + {/if} + + +
+ {:else if accessoryQuery.isLoading} +
Loading accessory...
+ {:else} +
Failed to load accessory
+ {/if} +
+ + diff --git a/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.server.ts b/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.server.ts new file mode 100644 index 00000000..4032b28c --- /dev/null +++ b/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.server.ts @@ -0,0 +1,34 @@ +import type { PageServerLoad } from './$types' +import { jobAdapter } from '$lib/api/adapters/job.adapter' +import { error, redirect } from '@sveltejs/kit' + +export const load: PageServerLoad = async ({ params, parent }) => { + // Get parent data to access role + const parentData = await parent() + + // Check if user has editor role + if (!parentData.role || parentData.role < 7) { + throw redirect(302, `/database/job-accessories/${params.granblueId}`) + } + + try { + const accessory = await jobAdapter.getAccessoryById(params.granblueId) + + if (!accessory) { + throw error(404, 'Job accessory not found') + } + + return { + accessory, + role: parentData.role + } + } catch (err) { + console.error('Failed to load job accessory:', err) + + if (err instanceof Error && 'status' in err && err.status === 404) { + throw error(404, 'Job accessory not found') + } + + throw error(500, 'Failed to load job accessory') + } +} diff --git a/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.svelte b/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.svelte new file mode 100644 index 00000000..c5c23dd7 --- /dev/null +++ b/src/routes/(app)/database/job-accessories/[granblueId]/edit/+page.svelte @@ -0,0 +1,283 @@ + + + + + + +
+ {#if accessory} +
+
+

Edit: {accessory.name.en}

+
+
+ + +
+
+ + {#if saveError} +
{saveError}
+ {/if} + + {#if saveSuccess} +
Changes saved successfully!
+ {/if} + +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ {:else if accessoryQuery.isLoading} +
Loading accessory...
+ {:else} +
Failed to load accessory
+ {/if} +
+ + diff --git a/src/routes/(app)/database/job-accessories/new/+page.server.ts b/src/routes/(app)/database/job-accessories/new/+page.server.ts new file mode 100644 index 00000000..8ac092a4 --- /dev/null +++ b/src/routes/(app)/database/job-accessories/new/+page.server.ts @@ -0,0 +1,16 @@ +import type { PageServerLoad } from './$types' +import { redirect } from '@sveltejs/kit' + +export const load: PageServerLoad = async ({ parent }) => { + // Get parent data to access role + const parentData = await parent() + + // Check if user has editor role + if (!parentData.role || parentData.role < 7) { + throw redirect(302, '/database/jobs?view=accessories') + } + + return { + role: parentData.role + } +} diff --git a/src/routes/(app)/database/job-accessories/new/+page.svelte b/src/routes/(app)/database/job-accessories/new/+page.svelte new file mode 100644 index 00000000..b25f3758 --- /dev/null +++ b/src/routes/(app)/database/job-accessories/new/+page.svelte @@ -0,0 +1,253 @@ + + + + + + +
+
+
+ +

New Job Accessory

+
+
+ + +
+
+ + {#if saveError} +
{saveError}
+ {/if} + +
+ +
+ + +
+
+ + +
+
+ + +
+ + +

The unique game identifier for this accessory

+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + diff --git a/src/routes/(app)/database/jobs/+page.svelte b/src/routes/(app)/database/jobs/+page.svelte index a1e5bcf3..738924a7 100644 --- a/src/routes/(app)/database/jobs/+page.svelte +++ b/src/routes/(app)/database/jobs/+page.svelte @@ -2,143 +2,218 @@
-
-
- -
+ {#if viewMode === 'jobs'} + + + {#snippet leftActions()} + + Jobs + Accessories + + {/snippet} + + {:else} + +
+
+
+ + Jobs + Accessories + - {#if jobsQuery.isLoading} -
Loading jobs...
- {:else if jobsQuery.isError} -
Failed to load jobs
- {:else} -
- - - - - - - - - - - - {#each sortedJobs as job (job.id)} - handleRowClick(job)} class="clickable"> - - - - - + + + + + + + {#if accessoriesQuery.isLoading} +
Loading accessories...
+ {:else if accessoriesQuery.isError} +
Failed to load accessories
+ {:else if sortedAccessories.length === 0} +
No accessories found
+ {:else} +
+
ImageNameRowProficienciesFeatures
- {job.name.en} - -
- {job.name.en} -
-
- {getJobTierName(job.row)} - -
- {#if job.proficiency?.[0]} - - {/if} - {#if job.proficiency?.[1]} - - {/if} -
-
-
- {#if job.masterLevel} - Master - {/if} - {#if job.ultimateMastery} - Ultimate - {/if} - {#if job.accessory} - Accessory - {/if} -
-
+ + + + + + + - {/each} - -
NameTypeJobRarityGranblue ID
-
+ + + {#each sortedAccessories as accessory (accessory.id)} + handleAccessoryRowClick(accessory)} class="clickable"> + +
+ {accessory.name.en} + {#if accessory.name.ja} + {accessory.name.ja} + {/if} +
+ + + + {getAccessoryTypeName(accessory.accessoryType)} + + + + {#if accessory.job} + {accessory.job.name.en} + {:else} + — + {/if} + + + {accessory.rarity ?? '—'} + + + {accessory.granblueId} + + + {/each} + + +
- - {/if} -
+ + {/if} +
+ {/if}