add feedback and report things
This commit is contained in:
77
src/app/website/report/AdversarySearch.svelte
Normal file
77
src/app/website/report/AdversarySearch.svelte
Normal file
@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import Select from '@components/input/Select.svelte';
|
||||
import Search from '@components/admin/search/Search.svelte';
|
||||
import { actions } from 'astro:actions';
|
||||
import { actionErrorPopup } from '@util/action.ts';
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
adversary: { type: 'user'; name: string | null } | { type: 'team'; name: string | null } | { type: 'unknown' };
|
||||
}
|
||||
|
||||
// input
|
||||
const { adversary = $bindable() }: Props = $props();
|
||||
|
||||
// states
|
||||
let adversaryTeamName = $state(adversary.type == 'team' ? adversary.name : null);
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('adversaryInput', {
|
||||
detail: {
|
||||
adversaryTeamName: adversaryTeamName
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// functions
|
||||
async function getSuggestions(query: string) {
|
||||
if (adversary.type == 'user') {
|
||||
const { data, error } = await actions.report.teamNamesByUsername({ username: query });
|
||||
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.teamNames;
|
||||
} else if (adversary.type == 'team') {
|
||||
const { data, error } = await actions.report.teamNamesByTeamName({ teamName: query });
|
||||
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.teamNames;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row space-x-4">
|
||||
<div class="w-1/3">
|
||||
<Select
|
||||
bind:value={adversary.type}
|
||||
values={{
|
||||
unknown: 'Ich möchte einen unbekannten Spieler / ein unbekanntes Team reporten',
|
||||
user: 'Ich möchte einen Spieler reporten',
|
||||
team: 'Ich möchte ein Team reporten'
|
||||
}}
|
||||
dynamicWidth
|
||||
/>
|
||||
</div>
|
||||
{#if adversary.type === 'user' || adversary.type === 'team'}
|
||||
<Search
|
||||
value={adversary.name}
|
||||
requestSuggestions={getSuggestions}
|
||||
onSubmit={(value) => (adversaryTeamName = value != null ? value.value : null)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-base-content/60 text-xs -mt-4" class:hidden={adversary.type !== 'user'}
|
||||
>Reports von Spielern werden immer auf das ganze Team übertragen</span
|
||||
>
|
183
src/app/website/report/Dropzone.svelte
Normal file
183
src/app/website/report/Dropzone.svelte
Normal file
@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import { popupState } from '@components/popup/Popup.ts';
|
||||
|
||||
// bindings
|
||||
let containerElem: HTMLDivElement;
|
||||
let hiddenFileInputElem: HTMLInputElement;
|
||||
let previewDialogElem: HTMLDialogElement;
|
||||
|
||||
// consts
|
||||
const allowedImageTypes = ['image/png', 'image/jpeg', 'image/avif'];
|
||||
const allowedVideoTypes = ['video/mp4'];
|
||||
|
||||
// types
|
||||
interface Props {
|
||||
maxFilesBytes: number;
|
||||
}
|
||||
|
||||
interface UploadFile {
|
||||
dataUrl: string;
|
||||
name: string;
|
||||
type: 'image' | 'video';
|
||||
size: number;
|
||||
file: File;
|
||||
}
|
||||
|
||||
// inputs
|
||||
const { maxFilesBytes }: Props = $props();
|
||||
|
||||
// states
|
||||
let uploadFiles = $state<UploadFile[]>([]);
|
||||
let previewUploadFile = $state<UploadFile | null>(null);
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('dropzoneInput', {
|
||||
detail: {
|
||||
files: uploadFiles.map((uf) => uf.file)
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (previewUploadFile) previewDialogElem.show();
|
||||
});
|
||||
|
||||
// functions
|
||||
function addFiles(files: FileList) {
|
||||
for (const file of files) {
|
||||
if (uploadFiles.find((uf) => uf.name === file.name && uf.size === file.size) !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let type: 'image' | 'video';
|
||||
if (allowedImageTypes.find((mime) => mime === file.type) !== undefined) {
|
||||
type = 'image';
|
||||
} else if (allowedVideoTypes.find((mime) => mime === file.type) !== undefined) {
|
||||
type = 'video';
|
||||
} else {
|
||||
$popupState = {
|
||||
type: 'error',
|
||||
title: 'Ungültige Datei',
|
||||
message:
|
||||
'Das Dateiformat wird nicht unterstützt. Nur Bilder (.png, .jpg, .jpeg, .avif) und Videos (.mp4) sind gültig'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadFiles.reduce((prev, curr) => prev + curr.size, 0) + file.size > maxFilesBytes) {
|
||||
$popupState = {
|
||||
type: 'error',
|
||||
title: 'Datei zu groß',
|
||||
message: `Die Dateien dürfen insgesamt nur ${(maxFilesBytes / 1024 / 1024).toFixed(2)}MB groß sein`
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
uploadFiles.push({
|
||||
dataUrl: reader.result as string,
|
||||
name: file.name,
|
||||
type: type,
|
||||
size: file.size,
|
||||
file: file
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(file: UploadFile) {
|
||||
const index = uploadFiles.findIndex((uf) => uf.size === file.size && uf.name === file.name);
|
||||
const uploadFile = uploadFiles.splice(index, 1).pop()!;
|
||||
URL.revokeObjectURL(uploadFile.dataUrl);
|
||||
}
|
||||
|
||||
function bytesToHumanReadable(bytes: number) {
|
||||
const sizes = ['B', 'KB', 'MB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const size = parseFloat((bytes / Math.pow(1024, i)).toFixed(2));
|
||||
|
||||
return `${size} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
// callbacks
|
||||
function onAddFiles(e: Event & { currentTarget: EventTarget & HTMLInputElement }) {
|
||||
e.preventDefault();
|
||||
if ((e.target as typeof e.currentTarget).files) addFiles((e.target as typeof e.currentTarget).files!);
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) addFiles(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
function onFileRemove(file: UploadFile) {
|
||||
removeFile(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Anhänge</legend>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
bind:this={containerElem}
|
||||
class="h-12 rounded border border-dashed flex cursor-pointer"
|
||||
class:h-26={uploadFiles.length > 0}
|
||||
dropzone="copy"
|
||||
onclick={() => hiddenFileInputElem.click()}
|
||||
ondrop={onDrop}
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
>
|
||||
{#if uploadFiles.length === 0}
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
<p>Hier Dateien droppen oder klicken um sie hochzuladen</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#each uploadFiles as uploadFile (uploadFile.name)}
|
||||
<div
|
||||
class="relative flex flex-col items-center w-22 h-22 m-1 cursor-default"
|
||||
onclick={(e) => e.stopImmediatePropagation()}
|
||||
>
|
||||
<div class="cursor-zoom-in" onclick={() => (previewUploadFile = uploadFile)}>
|
||||
{#if uploadFile.type === 'image'}
|
||||
<img src={uploadFile.dataUrl} alt={uploadFile.name} class="w-16 h-16" />
|
||||
{:else if uploadFile.type === 'video'}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video src={uploadFile.dataUrl} class="w-16 h-16"></video>
|
||||
{/if}
|
||||
</div>
|
||||
<span>{bytesToHumanReadable(uploadFile.size)}</span>
|
||||
<button class="cursor-pointer" onclick={() => onFileRemove(uploadFile)}>Datei entfernen</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
<input
|
||||
bind:this={hiddenFileInputElem}
|
||||
type="file"
|
||||
multiple
|
||||
accept={[...allowedImageTypes, ...allowedVideoTypes].join(', ')}
|
||||
class="hidden absolute top-0 left-0 h-0 w-0"
|
||||
onchange={onAddFiles}
|
||||
/>
|
||||
|
||||
<dialog class="modal" bind:this={previewDialogElem} onclose={() => setTimeout(() => (previewUploadFile = null), 300)}>
|
||||
<div class="modal-box">
|
||||
{#if previewUploadFile?.type === 'image'}
|
||||
<img src={previewUploadFile.dataUrl} alt={previewUploadFile.name} />
|
||||
{:else if previewUploadFile?.type === 'video'}
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video src={previewUploadFile.dataUrl} controls></video>
|
||||
{/if}
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||
<button class="absolute top-3 right-3 btn btn-circle">✕</button>
|
||||
<button class="!cursor-default">close</button>
|
||||
</form>
|
||||
</dialog>
|
Reference in New Issue
Block a user