mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
feat: ✨ Add file uploads to composer
This commit is contained in:
parent
294740c97f
commit
027847aa03
27 changed files with 718 additions and 31 deletions
|
|
@ -1,25 +1,34 @@
|
|||
import {
|
||||
confirmModalService,
|
||||
confirmModalWithInputService,
|
||||
} from "./service.ts";
|
||||
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
|
||||
export type ConfirmModalOptions = {
|
||||
title?: string;
|
||||
message?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
inputType?: "none" | "text" | "textarea";
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export function useConfirmModal() {
|
||||
const confirm = (
|
||||
options: ConfirmModalOptions,
|
||||
): Promise<ConfirmModalResult> => {
|
||||
return confirmModalService.confirm(options);
|
||||
};
|
||||
export type ConfirmModalResult = {
|
||||
confirmed: boolean;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const confirmWithInput = (
|
||||
options: ConfirmModalOptions,
|
||||
placeholder?: string,
|
||||
): Promise<ConfirmModalResult> => {
|
||||
return confirmModalWithInputService.confirm(options, placeholder);
|
||||
};
|
||||
class ConfirmModalService {
|
||||
private modalRef = ref<{
|
||||
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
|
||||
} | null>(null);
|
||||
|
||||
return {
|
||||
confirm,
|
||||
confirmWithInput,
|
||||
};
|
||||
register(modal: {
|
||||
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
|
||||
}) {
|
||||
this.modalRef.value = modal;
|
||||
}
|
||||
|
||||
confirm(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
|
||||
if (!this.modalRef.value) {
|
||||
throw new Error("Confirmation modal not initialized");
|
||||
}
|
||||
return this.modalRef.value.open(options);
|
||||
}
|
||||
}
|
||||
|
||||
export const confirmModalService = new ConfirmModalService();
|
||||
|
|
|
|||
69
components/modals/confirm-inline.vue
Normal file
69
components/modals/confirm-inline.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<script setup lang="ts">
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { ConfirmModalOptions, ConfirmModalResult } from "./composable.ts";
|
||||
|
||||
defineProps<{
|
||||
modalOptions: ConfirmModalOptions;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
confirm: (result: ConfirmModalResult) => void;
|
||||
cancel: () => void;
|
||||
}>();
|
||||
|
||||
const inputValue = ref<string>("");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog>
|
||||
<DialogTrigger :as-child="true">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ modalOptions.title }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ modalOptions.message }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div v-if="modalOptions.inputType === 'text'" class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="confirmInput" class="text-right">Value</Label>
|
||||
<Input id="confirmInput" v-model="inputValue" class="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="modalOptions.inputType === 'textarea'" class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="confirmTextarea" class="text-right">Value</Label>
|
||||
<Textarea id="confirmTextarea" v-model="inputValue" class="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="() => $emit('cancel')">
|
||||
{{ modalOptions.cancelText }}
|
||||
</Button>
|
||||
<Button @click="() => $emit('confirm', {
|
||||
confirmed: true,
|
||||
value: inputValue,
|
||||
})">
|
||||
{{ modalOptions.confirmText }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
102
components/modals/confirm.vue
Normal file
102
components/modals/confirm.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<script setup lang="ts">
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ref } from "vue";
|
||||
import {
|
||||
type ConfirmModalOptions,
|
||||
type ConfirmModalResult,
|
||||
confirmModalService,
|
||||
} from "./composable.ts";
|
||||
|
||||
const isOpen = ref(false);
|
||||
const modalOptions = ref<ConfirmModalOptions>({
|
||||
title: "Confirm",
|
||||
message: "",
|
||||
inputType: "none",
|
||||
confirmText: "Confirm",
|
||||
cancelText: "Cancel",
|
||||
});
|
||||
const inputValue = ref("");
|
||||
const resolvePromise = ref<((result: ConfirmModalResult) => void) | null>(null);
|
||||
|
||||
function open(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
|
||||
isOpen.value = true;
|
||||
modalOptions.value = {
|
||||
title: options.title || "Confirm",
|
||||
message: options.message,
|
||||
inputType: options.inputType || "none",
|
||||
confirmText: options.confirmText || "Confirm",
|
||||
cancelText: options.cancelText || "Cancel",
|
||||
};
|
||||
inputValue.value = options.defaultValue || "";
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise.value = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (resolvePromise.value) {
|
||||
resolvePromise.value({
|
||||
confirmed: true,
|
||||
value: inputValue.value,
|
||||
});
|
||||
}
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (resolvePromise.value) {
|
||||
resolvePromise.value({
|
||||
confirmed: false,
|
||||
});
|
||||
}
|
||||
isOpen.value = false;
|
||||
}
|
||||
|
||||
confirmModalService.register({
|
||||
open,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialog :key="String(isOpen)" :open="isOpen" @update:open="isOpen = false">
|
||||
<AlertDialogContent class="sm:max-w-[425px] flex flex-col">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{{ modalOptions.title }}</AlertDialogTitle>
|
||||
<AlertDialogDescription v-if="modalOptions.message">
|
||||
{{ modalOptions.message }}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Input v-if="modalOptions.inputType === 'text'" v-model="inputValue" />
|
||||
|
||||
<Textarea v-else-if="modalOptions.inputType === 'textarea'" v-model="inputValue" rows="6" />
|
||||
|
||||
<AlertDialogFooter class="w-full">
|
||||
<AlertDialogCancel :as-child="true">
|
||||
<Button variant="outline" @click="handleCancel">
|
||||
{{ modalOptions.cancelText }}
|
||||
</Button>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction :as-child="true">
|
||||
<Button @click="handleConfirm">
|
||||
{{ modalOptions.confirmText }}
|
||||
</Button>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<template>
|
||||
<HeadlessTransitionRoot as="template" :show="isOpen">
|
||||
<Dialog.Root :open="isOpen" @update:open="handleOpenChange" :close-on-escape="true"
|
||||
:close-on-interact-outside="true">
|
||||
<Teleport to="body">
|
||||
<Dialog.Positioner class="fixed inset-0 z-50 flex items-end md:items-center justify-center md:p-4">
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0"
|
||||
enter-to="opacity-100" leave="ease-in duration-300" leave-from="opacity-100"
|
||||
leave-to="opacity-0">
|
||||
<Dialog.Backdrop class="fixed inset-0 bg-black/70 backdrop-blur-sm" />
|
||||
</HeadlessTransitionChild>
|
||||
|
||||
<HeadlessTransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 scale-95"
|
||||
enter-to="opacity-100 scale-100" leave="ease-in duration-300" leave-from="opacity-100 scale-100"
|
||||
leave-to="opacity-0 scale-95">
|
||||
<Dialog.Content class="relative w-full md:max-w-md p-6 rounded bg-dark-800 ring-1 ring-white/10 shadow-xl">
|
||||
<Dialog.Title class="mb-4 text-lg font-bold tracking-tight text-gray-100 sm:text-xl">
|
||||
{{ modalOptions.title || 'Confirm Action' }}
|
||||
</Dialog.Title>
|
||||
|
||||
<div class="mb-6 text-gray-300">
|
||||
{{ modalOptions.message }}
|
||||
</div>
|
||||
|
||||
<div v-if="withInput" class="mb-4">
|
||||
<input v-model="inputValue" type="text"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:placeholder="inputPlaceholder" />
|
||||
</div>
|
||||
|
||||
<div class="mt-10 grid grid-cols-1 md:grid-cols-2 gap-3 *:!py-2">
|
||||
<Button @click="handleCancel"
|
||||
theme="outline">
|
||||
{{ modalOptions.cancelText || 'Cancel' }}
|
||||
</button>
|
||||
<Button @click="handleConfirm"
|
||||
theme="primary">
|
||||
{{ modalOptions.confirmText || 'Confirm' }}
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</HeadlessTransitionChild>
|
||||
</Dialog.Positioner>
|
||||
</Teleport>
|
||||
</Dialog.Root>
|
||||
</HeadlessTransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Dialog } from "@ark-ui/vue";
|
||||
import Button from "~/packages/ui/components/buttons/button.vue";
|
||||
import {
|
||||
confirmModalService,
|
||||
confirmModalWithInputService,
|
||||
} from "./service.ts";
|
||||
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
|
||||
|
||||
const isOpen = ref(false);
|
||||
const modalOptions = ref<ConfirmModalOptions>({ message: "" });
|
||||
const resolvePromise = ref<((result: ConfirmModalResult) => void) | null>(null);
|
||||
const inputValue = ref("");
|
||||
const withInput = ref(false);
|
||||
const inputPlaceholder = ref("");
|
||||
|
||||
const open = async (
|
||||
options: ConfirmModalOptions,
|
||||
): Promise<ConfirmModalResult> => {
|
||||
modalOptions.value = options;
|
||||
isOpen.value = true;
|
||||
withInput.value = false;
|
||||
inputValue.value = "";
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise.value = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const openWithInput = async (
|
||||
options: ConfirmModalOptions,
|
||||
placeholder = "Enter value",
|
||||
): Promise<ConfirmModalResult> => {
|
||||
modalOptions.value = options;
|
||||
isOpen.value = true;
|
||||
withInput.value = true;
|
||||
inputValue.value = "";
|
||||
inputPlaceholder.value = placeholder;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise.value = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (resolvePromise.value) {
|
||||
resolvePromise.value({
|
||||
confirmed: true,
|
||||
value: withInput.value ? inputValue.value : undefined,
|
||||
});
|
||||
isOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (resolvePromise.value) {
|
||||
resolvePromise.value({ confirmed: false });
|
||||
isOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open && resolvePromise.value) {
|
||||
resolvePromise.value({ confirmed: false });
|
||||
isOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Register the component with the service
|
||||
confirmModalService.register({
|
||||
open,
|
||||
});
|
||||
confirmModalWithInputService.register({
|
||||
open: openWithInput,
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { ref } from "vue";
|
||||
import type { ConfirmModalOptions, ConfirmModalResult } from "./types.ts";
|
||||
|
||||
class ConfirmModalService {
|
||||
private modalRef = ref<{
|
||||
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
|
||||
} | null>(null);
|
||||
|
||||
register(modal: {
|
||||
open: (options: ConfirmModalOptions) => Promise<ConfirmModalResult>;
|
||||
}) {
|
||||
this.modalRef.value = modal;
|
||||
}
|
||||
|
||||
confirm(options: ConfirmModalOptions): Promise<ConfirmModalResult> {
|
||||
if (!this.modalRef.value) {
|
||||
throw new Error("Confirmation modal not initialized");
|
||||
}
|
||||
return this.modalRef.value.open(options);
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmModalWithInputService {
|
||||
private modalRef = ref<{
|
||||
open: (
|
||||
options: ConfirmModalOptions,
|
||||
placeholder?: string,
|
||||
) => Promise<ConfirmModalResult>;
|
||||
} | null>(null);
|
||||
|
||||
register(modal: {
|
||||
open: (
|
||||
options: ConfirmModalOptions,
|
||||
placeholder?: string,
|
||||
) => Promise<ConfirmModalResult>;
|
||||
}) {
|
||||
this.modalRef.value = modal;
|
||||
}
|
||||
|
||||
confirm(
|
||||
options: ConfirmModalOptions,
|
||||
placeholder?: string,
|
||||
): Promise<ConfirmModalResult> {
|
||||
if (!this.modalRef.value) {
|
||||
throw new Error("Confirmation modal not initialized");
|
||||
}
|
||||
return this.modalRef.value.open(options, placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
export const confirmModalService = new ConfirmModalService();
|
||||
export const confirmModalWithInputService = new ConfirmModalWithInputService();
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
export interface ConfirmModalOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export interface ConfirmModalResult {
|
||||
confirmed: boolean;
|
||||
value?: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue