dragoman 0.4.37

Server for scholarly metadata in commonmeta format stored in a SQLite database.
<script>
  import Icon from '@iconify/svelte'
  import { _ } from '@sveltia/i18n'
  import { Button } from '$lib/components/ui/button/index.js'
  import { formatCount } from '$lib/bib-utils.js'
  import rorSvg from './assets/ror.svg'

  let {
    entry, container, bibLocale = 'en-US', index, worksCount = 0,
    copiedCiteId, addedId, isSaved = false, savedFull = false,
    oncopy, onadd,
    dragId, dropPosition, ondragstart, ondragover, ondragleave, ondrop, ondragend,
    ondelete,
    dragHandleActive = $bindable(false),
  } = $props()

  function countryName(code) {
    if (!code) return ''
    try {
      return new Intl.DisplayNames([bibLocale], { type: 'region' }).of(code) || code
    } catch { return code }
  }

  function languageName(code) {
    if (!code) return ''
    try {
      return new Intl.DisplayNames([bibLocale], { type: 'language' }).of(code) || code
    } catch { return code }
  }

  function idValue(type) {
    return container?.identifiers?.find(id => id.identifier_type === type)?.identifier ?? null
  }

  // Derived display data
  let issn      = $derived(idValue('ISSN'))
  let openalex  = $derived(idValue('OpenAlex'))
  let doi       = $derived(idValue('DOI'))
  let datacite  = $derived(idValue('DataCite'))
  let publisher = $derived(container?.publisher?.name ?? '')
  // A ROR-identified publisher (host organization) links to its internal org page
  // with the ROR logo; other publisher ids (e.g. OpenAlex) render as plain text.
  let publisherRor = $derived.by(() => {
    const pid = container?.publisher?.id ?? ''
    return pid.startsWith('https://ror.org/') ? pid : ''
  })

  // Identifiers listed vertically under "Also known as", each an external link.
  // The DOI comes first and renders like elsewhere (work card): the DOI logo
  // followed by the full URL.
  let alsoKnownAs = $derived.by(() => {
    const out = []
    if (doi) {
      const doiUrl = doi.startsWith('http') ? doi : `https://doi.org/${doi}`
      out.push({ doi: true, label: doiUrl, href: doiUrl })
    }
    if (issn)     out.push({ label: `ISSN ${issn}`, href: `https://portal.issn.org/resource/ISSN/${issn}` })
    if (openalex) out.push({ label: `OpenAlex ${openalex.replace('https://openalex.org/', '')}`, href: openalex })
    if (datacite) {
      const dc = datacite.replace(/^https?:\/\/commons\.datacite\.org\/repositories\//, '')
      out.push({ label: `DataCite ${dc}`, href: `https://commons.datacite.org/repositories/${dc}` })
    }
    return out
  })

  // Canonical internal path for the container page (ISSN, then OpenAlex source id, then DOI).
  let selfPath = $derived.by(() => {
    if (issn) return `/${issn}`
    if (openalex) return `/${openalex.replace('https://openalex.org/', '')}`
    if (doi) return `/${doi.replace(/^https?:\/\/doi\.org\//, '')}`
    if (datacite) return `/${datacite.replace(/^https?:\/\/commons\.datacite\.org\/repositories\//, '')}`
    return null
  })

  let hasDetails = $derived(publisher || container?.country || container?.language || container?.url || issn || openalex || doi || datacite)
</script>

{#if container}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
  draggable={dragHandleActive}
  onpointerdown={() => { dragHandleActive = false }}
  ondragstart={e => { if (!dragHandleActive) { e.preventDefault(); return } ondragstart?.(e) }}
  ondragover={ondragover}
  ondragleave={ondragleave}
  ondrop={ondrop}
  ondragend={() => { dragHandleActive = false; ondragend?.() }}
  class="flex items-start gap-3 px-5 py-4 bg-background transition-opacity
         {dragId === entry.id ? 'opacity-30' : ''}
         {dropPosition?.id === entry.id && dropPosition.before  ? 'border-t-2 border-primary' : ''}
         {dropPosition?.id === entry.id && !dropPosition.before ? 'border-b-2 border-primary' : ''}"
>
  {#if index !== null && index !== undefined}
    <span class="text-xs text-muted-foreground w-5 shrink-0 pt-0.5 text-right">{index + 1}.</span>
  {/if}
  <div class="flex-1 min-w-0">
    <p class="text-lg font-semibold leading-snug">
      {#if selfPath}<a href={selfPath} class="hover:underline">{container.title}</a>{:else}{container.title}{/if}
    </p>
    {#if container.additional_titles?.length > 0}
      <p class="text-sm text-muted-foreground mt-1">{container.additional_titles.join(' ยท ')}</p>
    {/if}
    {#if container.description}
      <p class="text-sm text-muted-foreground mt-2 whitespace-pre-line">{container.description}</p>
    {/if}
    <div class="mt-2">
      {#if hasDetails}
        <dl class="space-y-3 text-sm">
          {#if publisher}
            <div>
              <dt class="font-semibold text-foreground">{_('container.publisher')}</dt>
              <dd class="text-muted-foreground">
                {#if publisherRor}
                  <a href={publisherRor.replace('https://ror.org/', '/')} class="hover:underline">{publisher}</a><img src={rorSvg} width="14" height="14" alt="ROR" class="ml-0.5 inline-block align-middle opacity-70 shrink-0" />
                {:else}
                  {publisher}
                {/if}
              </dd>
            </div>
          {/if}
          {#if container.country}
            <div>
              <dt class="font-semibold text-foreground">{_('entity.country')}</dt>
              <dd class="text-muted-foreground">{countryName(container.country)}</dd>
            </div>
          {/if}
          {#if container.language}
            <div>
              <dt class="font-semibold text-foreground">{_('container.language')}</dt>
              <dd class="text-muted-foreground">{languageName(container.language)}</dd>
            </div>
          {/if}
          {#if container.url}
            <div>
              <dt class="font-semibold text-foreground">{_('container.website')}</dt>
              <dd class="text-muted-foreground">
                <a href={container.url} target="_blank" rel="noreferrer" class="text-primary hover:underline">{container.url}</a>
              </dd>
            </div>
          {/if}
          {#if alsoKnownAs.length > 0}
            <div>
              <dt class="font-semibold text-foreground">{_('container.also_known_as')}</dt>
              <dd class="text-muted-foreground space-y-0.5">
                {#each alsoKnownAs as item}
                  <div class="flex items-center leading-5"><a href={item.href} target="_blank" rel="noreferrer" class="text-primary hover:underline inline-flex items-center gap-1">{#if item.doi}<Icon icon="academicons:doi" width="16" height="16" color="#fab608" class="shrink-0" />{/if}{item.label}</a></div>
                {/each}
              </dd>
            </div>
          {/if}
        </dl>
      {/if}
    </div>
    {#if worksCount > 0}
      {@const worksKey = container.type === 'Blog' ? 'container.blog_posts' : 'container.works'}
      <p class="text-sm text-muted-foreground/60 mt-3">
        {_(worksKey, { values: { count: worksCount } }).replace(String(worksCount), formatCount(worksCount, bibLocale))}
      </p>
    {/if}
  </div>
  <div class="flex flex-col gap-0.5 shrink-0">
    {#if onadd && (!isSaved || addedId === entry.id)}
      <Button type="button" variant="ghost" size="icon"
        disabled={savedFull && !isSaved}
        onclick={() => onadd(entry.id, entry.data)}
        aria-label="Add to saved"
        class="h-7 w-7 text-muted-foreground hover:text-foreground"
      >
        {#if addedId === entry.id}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
            <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
          </svg>
        {:else}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
          </svg>
        {/if}
      </Button>
    {/if}
    <Button type="button" variant="ghost" size="icon"
      onclick={oncopy}
      aria-label={_('bibliography.copy_citation')}
      class="h-7 w-7 text-muted-foreground hover:text-foreground"
    >
      {#if copiedCiteId === entry.id}
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
          <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
        </svg>
      {:else}
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
          <path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
        </svg>
      {/if}
    </Button>

    {#if ondelete}
      <Button type="button" variant="ghost" size="icon"
        onclick={ondelete}
        aria-label="Delete entry"
        class="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
      >
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
          <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
        </svg>
      </Button>
    {/if}
  </div>
</div>
{/if}