update getJobSkillIcon to accept skill object

This commit is contained in:
Justin Edmund 2025-12-15 16:09:21 -08:00
parent adf38c0c28
commit ed282dfea4
4 changed files with 270 additions and 13 deletions

View file

@ -14,7 +14,7 @@
let { skill, onClick, disabled = false, variant = 'default', onRemove }: Props = $props()
function getSkillIcon(skill: JobSkill): string {
return getJobSkillIcon(skill.slug)
return getJobSkillIcon(skill)
}
function getSkillColorClass(skill: JobSkill): string {

View file

@ -27,7 +27,7 @@
}: Props = $props()
const categoryColor = $derived(skill ? getSkillCategoryColor(skill) : '')
const skillIconUrl = $derived(skill?.slug ? getJobSkillIcon(skill.slug) : '')
const skillIconUrl = $derived(skill ? getJobSkillIcon(skill) : '')
const isEditable = $derived(editable && !locked && available)
const isUnavailable = $derived(!available)

View file

@ -2,20 +2,37 @@
<script lang="ts">
import type { Job, JobSkill } from '$lib/types/api/entities'
import { createQuery } from '@tanstack/svelte-query'
import { jobQueries } from '$lib/api/queries/job.queries'
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
import { jobQueries, jobKeys } from '$lib/api/queries/job.queries'
import { jobAdapter } from '$lib/api/adapters/job.adapter'
import { getJobSkillIcon } from '$lib/utils/images'
import { getSkillCategoryName, getSkillCategoryColor } from '$lib/utils/jobUtils'
import { DropdownMenu } from 'bits-ui'
import Button from '$lib/components/ui/Button.svelte'
import Dialog from '$lib/components/ui/Dialog.svelte'
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
import ModalBody from '$lib/components/ui/ModalBody.svelte'
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
import { openJobSkillEditSidebar } from '../openJobSkillEditSidebar'
interface Props {
job: Job
/** Whether the user can edit (has editor role) */
canEdit?: boolean
}
let { job }: Props = $props()
let { job, canEdit = false }: Props = $props()
const queryClient = useQueryClient()
// Fetch skills for this job
const skillsQuery = createQuery(() => jobQueries.skillsByJob(job.granblueId))
// Delete dialog state
let deleteDialogOpen = $state(false)
let skillToDelete = $state<JobSkill | undefined>(undefined)
let isDeleting = $state(false)
// Group skills by type
const groupedSkills = $derived.by(() => {
const skills = skillsQuery.data ?? []
@ -45,6 +62,46 @@
function hasSkills(group: keyof typeof groupedSkills): boolean {
return groupedSkills[group].length > 0
}
// Open sidebar for creating a new skill
function handleAddSkill() {
openJobSkillEditSidebar({ jobId: job.granblueId })
}
// Open sidebar for editing a skill
function handleEditSkill(skill: JobSkill) {
openJobSkillEditSidebar({ jobId: job.granblueId, skill })
}
// Open delete confirmation dialog
function handleDeleteClick(skill: JobSkill) {
skillToDelete = skill
deleteDialogOpen = true
}
// Cancel delete
function handleDeleteCancel() {
deleteDialogOpen = false
skillToDelete = undefined
}
// Confirm delete
async function handleDeleteConfirm() {
if (!skillToDelete) return
isDeleting = true
try {
await jobAdapter.deleteSkill(job.granblueId, skillToDelete.id)
await queryClient.invalidateQueries({ queryKey: jobKeys.skills(job.granblueId) })
deleteDialogOpen = false
skillToDelete = undefined
} catch (error) {
console.error('Failed to delete skill:', error)
// TODO: Show error toast
} finally {
isDeleting = false
}
}
</script>
<div class="skills-tab">
@ -53,7 +110,12 @@
{:else if skillsQuery.isError}
<div class="error">Failed to load skills</div>
{:else if !skillsQuery.data?.length}
<div class="empty">No skills found for this job</div>
<div class="empty">
<p>No skills found for this job</p>
{#if canEdit}
<Button variant="secondary" onclick={handleAddSkill}>Add Skill</Button>
{/if}
</div>
{:else}
{#if hasSkills('main')}
<section class="skill-group">
@ -61,7 +123,7 @@
<div class="skill-list">
{#each groupedSkills.main as skill}
<div class="skill-item">
<img src={getJobSkillIcon(skill.slug)} alt={skill.name.en} class="skill-icon" />
<img src={getJobSkillIcon(skill)} alt={skill.name.en} class="skill-icon" />
<div class="skill-info">
<span class="skill-name">{skill.name.en}</span>
{#if skill.name.ja}
@ -71,6 +133,32 @@
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
{getSkillCategoryName(skill)}
</span>
{#if canEdit}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="small"
iconOnly
icon="ellipsis"
aria-label="Skill options"
/>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="skill-menu" side="bottom" align="end" sideOffset={4}>
<DropdownMenu.Item class="skill-menu-item" onSelect={() => handleEditSkill(skill)}>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item class="skill-menu-item danger" onSelect={() => handleDeleteClick(skill)}>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
</div>
{/each}
</div>
@ -83,7 +171,7 @@
<div class="skill-list">
{#each groupedSkills.sub as skill}
<div class="skill-item">
<img src={getJobSkillIcon(skill.slug)} alt={skill.name.en} class="skill-icon" />
<img src={getJobSkillIcon(skill)} alt={skill.name.en} class="skill-icon" />
<div class="skill-info">
<span class="skill-name">{skill.name.en}</span>
{#if skill.name.ja}
@ -93,6 +181,32 @@
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
{getSkillCategoryName(skill)}
</span>
{#if canEdit}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="small"
iconOnly
icon="ellipsis"
aria-label="Skill options"
/>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="skill-menu" side="bottom" align="end" sideOffset={4}>
<DropdownMenu.Item class="skill-menu-item" onSelect={() => handleEditSkill(skill)}>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item class="skill-menu-item danger" onSelect={() => handleDeleteClick(skill)}>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
</div>
{/each}
</div>
@ -105,7 +219,7 @@
<div class="skill-list">
{#each groupedSkills.emp as skill}
<div class="skill-item">
<img src={getJobSkillIcon(skill.slug)} alt={skill.name.en} class="skill-icon" />
<img src={getJobSkillIcon(skill)} alt={skill.name.en} class="skill-icon" />
<div class="skill-info">
<span class="skill-name">{skill.name.en}</span>
{#if skill.name.ja}
@ -115,6 +229,32 @@
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
{getSkillCategoryName(skill)}
</span>
{#if canEdit}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="small"
iconOnly
icon="ellipsis"
aria-label="Skill options"
/>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="skill-menu" side="bottom" align="end" sideOffset={4}>
<DropdownMenu.Item class="skill-menu-item" onSelect={() => handleEditSkill(skill)}>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item class="skill-menu-item danger" onSelect={() => handleDeleteClick(skill)}>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
</div>
{/each}
</div>
@ -127,7 +267,7 @@
<div class="skill-list">
{#each groupedSkills.base as skill}
<div class="skill-item">
<img src={getJobSkillIcon(skill.slug)} alt={skill.name.en} class="skill-icon" />
<img src={getJobSkillIcon(skill)} alt={skill.name.en} class="skill-icon" />
<div class="skill-info">
<span class="skill-name">{skill.name.en}</span>
{#if skill.name.ja}
@ -137,14 +277,69 @@
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
{getSkillCategoryName(skill)}
</span>
{#if canEdit}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="ghost"
size="small"
iconOnly
icon="ellipsis"
aria-label="Skill options"
/>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content class="skill-menu" side="bottom" align="end" sideOffset={4}>
<DropdownMenu.Item class="skill-menu-item" onSelect={() => handleEditSkill(skill)}>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item class="skill-menu-item danger" onSelect={() => handleDeleteClick(skill)}>
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
{/if}
</div>
{/each}
</div>
</section>
{/if}
{#if canEdit}
<div class="add-skill-section">
<Button variant="secondary" onclick={handleAddSkill}>Add Skill</Button>
</div>
{/if}
{/if}
</div>
<!-- Delete Confirmation Dialog -->
<Dialog bind:open={deleteDialogOpen}>
{#snippet children()}
<ModalHeader title="Delete Skill?" />
<ModalBody>
<p class="delete-message">
Are you sure you want to delete "{skillToDelete?.name?.en ?? 'this skill'}"?
This action cannot be undone.
</p>
</ModalBody>
<ModalFooter
onCancel={handleDeleteCancel}
cancelDisabled={isDeleting}
primaryAction={{
label: isDeleting ? 'Deleting...' : 'Delete',
onclick: handleDeleteConfirm,
destructive: true,
disabled: isDeleting
}}
/>
{/snippet}
</Dialog>
<style lang="scss">
@use '$src/themes/colors' as colors;
@use '$src/themes/spacing' as spacing;
@ -162,6 +357,10 @@
text-align: center;
padding: spacing.$unit * 4;
color: colors.$grey-50;
p {
margin: 0 0 spacing.$unit-2x 0;
}
}
.error {
@ -232,4 +431,51 @@
color: white;
white-space: nowrap;
}
.add-skill-section {
padding: spacing.$unit-2x;
display: flex;
justify-content: center;
}
.delete-message {
margin: 0;
font-size: typography.$font-regular;
line-height: 1.4;
color: var(--text-primary);
}
// Dropdown menu styles
:global(.skill-menu) {
background: var(--menu-bg, white);
border: 1px solid var(--border-color, #ddd);
border-radius: layout.$card-corner;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: spacing.$unit-half;
min-width: calc(spacing.$unit * 16);
z-index: 200;
}
:global(.skill-menu-item) {
padding: spacing.$unit spacing.$unit-2x;
border-radius: layout.$item-corner-small;
cursor: pointer;
font-size: typography.$font-medium;
color: var(--text-primary);
outline: none;
&:hover,
&:focus {
background: var(--button-bg-hover, #f5f5f5);
}
&.danger {
color: var(--danger, #dc3545);
&:hover,
&:focus {
background: var(--danger-bg, #fff5f5);
}
}
}
</style>

View file

@ -255,10 +255,21 @@ export function getWeaponGridImage(
/**
* Get job skill icon URL
* Uses slug for the image path
*/
export function getJobSkillIcon(slug: string | undefined): string {
if (!slug) return '/images/job-skills/default.png'
return `${getBasePath()}/job-skills/${slug}.png`
export function getJobSkillIcon(skill: { imageId?: string; slug?: string } | string | undefined): string {
if (!skill) return '/images/job-skills/default.png'
// Handle string input (backward compatibility)
if (typeof skill === 'string') {
return `${getBasePath()}/job-skills/${skill}.png`
}
// Use slug for the image path
if (skill.slug) {
return `${getBasePath()}/job-skills/${skill.slug}.png`
}
return '/images/job-skills/default.png'
}
/**