diff --git a/frontend/rabbi_gerzi/src/stores/mediaSets.ts b/frontend/rabbi_gerzi/src/stores/mediaSets.ts
index b899929..e869470 100644
--- a/frontend/rabbi_gerzi/src/stores/mediaSets.ts
+++ b/frontend/rabbi_gerzi/src/stores/mediaSets.ts
@@ -9,16 +9,32 @@ export interface MediaSet {
rootElementId: number | null
}
+export interface CreateMediaSetInput {
+ name: string
+ description: string
+ iconImage: File
+}
+
interface SetsResponse {
sets: MediaSet[]
}
+interface CreateSetResponse {
+ set: MediaSet
+}
+
+interface ErrorResponse {
+ error?: string
+}
+
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
export const useMediaSetsStore = defineStore('mediaSets', () => {
const sets = ref([])
const isLoading = ref(false)
const error = ref(null)
+ const isCreating = ref(false)
+ const createError = ref(null)
async function fetchSets(): Promise {
error.value = null
@@ -43,5 +59,49 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
}
}
- return { sets, isLoading, error, fetchSets }
+ async function createSet(
+ input: CreateMediaSetInput,
+ ): Promise {
+ createError.value = null
+ isCreating.value = true
+
+ const formData = new FormData()
+ formData.append('name', input.name)
+ formData.append('description', input.description)
+ formData.append('iconImage', input.iconImage)
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/sets`, {
+ method: 'POST',
+ credentials: 'include',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ const data: ErrorResponse = await response.json()
+ createError.value = data.error ?? 'Could not create media set'
+ return null
+ }
+
+ const data: CreateSetResponse = await response.json()
+ sets.value = [...sets.value, data.set]
+
+ return data.set
+ } catch {
+ createError.value = 'Network error - could not create media set'
+ return null
+ } finally {
+ isCreating.value = false
+ }
+ }
+
+ return {
+ sets,
+ isLoading,
+ error,
+ isCreating,
+ createError,
+ fetchSets,
+ createSet,
+ }
})
diff --git a/frontend/rabbi_gerzi/src/views/MediaPage.vue b/frontend/rabbi_gerzi/src/views/MediaPage.vue
index c1880cd..513a329 100644
--- a/frontend/rabbi_gerzi/src/views/MediaPage.vue
+++ b/frontend/rabbi_gerzi/src/views/MediaPage.vue
@@ -1,15 +1,121 @@
@@ -29,7 +135,10 @@ onMounted(() => {
>
{{ error }}
-
+
No media sets are available yet.
@@ -68,9 +177,115 @@ onMounted(() => {
+
+
@@ -183,6 +398,205 @@ a.media-page__card:focus-visible {
line-height: 1.38;
}
+.media-page__create-card {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ border-style: dashed;
+ border-color: #b8a16a;
+ background: #fbf8ef;
+ color: var(--color-slate);
+}
+
+.media-page__create-card:hover,
+.media-page__create-card:focus-visible {
+ border-color: #8d7b4e;
+ background: #f7f0db;
+ box-shadow: 0 14px 35px rgb(44 44 44 / 8%);
+ transform: translateY(-2px);
+}
+
+.media-page__create-card:focus-visible {
+ outline: 3px solid rgb(94 107 76 / 35%);
+ outline-offset: 4px;
+}
+
+.media-page__create-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 140px;
+ height: 140px;
+ margin: 0 auto 2.65rem;
+ background: var(--color-white);
+ border: 1px solid #e5cf9f;
+ border-radius: 9999px;
+ color: var(--color-olive);
+}
+
+.media-page__create-title {
+ color: #333333;
+ font-family: var(--font-serif);
+ font-size: clamp(2rem, 3.7vw, 2.4rem);
+ font-weight: 400;
+ line-height: 1.2;
+}
+
+.media-page__create-description {
+ margin-top: 2.15rem;
+ color: var(--color-text-muted);
+ font-family: var(--font-serif);
+ font-size: clamp(1.25rem, 2.3vw, 1.45rem);
+ line-height: 1.38;
+}
+
+.create-set-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1.5rem;
+ background: rgb(31 31 31 / 46%);
+}
+
+.create-set-modal__panel {
+ width: min(100%, 520px);
+ padding: 1.5rem;
+ background: var(--color-white);
+ border: 1px solid var(--color-border);
+ border-radius: 8px;
+ box-shadow: 0 24px 70px rgb(31 31 31 / 20%);
+}
+
+.create-set-modal__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.create-set-modal__title {
+ margin: 0;
+ color: var(--color-slate);
+ font-family: var(--font-serif);
+ font-size: 1.6rem;
+ font-weight: 400;
+ line-height: 1.2;
+}
+
+.create-set-modal__close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.25rem;
+ height: 2.25rem;
+ padding: 0;
+ color: var(--color-slate);
+ background: var(--color-cream);
+ border: 1px solid var(--color-border);
+ border-radius: 9999px;
+}
+
+.create-set-modal__form {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin-top: 1.5rem;
+}
+
+.create-set-modal__field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.create-set-modal__label {
+ color: var(--color-text-muted);
+ font-size: 0.9rem;
+}
+
+.create-set-modal__input,
+.create-set-modal__textarea {
+ width: 100%;
+ padding: 0.7rem 0.8rem;
+ color: var(--color-text);
+ background: var(--color-white);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ font-family: var(--font-sans);
+ font-size: 0.95rem;
+}
+
+.create-set-modal__textarea {
+ resize: vertical;
+}
+
+.create-set-modal__input:focus,
+.create-set-modal__textarea:focus {
+ border-color: var(--color-olive);
+ outline: 3px solid rgb(94 107 76 / 18%);
+}
+
+.create-set-modal__file {
+ width: 100%;
+ color: var(--color-text);
+ font-size: 0.92rem;
+}
+
+.create-set-modal__file-name {
+ color: var(--color-text-muted);
+ font-size: 0.85rem;
+}
+
+.create-set-modal__error {
+ margin: 0;
+ padding: 0.65rem 0.8rem;
+ color: #7c2d2d;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 6px;
+ font-size: 0.9rem;
+}
+
+.create-set-modal__actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ margin-top: 0.5rem;
+}
+
+.create-set-modal__secondary,
+.create-set-modal__submit {
+ min-width: 7.5rem;
+ padding: 0.7rem 1rem;
+ border-radius: 6px;
+ font-size: 0.95rem;
+ font-weight: 600;
+}
+
+.create-set-modal__secondary {
+ color: var(--color-slate);
+ background: var(--color-white);
+ border: 1px solid var(--color-border);
+}
+
+.create-set-modal__submit {
+ color: var(--color-white);
+ background: var(--color-olive);
+ border: 1px solid var(--color-olive);
+}
+
+.create-set-modal__secondary:disabled,
+.create-set-modal__submit:disabled,
+.create-set-modal__close:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+}
+
@media (max-width: 768px) {
.media-page__main {
padding: 3rem 1rem;
@@ -207,5 +621,20 @@ a.media-page__card:focus-visible {
height: 118px;
margin-bottom: 2rem;
}
+
+ .media-page__create-icon {
+ width: 118px;
+ height: 118px;
+ margin-bottom: 2rem;
+ }
+
+ .create-set-modal {
+ align-items: flex-start;
+ padding: 1rem;
+ }
+
+ .create-set-modal__panel {
+ padding: 1.25rem;
+ }
}