Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,38 @@
@import "tailwindcss";
@import "@nuxt/ui";

@theme {
--color-primary: #023C36;
--color-secondary: #06B2A1;
--color-warning: #927907;
--color-success: #2E933C;
--color-error: #DB222A;

--ui-bg: #FDFCF9;

--ui-text: #3B3830;
--ui-text-highlighted: #1C1B1A;

--ui-color-neutral-50: #FDFCF9;
--ui-color-neutral-100: #F9F6EC;
--ui-color-neutral-200: #F1E9D1;
--ui-color-neutral-300: #B1A991;
--ui-color-neutral-400: #767161;
--ui-color-neutral-500: #47443A;
--ui-color-neutral-600: #3B3830;
--ui-color-neutral-700: #191817;
--ui-color-neutral-800: #1C1B1A;
--ui-color-neutral-900: #100F0E;
}

.dark {
--color-primary: #ece1c1;
--color-secondary: #3F8D84;
--color-success: #297045;
--color-error: #E13D45;
--color-warning: #C3A109;

--ui-bg: #100F0E;
--ui-text: #F1E9D1;
--ui-text-highlighted: #F9F6EC;
}
2 changes: 1 addition & 1 deletion app/components/Base/Category.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defineProps<{
</script>

<template>
<UCard variant="subtle" class="BaseCategory">
<UCard variant="subtle" class="mx-2">
<template #header>
<h2 class="font-semibold">
{{ title }}
Expand Down
12 changes: 9 additions & 3 deletions app/components/Base/Collapsible.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<script setup lang="ts">
defineProps<{
defaultOpen?: boolean
disabled?: boolean
}>()

const { firstSelectedFile } = useFileSelection()
const { fileChanges } = useFileState()

Expand All @@ -12,7 +17,7 @@ const amountOfChanges = computed(() => {
</script>

<template>
<UCollapsible default-open>
<UCollapsible :default-open="defaultOpen" :disabled="disabled">
<UButton
class="group"
color="neutral"
Expand All @@ -23,12 +28,13 @@ const amountOfChanges = computed(() => {
trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200',
}"
block
:disabled="disabled"
>
<div class="flex w-full items-center justify-between">
<div>
<div class="font-semibold">
<slot name="title" />
</div>
<span v-if="amountOfChanges && firstSelectedFile && !firstSelectedFile.isDownloaded" class="text-sm text-gray-400">
<span v-if="amountOfChanges && firstSelectedFile && !firstSelectedFile.isDownloaded && !disabled" class="text-sm text-gray-400">
{{ amountOfChanges }} unsaved change{{ amountOfChanges === 1 ? '' : 's' }}
</span>
</div>
Expand Down
20 changes: 15 additions & 5 deletions app/components/Editor/Container.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const fileId = computed(() => firstSelectedFile.value?.id || '')
</script>

<template>
<div v-if="firstSelectedFile" class="w-full h-full">
<div v-if="firstSelectedFile && selectedState.length > 0" class="w-full h-full pr-4 sm:pr-6">
<EditorFileInformation class="bg-accented/20 rounded-lg" :file-ids="getSelectedIds()" />
<div class="pt-8 pb-14">
<BaseCollapsible v-if="selectedState.length > 0">
<div class="flex flex-col w-full gap-4 pt-8">
<BaseCollapsible :default-open="true">
<template #title>
<span>IPTC-IIM</span>
</template>
Expand All @@ -23,9 +23,19 @@ const fileId = computed(() => firstSelectedFile.value?.id || '')
</div>
</template>
</BaseCollapsible>
<BaseCollapsible :disabled="true">
<template #title>
<div class="flex items-center gap-2">
<span>IPTC Core & Extension</span>
<UBadge color="warning" variant="subtle" size="sm">
COMING SOON
</UBadge>
</div>
</template>
</BaseCollapsible>
</div>
</div>
<div v-else>
<p>No files selected!</p>
<div v-else class="w-full h-[80vh] flex items-center justify-center">
<UEmpty size="xl" variant="naked" title="No file selected" description="Select a file to begin editing its metadata" icon="i-lucide-file-minus" />
</div>
</template>
19 changes: 16 additions & 3 deletions app/components/Editor/SaveButton.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
<script setup lang="ts">
import { supported } from 'browser-fs-access'

const { filesChanged, saveAll } = useFileState()
</script>

<template>
<div>
<UButton :disabled="!filesChanged" icon="i-lucide-save" class="w-full" @click="saveAll">
Save {{ filesChanged }} File{{ filesChanged === 1 ? '' : 's' }}
<div class="flex items-center gap-2">
<UTooltip v-if="!supported" :content="{ align: 'center', side: 'left' }" :ui="{ content: 'h-auto' }" :delay-duration="0">
<template #content>
<p class="max-w-sm text-wrap">
Your browser does not support saving files directly. For the best experience, please consider using a chromium-based browser.
</p>
</template>

<div class="flex items-center justify-center border border-neutral-600 rounded-md bg-accented p-1">
<UIcon name="i-lucide-alert-triangle" size="22" class="text-warning" />
</div>
</UTooltip>
<UButton :disabled="!filesChanged" icon="i-lucide-save" class="w-full font-semibold" @click="saveAll">
{{ supported ? 'Save' : 'Download' }} {{ filesChanged }} File{{ filesChanged === 1 ? '' : 's' }}
</UButton>
</div>
</template>
2 changes: 2 additions & 0 deletions app/components/File/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defineProps<{
const emit = defineEmits<{
(e: 'select', file: FileWithMetadata): void
(e: 'remove', fileId: string): void
(e: 'reset', fileId: string): void
}>()
</script>

Expand All @@ -21,6 +22,7 @@ const emit = defineEmits<{
show-details
@select="emit('select', $event)"
@remove="emit('remove', $event)"
@reset="emit('reset', $event)"
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/File/LoadButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async function openFiles() {

<template>
<div>
<UButton icon="i-lucide-folder-open" class="justify-center w-full" @click="openFiles">
<UButton icon="i-lucide-folder-open" class="justify-center w-full font-semibold" @click="openFiles">
Load Files
</UButton>
</div>
Expand Down
19 changes: 17 additions & 2 deletions app/components/File/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(e: 'select', file: FileWithMetadata): void
(e: 'remove', fileId: string): void
(e: 'reset', fileId: string): void
}>()

const file = computed(() => props.image.file)
Expand All @@ -23,10 +24,21 @@ const fileSize = (file.value.size / 1024).toFixed(2)
const altText = `${file.value.name} - ${fileSize} KB`

const { isSelected } = useFileSelection()
const { fileChanges } = useFileState()

const hasChanged = computed(() => fileChanges(props.image.id) > 0)
</script>

<template>
<div class="flex relative items-center gap-4 border border-accented rounded-lg p-2 hover:bg-accented/20" :class="{ 'border-primary bg-accented/20': isSelected(image.id) }" @click="emit('select', image)">
<div
class="flex relative items-center gap-4 border border-accented rounded-lg p-2 hover:bg-accented/20"
:class="{
'border-primary bg-accented/20': isSelected(image.id),
'border-secondary': hasChanged && !isSelected(image.id),
'bg-secondary/10 hover:bg-secondary/20': hasChanged,
}"
@click="emit('select', image)"
>
<NuxtImg :src="fileUrl" :alt="altText" :width="width" :height="height" />
<div v-if="showDetails" class="flex flex-col max-w-[60%]">
<UTooltip :text="file.name">
Expand All @@ -39,6 +51,9 @@ const { isSelected } = useFileSelection()
<span class="text-default/50 text-sm truncate">{{ formatDate(file.lastModified).value }}</span>
</UTooltip>
</div>
<UButton color="error" variant="ghost" icon="i-lucide-circle-x" class="absolute top-0 right-0 mt-1 mr-1" @click.stop="emit('remove', image.id)" />
<div class="absolute top-0 right-0 flex items-center mr-1 mt-1 gap-1">
<UButton v-if="hasChanged" color="secondary" size="sm" icon="i-lucide-timer-reset" variant="subtle" @click.stop="emit('reset', image.id)" />
<UButton color="error" variant="subtle" size="sm" icon="i-lucide-x" @click.stop="emit('remove', image.id)" />
</div>
</div>
</template>
5 changes: 3 additions & 2 deletions app/components/Layout/Header.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<div class="HeaderHeight px-16 flex justify-between items-center bg-default/75 border-default border-b">
<div>
<h1 class="font-bold text-lg">
<div class="flex items-center gap-2">
<Icon name="i-lucide-clipboard-pen" size="24" />
<h1 class="font-bold text-lg font-sans">
IPTC Web Editor
</h1>
</div>
Expand Down
57 changes: 57 additions & 0 deletions app/components/Modal/Confirm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script setup lang="ts">
withDefaults(defineProps<{
title?: string
description?: string
labels?: {
confirm?: string
cancel?: string
}
icon?: string
}>(), {
title: 'Are you sure?',
labels: () => ({
confirm: 'Confirm',
cancel: 'Cancel',
}),
icon: 'i-lucide-circle-alert',
})

const emit = defineEmits<{
(e: 'confirm'): void
}>()

const open = defineModel<object | boolean | null>({ required: true })
</script>

<template>
<UModal :open="Boolean(open)" @update:open="(value) => !value ? open = null : undefined">
<slot name="trigger" />

<template #title>
<UEmpty
class="ModalConfirmBodyComponent"
variant="naked"
:icon="icon"
:title="title"
:description="description"
>
<template #actions>
<div class="flex flex-col sm:flex-row gap-2 w-full sm:items-center">
<UButton color="primary" variant="subtle" @click="emit('confirm')">
<span class="min-w-[8rem]">{{ labels?.confirm }}</span>
</UButton>
<UButton color="neutral" variant="subtle" @click="open = null">
<span class="min-w-[8rem]">{{ labels?.cancel }}</span>
</UButton>
</div>
</template>
</UEmpty>
</template>
</UModal>
</template>

<style>
div:has(.ModalConfirmBodyComponent) {
width: 100%;
}
</style>
46 changes: 46 additions & 0 deletions app/components/Modal/SupportedBrowserNotice.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
const emit = defineEmits<{
(e: 'close'): void
}>()

const open = defineModel<boolean>({ required: true })
</script>

<template>
<UModal :open="Boolean(open)" @update:open="(value) => !value ? emit('close') : undefined">
<slot name="trigger" />

<template #title>
<UEmpty
class="ModalConfirmBodyComponent"
variant="naked"
icon="i-lucide-circle-alert"
title="Your browser is not fully supported"
>
<template #description>
<div class="text-center space-y-4">
<p>The browser you are using currently does not support directly reading and saving files directly to your local file system. For the best experience with this application, please consider using a supported browser such as Google Chrome.</p>
<p>
For more information about which browsers are supported, please visit
<ULink class="text-blue-400 hover:text-blue-300 underline" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker#browser_compatibility" target="_blank">
the official documentation
</ULink>
</p>
</div>
</template>

<template #actions>
<UButton color="primary" variant="subtle" @click="emit('close')">
<span class="min-w-[16rem]">I understand</span>
</UButton>
</template>
</UEmpty>
</template>
</UModal>
</template>

<style>
div:has(.ModalConfirmBodyComponent) {
width: 100%;
}
</style>
8 changes: 5 additions & 3 deletions app/composables/useFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ export default function useFiles() {
return acc
}, {})

const originatingProgram: string | undefined = import.meta.env.VITE_APP_NAME
const programVersion: string | undefined = import.meta.env.VITE_APP_VERSION
const config = useRuntimeConfig()

const originatingProgram = config.public.appName as string | undefined
const programVersion = config.public.appVersion as string | undefined

if (!originatingProgram || !programVersion) {
console.warn('Could not set originating program and version in metadata update')
Expand All @@ -117,7 +119,7 @@ export default function useFiles() {
const updatedMetadata = {
...file.metadata,
...mappedMetadata,
// Automatically set originating program and version using values from env file if defined
// Automatically set originating program and version using values from package.json
...(originatingProgram ? { '2:65': originatingProgram } : {}),
...(programVersion ? { '2:70': programVersion } : {}),
}
Expand Down
15 changes: 14 additions & 1 deletion app/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { defineNuxtConfig } from 'nuxt/config'
import { name, version } from './package.json'

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-05-15',
compatibilityDate: '2025-11-30',
devtools: { enabled: true },
runtimeConfig: {
public: {
appName: name,
appVersion: version,
},
},
modules: [
'@nuxt/ui',
'@nuxt/icon',
Expand All @@ -19,4 +28,8 @@ export default defineNuxtConfig({
},
],
},
colorMode: {
preference: 'system',
fallback: 'dark',
},
})
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "iptc-web-editor",
"private": true,
"type": "module",
"version": "0.1.0",
"version": "0.2.0",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
Expand Down
Loading