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()
|
let { skill, onClick, disabled = false, variant = 'default', onRemove }: Props = $props()
|
||||||
|
|
||||||
function getSkillIcon(skill: JobSkill): string {
|
function getSkillIcon(skill: JobSkill): string {
|
||||||
return getJobSkillIcon(skill.slug)
|
return getJobSkillIcon(skill)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSkillColorClass(skill: JobSkill): string {
|
function getSkillColorClass(skill: JobSkill): string {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
const categoryColor = $derived(skill ? getSkillCategoryColor(skill) : '')
|
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 isEditable = $derived(editable && !locked && available)
|
||||||
const isUnavailable = $derived(!available)
|
const isUnavailable = $derived(!available)
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,37 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Job, JobSkill } from '$lib/types/api/entities'
|
import type { Job, JobSkill } from '$lib/types/api/entities'
|
||||||
import { createQuery } from '@tanstack/svelte-query'
|
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
||||||
import { jobQueries } from '$lib/api/queries/job.queries'
|
import { jobQueries, jobKeys } from '$lib/api/queries/job.queries'
|
||||||
|
import { jobAdapter } from '$lib/api/adapters/job.adapter'
|
||||||
import { getJobSkillIcon } from '$lib/utils/images'
|
import { getJobSkillIcon } from '$lib/utils/images'
|
||||||
import { getSkillCategoryName, getSkillCategoryColor } from '$lib/utils/jobUtils'
|
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 {
|
interface Props {
|
||||||
job: Job
|
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
|
// Fetch skills for this job
|
||||||
const skillsQuery = createQuery(() => jobQueries.skillsByJob(job.granblueId))
|
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
|
// Group skills by type
|
||||||
const groupedSkills = $derived.by(() => {
|
const groupedSkills = $derived.by(() => {
|
||||||
const skills = skillsQuery.data ?? []
|
const skills = skillsQuery.data ?? []
|
||||||
|
|
@ -45,6 +62,46 @@
|
||||||
function hasSkills(group: keyof typeof groupedSkills): boolean {
|
function hasSkills(group: keyof typeof groupedSkills): boolean {
|
||||||
return groupedSkills[group].length > 0
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="skills-tab">
|
<div class="skills-tab">
|
||||||
|
|
@ -53,7 +110,12 @@
|
||||||
{:else if skillsQuery.isError}
|
{:else if skillsQuery.isError}
|
||||||
<div class="error">Failed to load skills</div>
|
<div class="error">Failed to load skills</div>
|
||||||
{:else if !skillsQuery.data?.length}
|
{: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}
|
{:else}
|
||||||
{#if hasSkills('main')}
|
{#if hasSkills('main')}
|
||||||
<section class="skill-group">
|
<section class="skill-group">
|
||||||
|
|
@ -61,7 +123,7 @@
|
||||||
<div class="skill-list">
|
<div class="skill-list">
|
||||||
{#each groupedSkills.main as skill}
|
{#each groupedSkills.main as skill}
|
||||||
<div class="skill-item">
|
<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">
|
<div class="skill-info">
|
||||||
<span class="skill-name">{skill.name.en}</span>
|
<span class="skill-name">{skill.name.en}</span>
|
||||||
{#if skill.name.ja}
|
{#if skill.name.ja}
|
||||||
|
|
@ -71,6 +133,32 @@
|
||||||
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
||||||
{getSkillCategoryName(skill)}
|
{getSkillCategoryName(skill)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,7 +171,7 @@
|
||||||
<div class="skill-list">
|
<div class="skill-list">
|
||||||
{#each groupedSkills.sub as skill}
|
{#each groupedSkills.sub as skill}
|
||||||
<div class="skill-item">
|
<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">
|
<div class="skill-info">
|
||||||
<span class="skill-name">{skill.name.en}</span>
|
<span class="skill-name">{skill.name.en}</span>
|
||||||
{#if skill.name.ja}
|
{#if skill.name.ja}
|
||||||
|
|
@ -93,6 +181,32 @@
|
||||||
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
||||||
{getSkillCategoryName(skill)}
|
{getSkillCategoryName(skill)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -105,7 +219,7 @@
|
||||||
<div class="skill-list">
|
<div class="skill-list">
|
||||||
{#each groupedSkills.emp as skill}
|
{#each groupedSkills.emp as skill}
|
||||||
<div class="skill-item">
|
<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">
|
<div class="skill-info">
|
||||||
<span class="skill-name">{skill.name.en}</span>
|
<span class="skill-name">{skill.name.en}</span>
|
||||||
{#if skill.name.ja}
|
{#if skill.name.ja}
|
||||||
|
|
@ -115,6 +229,32 @@
|
||||||
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
||||||
{getSkillCategoryName(skill)}
|
{getSkillCategoryName(skill)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,7 +267,7 @@
|
||||||
<div class="skill-list">
|
<div class="skill-list">
|
||||||
{#each groupedSkills.base as skill}
|
{#each groupedSkills.base as skill}
|
||||||
<div class="skill-item">
|
<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">
|
<div class="skill-info">
|
||||||
<span class="skill-name">{skill.name.en}</span>
|
<span class="skill-name">{skill.name.en}</span>
|
||||||
{#if skill.name.ja}
|
{#if skill.name.ja}
|
||||||
|
|
@ -137,14 +277,69 @@
|
||||||
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
<span class="skill-category" style="background: {getSkillCategoryColor(skill)}">
|
||||||
{getSkillCategoryName(skill)}
|
{getSkillCategoryName(skill)}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if canEdit}
|
||||||
|
<div class="add-skill-section">
|
||||||
|
<Button variant="secondary" onclick={handleAddSkill}>Add Skill</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<style lang="scss">
|
||||||
@use '$src/themes/colors' as colors;
|
@use '$src/themes/colors' as colors;
|
||||||
@use '$src/themes/spacing' as spacing;
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
|
@ -162,6 +357,10 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: spacing.$unit * 4;
|
padding: spacing.$unit * 4;
|
||||||
color: colors.$grey-50;
|
color: colors.$grey-50;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 spacing.$unit-2x 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
|
@ -232,4 +431,51 @@
|
||||||
color: white;
|
color: white;
|
||||||
white-space: nowrap;
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -255,10 +255,21 @@ export function getWeaponGridImage(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get job skill icon URL
|
* Get job skill icon URL
|
||||||
|
* Uses slug for the image path
|
||||||
*/
|
*/
|
||||||
export function getJobSkillIcon(slug: string | undefined): string {
|
export function getJobSkillIcon(skill: { imageId?: string; slug?: string } | string | undefined): string {
|
||||||
if (!slug) return '/images/job-skills/default.png'
|
if (!skill) return '/images/job-skills/default.png'
|
||||||
return `${getBasePath()}/job-skills/${slug}.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