update getJobSkillIcon to accept skill object
This commit is contained in:
parent
adf38c0c28
commit
ed282dfea4
4 changed files with 270 additions and 13 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue