mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
컴포넌트를 도메인과 무관한 UI 컴포넌트와 도메인 컴포넌트로 재구성하고 중복된 파일 목록 컴포넌트 통합 및 모듈 간 의존성 방향을 ESLint로 강제
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e82c5e556
commit
877cf9c42e
94 files changed
+398
-518
No files matched your search
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { ActionModal } from "#lib/ui/index.js";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
oncancel?: () => void;
|
||||
onLoginClick: () => void;
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(), oncancel, onLoginClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ActionModal
|
||||
bind:isOpen
|
||||
title="다른 디바이스에 이미 로그인되어 있어요."
|
||||
cancelText="아니요"
|
||||
{oncancel}
|
||||
confirmText="네"
|
||||
onConfirmClick={onLoginClick}
|
||||
>
|
||||
<p>다른 디바이스에서는 로그아웃하고, 이 디바이스에서 로그인할까요?</p>
|
||||
</ActionModal>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ForceLoginModal } from "./ForceLoginModal.svelte";
|
||||
@@ -0,0 +1,30 @@
|
||||
import { showToast } from "#lib/ui/index.js";
|
||||
import { isTRPCClientError } from "#trpc/client.js";
|
||||
|
||||
const getErrorMessage = (e: unknown, fallbackMessage: string) => {
|
||||
if (isTRPCClientError(e)) {
|
||||
switch (e.data?.code) {
|
||||
case "NOT_FOUND":
|
||||
return "대상을 찾을 수 없어요. 이미 삭제되었을 수 있어요.";
|
||||
case undefined:
|
||||
return "서버에 연결할 수 없어요.";
|
||||
}
|
||||
}
|
||||
return fallbackMessage;
|
||||
};
|
||||
|
||||
export const showErrorToast = (e: unknown, fallbackMessage: string) => {
|
||||
console.error(e);
|
||||
showToast(getErrorMessage(e, fallbackMessage));
|
||||
};
|
||||
|
||||
// Runs the action and shows the error to the user if it fails. Returns whether it succeeded.
|
||||
export const tryAction = async (action: () => Promise<unknown>, errorMessage: string) => {
|
||||
try {
|
||||
await action();
|
||||
return true;
|
||||
} catch (e) {
|
||||
showErrorToast(e, errorMessage);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<script module lang="ts">
|
||||
import type { DataKey, SubCategoryInfo } from "#lib/features/filesystem/index.js";
|
||||
|
||||
export interface SelectedCategory {
|
||||
id: number;
|
||||
dataKey?: DataKey;
|
||||
name: string;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import type { SvelteHTMLElements } from "svelte/elements";
|
||||
import { ActionEntryButton, CategoryLabel } from "#lib/ui/index.js";
|
||||
import { sortEntries } from "#lib/utils/index.js";
|
||||
|
||||
interface Props {
|
||||
categories: SubCategoryInfo[];
|
||||
categoryMenuIcon?: Component<SvelteHTMLElements["svg"]>;
|
||||
onCategoryClick: (category: SelectedCategory) => void;
|
||||
onCategoryMenuClick?: (category: SelectedCategory) => void;
|
||||
}
|
||||
|
||||
let { categories, categoryMenuIcon, onCategoryClick, onCategoryMenuClick }: Props = $props();
|
||||
|
||||
let categoriesWithName = $derived(sortEntries([...categories]));
|
||||
</script>
|
||||
|
||||
{#if categoriesWithName.length > 0}
|
||||
<div class="space-y-1">
|
||||
{#each categoriesWithName as category (category.id)}
|
||||
<ActionEntryButton
|
||||
class="h-12"
|
||||
onclick={() => onCategoryClick(category)}
|
||||
actionButtonIcon={categoryMenuIcon}
|
||||
onActionButtonClick={() => onCategoryMenuClick?.(category)}
|
||||
>
|
||||
<CategoryLabel name={category.name} />
|
||||
</ActionEntryButton>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { TextInputModal } from "#lib/ui/index.js";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onCreateClick: (name: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(), onCreateClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<TextInputModal
|
||||
bind:isOpen
|
||||
title="새 카테고리"
|
||||
placeholder="카테고리 이름"
|
||||
submitText="만들기"
|
||||
onSubmitClick={onCreateClick}
|
||||
/>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import type { SvelteHTMLElements } from "svelte/elements";
|
||||
import { ActionEntryButton, DirectoryEntryLabel } from "#lib/ui/index.js";
|
||||
|
||||
interface Props {
|
||||
action?: {
|
||||
icon: Component<SvelteHTMLElements["svg"]>;
|
||||
onclick: () => void;
|
||||
};
|
||||
info: { name: string; isFavorite?: boolean };
|
||||
onclick?: () => void;
|
||||
}
|
||||
|
||||
let { action, info, onclick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
{onclick}
|
||||
actionButtonIcon={action?.icon}
|
||||
onActionButtonClick={action?.onclick}
|
||||
>
|
||||
<DirectoryEntryLabel type="directory" name={info.name} isFavorite={info.isFavorite} />
|
||||
</ActionEntryButton>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
|
||||
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||
import { ActionEntryButton, DirectoryEntryLabel } from "#lib/ui/index.js";
|
||||
import { formatDateTime } from "#lib/utils/index.js";
|
||||
import type { SummarizedFileInfo } from "../types";
|
||||
|
||||
interface Props {
|
||||
action?: {
|
||||
icon: Component<SvelteHTMLElements["svg"]>;
|
||||
onclick: () => void;
|
||||
class?: ClassValue;
|
||||
};
|
||||
info: SummarizedFileInfo;
|
||||
onclick?: () => void;
|
||||
showThumbnail?: boolean;
|
||||
subtext?: string;
|
||||
}
|
||||
|
||||
let { action, info, onclick, showThumbnail = true, subtext }: Props = $props();
|
||||
|
||||
let thumbnail = $derived(showThumbnail ? getFileThumbnail(info) : undefined);
|
||||
</script>
|
||||
|
||||
<ActionEntryButton
|
||||
class="h-14"
|
||||
{onclick}
|
||||
actionButtonIcon={action?.icon}
|
||||
actionButtonClass={action?.class}
|
||||
onActionButtonClick={action?.onclick}
|
||||
>
|
||||
<DirectoryEntryLabel
|
||||
type="file"
|
||||
thumbnail={thumbnail?.url}
|
||||
name={info.name}
|
||||
subtext={subtext ?? formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||
isFavorite={info.isFavorite}
|
||||
/>
|
||||
</ActionEntryButton>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||
import type { SummarizedFileInfo } from "#lib/features/filesystem/index.js";
|
||||
|
||||
import IconFavorite from "~icons/material-symbols/favorite";
|
||||
|
||||
interface Props {
|
||||
info: SummarizedFileInfo;
|
||||
onclick?: (file: SummarizedFileInfo) => void;
|
||||
}
|
||||
|
||||
let { info, onclick }: Props = $props();
|
||||
|
||||
let thumbnail = $derived(getFileThumbnail(info));
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={onclick && (() => setTimeout(() => onclick(info), 100))}
|
||||
class="relative aspect-square overflow-hidden rounded-sm transition active:scale-95 active:brightness-90"
|
||||
>
|
||||
{#if thumbnail?.url}
|
||||
<img src={thumbnail?.url} alt={info.name} class="size-full object-cover" />
|
||||
{:else}
|
||||
<div class="size-full bg-gray-100"></div>
|
||||
{/if}
|
||||
{#if info.isFavorite}
|
||||
<div class={["absolute right-0.5 bottom-0.5"]}>
|
||||
<IconFavorite
|
||||
class="text-sm text-red-500"
|
||||
style="filter: drop-shadow(0 0 1px white) drop-shadow(0 0 1px white)"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
|
||||
import { Categories, type SelectedCategory } from "#lib/features/filesystem/components/index.js";
|
||||
import type { CategoryInfo } from "#lib/features/filesystem/index.js";
|
||||
import { IconEntryButton } from "#lib/ui/index.js";
|
||||
|
||||
import IconAddCircle from "~icons/material-symbols/add-circle";
|
||||
|
||||
interface Props {
|
||||
class?: ClassValue;
|
||||
info: CategoryInfo;
|
||||
onSubCategoryClick: (subCategory: SelectedCategory) => void;
|
||||
onSubCategoryCreateClick?: () => void;
|
||||
onSubCategoryMenuClick?: (category: SelectedCategory) => void;
|
||||
subCategoryCreatePosition?: "top" | "bottom" | "none";
|
||||
subCategoryMenuIcon?: Component<SvelteHTMLElements["svg"]>;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className,
|
||||
info,
|
||||
onSubCategoryClick,
|
||||
onSubCategoryCreateClick,
|
||||
onSubCategoryMenuClick,
|
||||
subCategoryCreatePosition = "none",
|
||||
subCategoryMenuIcon,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={["space-y-1", className]}>
|
||||
{#snippet subCategoryCreate()}
|
||||
<IconEntryButton
|
||||
icon={IconAddCircle}
|
||||
onclick={onSubCategoryCreateClick}
|
||||
class="h-12 w-full"
|
||||
iconClass="text-gray-600"
|
||||
textClass="text-gray-700"
|
||||
>
|
||||
카테고리 추가하기
|
||||
</IconEntryButton>
|
||||
{/snippet}
|
||||
|
||||
{#if subCategoryCreatePosition === "top"}
|
||||
{@render subCategoryCreate()}
|
||||
{/if}
|
||||
<Categories
|
||||
categories={info.subCategories}
|
||||
categoryMenuIcon={subCategoryMenuIcon}
|
||||
onCategoryClick={onSubCategoryClick}
|
||||
onCategoryMenuClick={onSubCategoryMenuClick}
|
||||
/>
|
||||
{#if subCategoryCreatePosition === "bottom"}
|
||||
{@render subCategoryCreate()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "./Categories.svelte";
|
||||
export { default as Categories } from "./Categories.svelte";
|
||||
export { default as CategoryCreateModal } from "./CategoryCreateModal.svelte";
|
||||
export { default as DirectoryEntry } from "./DirectoryEntry.svelte";
|
||||
export { default as FileEntry } from "./FileEntry.svelte";
|
||||
export { default as FileThumbnailButton } from "./FileThumbnailButton.svelte";
|
||||
export { default as SubCategories } from "./SubCategories.svelte";
|
||||
Reference in new issue
Block a user