add CopyableText component for raw data tab
This commit is contained in:
parent
dea02ef359
commit
2771e202cb
2 changed files with 131 additions and 9 deletions
81
src/lib/components/ui/CopyableText.svelte
Normal file
81
src/lib/components/ui/CopyableText.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
let { value }: Props = $props()
|
||||||
|
|
||||||
|
let copied = $state(false)
|
||||||
|
|
||||||
|
async function copyToClipboard() {
|
||||||
|
if (!value) return
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(String(value))
|
||||||
|
copied = true
|
||||||
|
setTimeout(() => {
|
||||||
|
copied = false
|
||||||
|
}, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="copyable-text" class:copied onclick={copyToClipboard} title="Click to copy">
|
||||||
|
<span class="text">{value}</span>
|
||||||
|
{#if copied}
|
||||||
|
<span class="copied-indicator">Copied!</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
|
.copyable-text {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: colors.$grey-30;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: colors.$grey-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.copied .text {
|
||||||
|
color: colors.$grey-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copied-indicator {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: colors.$grey-50;
|
||||||
|
animation: fadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,34 +3,73 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
import SegmentedControl from '$lib/components/ui/segmented-control/SegmentedControl.svelte'
|
||||||
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
import Segment from '$lib/components/ui/segmented-control/Segment.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import type { EntityRawData } from '$lib/api/adapters/entity.adapter'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wikiRaw?: string | null
|
wikiRaw?: string | null
|
||||||
gameRawEn?: Record<string, unknown> | null
|
gameRawEn?: Record<string, unknown> | null
|
||||||
gameRawJp?: Record<string, unknown> | null
|
gameRawJp?: Record<string, unknown> | null
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
|
canEdit?: boolean
|
||||||
|
onFetchWiki?: () => Promise<EntityRawData>
|
||||||
}
|
}
|
||||||
|
|
||||||
let { wikiRaw, gameRawEn, gameRawJp, isLoading = false }: Props = $props()
|
let { wikiRaw, gameRawEn, gameRawJp, isLoading = false, canEdit = false, onFetchWiki }: Props =
|
||||||
|
$props()
|
||||||
|
|
||||||
let selectedLang = $state('en')
|
let selectedLang = $state('en')
|
||||||
|
let isFetching = $state(false)
|
||||||
|
let fetchError = $state<string | null>(null)
|
||||||
|
|
||||||
const currentGameRaw = $derived(selectedLang === 'en' ? gameRawEn : gameRawJp)
|
const currentGameRaw = $derived(selectedLang === 'en' ? gameRawEn : gameRawJp)
|
||||||
const formattedGameRaw = $derived(
|
const formattedGameRaw = $derived(
|
||||||
currentGameRaw ? JSON.stringify(currentGameRaw, null, 2) : null
|
currentGameRaw ? JSON.stringify(currentGameRaw, null, 2) : null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async function handleFetchWiki() {
|
||||||
|
if (!onFetchWiki) return
|
||||||
|
|
||||||
|
isFetching = true
|
||||||
|
fetchError = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onFetchWiki()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
fetchError = err instanceof Error ? err.message : 'Failed to fetch wiki data'
|
||||||
|
} finally {
|
||||||
|
isFetching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="raw-data-tab">
|
<div class="raw-data-tab">
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<p class="loading">Loading raw data...</p>
|
<p class="loading">Loading raw data...</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#if wikiRaw}
|
<section class="raw-section">
|
||||||
<section class="raw-section">
|
<div class="section-header">
|
||||||
<h3>Wiki Raw</h3>
|
<h3>Wiki Raw</h3>
|
||||||
|
{#if canEdit && onFetchWiki}
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onclick={handleFetchWiki}
|
||||||
|
disabled={isFetching}
|
||||||
|
>
|
||||||
|
{isFetching ? 'Fetching...' : 'Fetch Wiki'}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if fetchError}
|
||||||
|
<p class="error">{fetchError}</p>
|
||||||
|
{/if}
|
||||||
|
{#if wikiRaw}
|
||||||
<pre class="raw-content">{wikiRaw}</pre>
|
<pre class="raw-content">{wikiRaw}</pre>
|
||||||
</section>
|
{:else}
|
||||||
{/if}
|
<p class="no-data">No wiki data available</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
{#if gameRawEn || gameRawJp}
|
{#if gameRawEn || gameRawJp}
|
||||||
<section class="raw-section">
|
<section class="raw-section">
|
||||||
|
|
@ -48,10 +87,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !wikiRaw && !gameRawEn && !gameRawJp}
|
|
||||||
<p class="no-data">No raw data available</p>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -110,4 +145,10 @@
|
||||||
color: colors.$grey-50;
|
color: colors.$grey-50;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: colors.$error;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
margin: 0 0 spacing.$unit 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue