[FA-misc] Astro migration works, probably want to touchup the frontend but that can be in Phase 4
This commit is contained in:
29
fictionarchive-web-astro/src/layouts/AppLayout.astro
Normal file
29
fictionarchive-web-astro/src/layouts/AppLayout.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import Navbar from '../lib/components/Navbar.svelte';
|
||||
import AuthInit from '../lib/components/AuthInit.svelte';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { title = 'FictionArchive' } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background">
|
||||
<AuthInit client:load />
|
||||
<Navbar client:load />
|
||||
<main class="mx-auto flex max-w-6xl flex-col gap-6 px-4 py-8 sm:px-6 lg:px-8">
|
||||
<slot />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
115
fictionarchive-web-astro/src/lib/auth/authStore.ts
Normal file
115
fictionarchive-web-astro/src/lib/auth/authStore.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import type { User } from 'oidc-client-ts';
|
||||
import { userManager, isOidcConfigured } from './oidcConfig';
|
||||
|
||||
// Stores
|
||||
export const user = writable<User | null>(null);
|
||||
export const isLoading = writable(true);
|
||||
export const isAuthenticated = derived(user, ($user) => $user !== null);
|
||||
export const isConfigured = isOidcConfigured;
|
||||
|
||||
// Cookie management
|
||||
function setCookieFromUser(u: User) {
|
||||
if (!u?.access_token) return;
|
||||
|
||||
const isProduction = window.location.hostname !== 'localhost';
|
||||
const domain = isProduction ? '.orfl.xyz' : undefined;
|
||||
const secure = isProduction;
|
||||
const sameSite = isProduction ? 'None' : 'Lax';
|
||||
|
||||
const cookieValue = `fa_session=${u.access_token}; path=/; ${secure ? 'secure; ' : ''}samesite=${sameSite}${domain ? `; domain=${domain}` : ''}`;
|
||||
document.cookie = cookieValue;
|
||||
}
|
||||
|
||||
function clearFaSessionCookie() {
|
||||
const isProduction = window.location.hostname !== 'localhost';
|
||||
const domain = isProduction ? '.orfl.xyz' : undefined;
|
||||
|
||||
const cookieValue = `fa_session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT${domain ? `; domain=${domain}` : ''}`;
|
||||
document.cookie = cookieValue;
|
||||
}
|
||||
|
||||
// Track if callback has been handled to prevent double processing
|
||||
let callbackHandled = false;
|
||||
|
||||
export async function initAuth() {
|
||||
if (!userManager) {
|
||||
isLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle callback if auth params are present
|
||||
const url = new URL(window.location.href);
|
||||
const hasAuthParams =
|
||||
url.searchParams.has('code') ||
|
||||
url.searchParams.has('id_token') ||
|
||||
url.searchParams.has('error');
|
||||
|
||||
if (hasAuthParams && !callbackHandled) {
|
||||
callbackHandled = true;
|
||||
try {
|
||||
const result = await userManager.signinRedirectCallback();
|
||||
user.set(result ?? null);
|
||||
if (result) {
|
||||
setCookieFromUser(result);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to complete sign-in redirect', e);
|
||||
} finally {
|
||||
const cleanUrl = `${url.origin}${url.pathname}`;
|
||||
window.history.replaceState({}, document.title, cleanUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing user
|
||||
try {
|
||||
const loadedUser = await userManager.getUser();
|
||||
user.set(loadedUser ?? null);
|
||||
if (loadedUser) {
|
||||
setCookieFromUser(loadedUser);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load user', e);
|
||||
}
|
||||
|
||||
isLoading.set(false);
|
||||
|
||||
// Event listeners
|
||||
userManager.events.addUserLoaded((u) => {
|
||||
user.set(u);
|
||||
setCookieFromUser(u);
|
||||
});
|
||||
|
||||
userManager.events.addUserUnloaded(() => {
|
||||
user.set(null);
|
||||
clearFaSessionCookie();
|
||||
});
|
||||
|
||||
userManager.events.addUserSignedOut(() => {
|
||||
user.set(null);
|
||||
clearFaSessionCookie();
|
||||
});
|
||||
}
|
||||
|
||||
export async function login() {
|
||||
if (!userManager) {
|
||||
console.warn('OIDC is not configured; set PUBLIC_OIDC_* environment variables.');
|
||||
return;
|
||||
}
|
||||
await userManager.signinRedirect();
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
if (!userManager) {
|
||||
console.warn('OIDC is not configured; set PUBLIC_OIDC_* environment variables.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
clearFaSessionCookie();
|
||||
await userManager.signoutRedirect();
|
||||
} catch (error) {
|
||||
console.error('Failed to sign out via redirect, clearing local session instead.', error);
|
||||
await userManager.removeUser();
|
||||
user.set(null);
|
||||
}
|
||||
}
|
||||
35
fictionarchive-web-astro/src/lib/auth/oidcConfig.ts
Normal file
35
fictionarchive-web-astro/src/lib/auth/oidcConfig.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { UserManager, WebStorageStateStore, type UserManagerSettings } from 'oidc-client-ts';
|
||||
|
||||
const authority = import.meta.env.PUBLIC_OIDC_AUTHORITY;
|
||||
const clientId = import.meta.env.PUBLIC_OIDC_CLIENT_ID;
|
||||
const redirectUri = import.meta.env.PUBLIC_OIDC_REDIRECT_URI;
|
||||
const postLogoutRedirectUri = import.meta.env.PUBLIC_OIDC_POST_LOGOUT_REDIRECT_URI ?? redirectUri;
|
||||
const scope = import.meta.env.PUBLIC_OIDC_SCOPE ?? 'openid profile email';
|
||||
|
||||
export const isOidcConfigured =
|
||||
Boolean(authority) && Boolean(clientId) && Boolean(redirectUri);
|
||||
|
||||
function buildSettings(): UserManagerSettings | null {
|
||||
if (!isOidcConfigured) return null;
|
||||
|
||||
return {
|
||||
authority: authority!,
|
||||
client_id: clientId!,
|
||||
redirect_uri: redirectUri!,
|
||||
post_logout_redirect_uri: postLogoutRedirectUri,
|
||||
response_type: 'code',
|
||||
scope,
|
||||
loadUserInfo: true,
|
||||
automaticSilentRenew: true,
|
||||
userStore:
|
||||
typeof window !== 'undefined'
|
||||
? new WebStorageStateStore({ store: window.localStorage })
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const userManager = (() => {
|
||||
const settings = buildSettings();
|
||||
if (!settings) return null;
|
||||
return new UserManager(settings);
|
||||
})();
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { initAuth } from '$lib/auth/authStore';
|
||||
|
||||
onMount(() => {
|
||||
initAuth();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { user, isLoading, isConfigured, login, logout } from '$lib/auth/authStore';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
||||
let isOpen = $state(false);
|
||||
|
||||
const email = $derived(
|
||||
$user?.profile?.email ??
|
||||
$user?.profile?.preferred_username ??
|
||||
$user?.profile?.name ??
|
||||
$user?.profile?.sub ??
|
||||
'User'
|
||||
);
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (!target.closest('.auth-dropdown')) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
isOpen = false;
|
||||
await logout();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleClickOutside} />
|
||||
|
||||
{#if $isLoading}
|
||||
<Button variant="outline" disabled>Loading...</Button>
|
||||
{:else if !isConfigured}
|
||||
<span class="text-sm text-yellow-600">Auth not configured</span>
|
||||
{:else if $user}
|
||||
<div class="auth-dropdown relative">
|
||||
<Button variant="outline" onclick={toggleDropdown}>
|
||||
{email}
|
||||
</Button>
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="absolute right-0 z-50 mt-2 w-48 rounded-md bg-white p-2 shadow-lg dark:bg-gray-800"
|
||||
>
|
||||
<Button variant="ghost" class="w-full justify-start" onclick={handleLogout}>
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Button onclick={login}>Sign in</Button>
|
||||
{/if}
|
||||
21
fictionarchive-web-astro/src/lib/components/Navbar.svelte
Normal file
21
fictionarchive-web-astro/src/lib/components/Navbar.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import AuthenticationDisplay from './AuthenticationDisplay.svelte';
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-10 border-b bg-white/80 backdrop-blur dark:bg-gray-900/80">
|
||||
<nav class="mx-auto flex max-w-6xl items-center gap-4 px-4 py-3">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<span class="rounded bg-primary px-2 py-1 font-bold text-white">FA</span>
|
||||
<span class="font-semibold">FictionArchive</span>
|
||||
</a>
|
||||
<a href="/">
|
||||
<Button variant="ghost">Novels</Button>
|
||||
</a>
|
||||
<div class="flex-1">
|
||||
<Input type="search" placeholder="Search..." class="max-w-xs" />
|
||||
</div>
|
||||
<AuthenticationDisplay />
|
||||
</nav>
|
||||
</header>
|
||||
45
fictionarchive-web-astro/src/lib/components/NovelCard.svelte
Normal file
45
fictionarchive-web-astro/src/lib/components/NovelCard.svelte
Normal file
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" module>
|
||||
import type { NovelsQuery } from '$lib/graphql/__generated__/graphql';
|
||||
|
||||
export type NovelNode = NonNullable<NonNullable<NovelsQuery['novels']>['edges']>[number]['node'];
|
||||
|
||||
export interface NovelCardProps {
|
||||
novel: NovelNode;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
||||
let { novel }: NovelCardProps = $props();
|
||||
|
||||
function pickText(novelText?: NovelNode['name'] | NovelNode['description']) {
|
||||
const texts = novelText?.texts ?? [];
|
||||
const english = texts.find((t) => t.language === 'EN');
|
||||
return (english ?? texts[0])?.text ?? 'No description available.';
|
||||
}
|
||||
|
||||
const title = $derived(pickText(novel.name));
|
||||
const description = $derived(pickText(novel.description));
|
||||
const coverSrc = $derived(novel.coverImage?.newPath ?? novel.coverImage?.originalPath);
|
||||
</script>
|
||||
|
||||
<Card class="overflow-hidden border shadow-sm transition-shadow hover:shadow-md">
|
||||
{#if coverSrc}
|
||||
<div class="aspect-[3/4] w-full overflow-hidden bg-muted/50">
|
||||
<img src={coverSrc} alt={title} class="h-full w-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="aspect-[3/4] w-full bg-muted/50"></div>
|
||||
{/if}
|
||||
<CardHeader class="space-y-2">
|
||||
<CardTitle class="line-clamp-2 text-lg leading-tight">
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="pt-0">
|
||||
<p class="line-clamp-3 text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
||||
interface Props {
|
||||
novelId?: string;
|
||||
}
|
||||
|
||||
let { novelId }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Card class="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Novel Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{#if novelId}
|
||||
Viewing novel ID: <code class="rounded bg-muted px-1 py-0.5">{novelId}</code>
|
||||
{/if}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-muted-foreground">
|
||||
Detail view coming soon. Select a novel to explore chapters and metadata once implemented.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
117
fictionarchive-web-astro/src/lib/components/NovelsPage.svelte
Normal file
117
fictionarchive-web-astro/src/lib/components/NovelsPage.svelte
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { client } from '$lib/graphql/client';
|
||||
import { NovelsDocument, type NovelsQuery } from '$lib/graphql/__generated__/graphql';
|
||||
import NovelCard from './NovelCard.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
|
||||
type NovelEdge = NonNullable<NovelsQuery['novels']>['edges'][number];
|
||||
|
||||
let edges: NovelEdge[] = $state([]);
|
||||
let pageInfo: NonNullable<NovelsQuery['novels']>['pageInfo'] | null = $state(null);
|
||||
let fetching = $state(false);
|
||||
let error: string | null = $state(null);
|
||||
let initialLoad = $state(true);
|
||||
|
||||
const hasNextPage = $derived(pageInfo?.hasNextPage ?? false);
|
||||
const novels = $derived(edges.map((edge) => edge.node).filter(Boolean));
|
||||
|
||||
async function fetchNovels(after: string | null = null) {
|
||||
fetching = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const result = await client.query(NovelsDocument, { first: PAGE_SIZE, after }).toPromise();
|
||||
|
||||
if (result.error) {
|
||||
error = result.error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.data?.novels) {
|
||||
if (after) {
|
||||
// Append for pagination
|
||||
edges = [...edges, ...result.data.novels.edges];
|
||||
} else {
|
||||
// Initial load
|
||||
edges = result.data.novels.edges;
|
||||
}
|
||||
pageInfo = result.data.novels.pageInfo;
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Unknown error';
|
||||
} finally {
|
||||
fetching = false;
|
||||
initialLoad = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (pageInfo?.endCursor) {
|
||||
fetchNovels(pageInfo.endCursor);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchNovels();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<Card class="shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle>Latest Novels</CardTitle>
|
||||
<p class="text-sm text-muted-foreground">Novels that have recently been updated.</p>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{#if fetching && initialLoad}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div
|
||||
class="h-10 w-10 animate-spin rounded-full border-2 border-primary border-t-transparent"
|
||||
aria-label="Loading novels"
|
||||
></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<Card class="border-destructive/40 bg-destructive/5">
|
||||
<CardContent>
|
||||
<p class="py-4 text-sm text-destructive">Could not load novels: {error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if !fetching && novels.length === 0 && !error && !initialLoad}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p class="py-4 text-sm text-muted-foreground">
|
||||
No novels found yet. Try adding content to the gateway.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
{#if novels.length > 0}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each novels as novel (novel.id)}
|
||||
<NovelCard {novel} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasNextPage}
|
||||
<div class="flex justify-center">
|
||||
<Button onclick={loadMore} variant="outline" disabled={fetching} class="min-w-[160px]">
|
||||
{fetching ? 'Loading...' : 'Load more'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||
outline:
|
||||
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
href={disabled ? undefined : href}
|
||||
aria-disabled={disabled}
|
||||
role={disabled ? "link" : undefined}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
data-slot="button"
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{disabled}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
fictionarchive-web-astro/src/lib/components/ui/card/index.ts
Normal file
25
fictionarchive-web-astro/src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "input",
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
785
fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
785
fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
@@ -0,0 +1,785 @@
|
||||
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
|
||||
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: { input: string; output: string; }
|
||||
String: { input: string; output: string; }
|
||||
Boolean: { input: boolean; output: boolean; }
|
||||
Int: { input: number; output: number; }
|
||||
Float: { input: number; output: number; }
|
||||
Instant: { input: any; output: any; }
|
||||
UUID: { input: any; output: any; }
|
||||
UnsignedInt: { input: any; output: any; }
|
||||
};
|
||||
|
||||
/** Defines when a policy shall be executed. */
|
||||
export const ApplyPolicy = {
|
||||
/** After the resolver was executed. */
|
||||
AfterResolver: 'AFTER_RESOLVER',
|
||||
/** Before the resolver was executed. */
|
||||
BeforeResolver: 'BEFORE_RESOLVER',
|
||||
/** The policy is applied in the validation step before the execution. */
|
||||
Validation: 'VALIDATION'
|
||||
} as const;
|
||||
|
||||
export type ApplyPolicy = typeof ApplyPolicy[keyof typeof ApplyPolicy];
|
||||
export type Chapter = {
|
||||
body: LocalizationKey;
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
images: Array<Image>;
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
name: LocalizationKey;
|
||||
order: Scalars['UnsignedInt']['output'];
|
||||
revision: Scalars['UnsignedInt']['output'];
|
||||
url: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type ChapterFilterInput = {
|
||||
and?: InputMaybe<Array<ChapterFilterInput>>;
|
||||
body?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
images?: InputMaybe<ListFilterInputTypeOfImageFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
name?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
or?: InputMaybe<Array<ChapterFilterInput>>;
|
||||
order?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
revision?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
url?: InputMaybe<StringOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type ChapterPullRequestedEvent = {
|
||||
chapterNumber: Scalars['UnsignedInt']['output'];
|
||||
novelId: Scalars['UnsignedInt']['output'];
|
||||
};
|
||||
|
||||
export type ChapterSortInput = {
|
||||
body?: InputMaybe<LocalizationKeySortInput>;
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
name?: InputMaybe<LocalizationKeySortInput>;
|
||||
order?: InputMaybe<SortEnumType>;
|
||||
revision?: InputMaybe<SortEnumType>;
|
||||
url?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export type DeleteJobError = KeyNotFoundError;
|
||||
|
||||
export type DeleteJobInput = {
|
||||
jobKey: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type DeleteJobPayload = {
|
||||
boolean: Maybe<Scalars['Boolean']['output']>;
|
||||
errors: Maybe<Array<DeleteJobError>>;
|
||||
};
|
||||
|
||||
export type DuplicateNameError = Error & {
|
||||
message: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Error = {
|
||||
message: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FetchChapterContentsInput = {
|
||||
chapterNumber: Scalars['UnsignedInt']['input'];
|
||||
novelId: Scalars['UnsignedInt']['input'];
|
||||
};
|
||||
|
||||
export type FetchChapterContentsPayload = {
|
||||
chapterPullRequestedEvent: Maybe<ChapterPullRequestedEvent>;
|
||||
};
|
||||
|
||||
export type FormatError = Error & {
|
||||
message: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
chapter: Maybe<Chapter>;
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
newPath: Maybe<Scalars['String']['output']>;
|
||||
originalPath: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ImageFilterInput = {
|
||||
and?: InputMaybe<Array<ImageFilterInput>>;
|
||||
chapter?: InputMaybe<ChapterFilterInput>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UuidOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
newPath?: InputMaybe<StringOperationFilterInput>;
|
||||
or?: InputMaybe<Array<ImageFilterInput>>;
|
||||
originalPath?: InputMaybe<StringOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type ImageSortInput = {
|
||||
chapter?: InputMaybe<ChapterSortInput>;
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
newPath?: InputMaybe<SortEnumType>;
|
||||
originalPath?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export type ImportNovelInput = {
|
||||
novelUrl: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ImportNovelPayload = {
|
||||
novelUpdateRequestedEvent: Maybe<NovelUpdateRequestedEvent>;
|
||||
};
|
||||
|
||||
export type InstantFilterInput = {
|
||||
and?: InputMaybe<Array<InstantFilterInput>>;
|
||||
or?: InputMaybe<Array<InstantFilterInput>>;
|
||||
};
|
||||
|
||||
export type JobKey = {
|
||||
group: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type JobPersistenceError = Error & {
|
||||
message: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type KeyNotFoundError = Error & {
|
||||
message: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type KeyValuePairOfStringAndString = {
|
||||
key: Scalars['String']['output'];
|
||||
value: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export const Language = {
|
||||
Ch: 'CH',
|
||||
En: 'EN',
|
||||
Ja: 'JA',
|
||||
Kr: 'KR'
|
||||
} as const;
|
||||
|
||||
export type Language = typeof Language[keyof typeof Language];
|
||||
export type LanguageOperationFilterInput = {
|
||||
eq?: InputMaybe<Language>;
|
||||
in?: InputMaybe<Array<Language>>;
|
||||
neq?: InputMaybe<Language>;
|
||||
nin?: InputMaybe<Array<Language>>;
|
||||
};
|
||||
|
||||
export type ListFilterInputTypeOfChapterFilterInput = {
|
||||
all?: InputMaybe<ChapterFilterInput>;
|
||||
any?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
none?: InputMaybe<ChapterFilterInput>;
|
||||
some?: InputMaybe<ChapterFilterInput>;
|
||||
};
|
||||
|
||||
export type ListFilterInputTypeOfImageFilterInput = {
|
||||
all?: InputMaybe<ImageFilterInput>;
|
||||
any?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
none?: InputMaybe<ImageFilterInput>;
|
||||
some?: InputMaybe<ImageFilterInput>;
|
||||
};
|
||||
|
||||
export type ListFilterInputTypeOfLocalizationTextFilterInput = {
|
||||
all?: InputMaybe<LocalizationTextFilterInput>;
|
||||
any?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
none?: InputMaybe<LocalizationTextFilterInput>;
|
||||
some?: InputMaybe<LocalizationTextFilterInput>;
|
||||
};
|
||||
|
||||
export type ListFilterInputTypeOfNovelFilterInput = {
|
||||
all?: InputMaybe<NovelFilterInput>;
|
||||
any?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
none?: InputMaybe<NovelFilterInput>;
|
||||
some?: InputMaybe<NovelFilterInput>;
|
||||
};
|
||||
|
||||
export type ListFilterInputTypeOfNovelTagFilterInput = {
|
||||
all?: InputMaybe<NovelTagFilterInput>;
|
||||
any?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
none?: InputMaybe<NovelTagFilterInput>;
|
||||
some?: InputMaybe<NovelTagFilterInput>;
|
||||
};
|
||||
|
||||
export type LocalizationKey = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
texts: Array<LocalizationText>;
|
||||
};
|
||||
|
||||
export type LocalizationKeyFilterInput = {
|
||||
and?: InputMaybe<Array<LocalizationKeyFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UuidOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
or?: InputMaybe<Array<LocalizationKeyFilterInput>>;
|
||||
texts?: InputMaybe<ListFilterInputTypeOfLocalizationTextFilterInput>;
|
||||
};
|
||||
|
||||
export type LocalizationKeySortInput = {
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export type LocalizationText = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
language: Language;
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
text: Scalars['String']['output'];
|
||||
translationEngine: Maybe<TranslationEngine>;
|
||||
};
|
||||
|
||||
export type LocalizationTextFilterInput = {
|
||||
and?: InputMaybe<Array<LocalizationTextFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UuidOperationFilterInput>;
|
||||
language?: InputMaybe<LanguageOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
or?: InputMaybe<Array<LocalizationTextFilterInput>>;
|
||||
text?: InputMaybe<StringOperationFilterInput>;
|
||||
translationEngine?: InputMaybe<TranslationEngineFilterInput>;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
deleteJob: DeleteJobPayload;
|
||||
fetchChapterContents: FetchChapterContentsPayload;
|
||||
importNovel: ImportNovelPayload;
|
||||
registerUser: RegisterUserPayload;
|
||||
runJob: RunJobPayload;
|
||||
scheduleEventJob: ScheduleEventJobPayload;
|
||||
translateText: TranslateTextPayload;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteJobArgs = {
|
||||
input: DeleteJobInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationFetchChapterContentsArgs = {
|
||||
input: FetchChapterContentsInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationImportNovelArgs = {
|
||||
input: ImportNovelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationRegisterUserArgs = {
|
||||
input: RegisterUserInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationRunJobArgs = {
|
||||
input: RunJobInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationScheduleEventJobArgs = {
|
||||
input: ScheduleEventJobInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationTranslateTextArgs = {
|
||||
input: TranslateTextInput;
|
||||
};
|
||||
|
||||
export type Novel = {
|
||||
author: Person;
|
||||
chapters: Array<Chapter>;
|
||||
coverImage: Maybe<Image>;
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
description: LocalizationKey;
|
||||
externalId: Scalars['String']['output'];
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
name: LocalizationKey;
|
||||
rawLanguage: Language;
|
||||
rawStatus: NovelStatus;
|
||||
source: Source;
|
||||
statusOverride: Maybe<NovelStatus>;
|
||||
tags: Array<NovelTag>;
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type NovelFilterInput = {
|
||||
and?: InputMaybe<Array<NovelFilterInput>>;
|
||||
author?: InputMaybe<PersonFilterInput>;
|
||||
chapters?: InputMaybe<ListFilterInputTypeOfChapterFilterInput>;
|
||||
coverImage?: InputMaybe<ImageFilterInput>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
description?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
externalId?: InputMaybe<StringOperationFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
name?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
or?: InputMaybe<Array<NovelFilterInput>>;
|
||||
rawLanguage?: InputMaybe<LanguageOperationFilterInput>;
|
||||
rawStatus?: InputMaybe<NovelStatusOperationFilterInput>;
|
||||
source?: InputMaybe<SourceFilterInput>;
|
||||
statusOverride?: InputMaybe<NullableOfNovelStatusOperationFilterInput>;
|
||||
tags?: InputMaybe<ListFilterInputTypeOfNovelTagFilterInput>;
|
||||
url?: InputMaybe<StringOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type NovelSortInput = {
|
||||
author?: InputMaybe<PersonSortInput>;
|
||||
coverImage?: InputMaybe<ImageSortInput>;
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
description?: InputMaybe<LocalizationKeySortInput>;
|
||||
externalId?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
name?: InputMaybe<LocalizationKeySortInput>;
|
||||
rawLanguage?: InputMaybe<SortEnumType>;
|
||||
rawStatus?: InputMaybe<SortEnumType>;
|
||||
source?: InputMaybe<SourceSortInput>;
|
||||
statusOverride?: InputMaybe<SortEnumType>;
|
||||
url?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export const NovelStatus = {
|
||||
Abandoned: 'ABANDONED',
|
||||
Completed: 'COMPLETED',
|
||||
Hiatus: 'HIATUS',
|
||||
InProgress: 'IN_PROGRESS',
|
||||
Unknown: 'UNKNOWN'
|
||||
} as const;
|
||||
|
||||
export type NovelStatus = typeof NovelStatus[keyof typeof NovelStatus];
|
||||
export type NovelStatusOperationFilterInput = {
|
||||
eq?: InputMaybe<NovelStatus>;
|
||||
in?: InputMaybe<Array<NovelStatus>>;
|
||||
neq?: InputMaybe<NovelStatus>;
|
||||
nin?: InputMaybe<Array<NovelStatus>>;
|
||||
};
|
||||
|
||||
export type NovelTag = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
displayName: LocalizationKey;
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
key: Scalars['String']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
novels: Array<Novel>;
|
||||
source: Maybe<Source>;
|
||||
tagType: TagType;
|
||||
};
|
||||
|
||||
export type NovelTagFilterInput = {
|
||||
and?: InputMaybe<Array<NovelTagFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
displayName?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
key?: InputMaybe<StringOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
novels?: InputMaybe<ListFilterInputTypeOfNovelFilterInput>;
|
||||
or?: InputMaybe<Array<NovelTagFilterInput>>;
|
||||
source?: InputMaybe<SourceFilterInput>;
|
||||
tagType?: InputMaybe<TagTypeOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type NovelUpdateRequestedEvent = {
|
||||
novelUrl: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
/** A connection to a list of items. */
|
||||
export type NovelsConnection = {
|
||||
/** A list of edges. */
|
||||
edges: Maybe<Array<NovelsEdge>>;
|
||||
/** A flattened list of the nodes. */
|
||||
nodes: Maybe<Array<Novel>>;
|
||||
/** Information to aid in pagination. */
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
/** An edge in a connection. */
|
||||
export type NovelsEdge = {
|
||||
/** A cursor for use in pagination. */
|
||||
cursor: Scalars['String']['output'];
|
||||
/** The item at the end of the edge. */
|
||||
node: Novel;
|
||||
};
|
||||
|
||||
export type NullableOfNovelStatusOperationFilterInput = {
|
||||
eq?: InputMaybe<NovelStatus>;
|
||||
in?: InputMaybe<Array<InputMaybe<NovelStatus>>>;
|
||||
neq?: InputMaybe<NovelStatus>;
|
||||
nin?: InputMaybe<Array<InputMaybe<NovelStatus>>>;
|
||||
};
|
||||
|
||||
/** Information about pagination in a connection. */
|
||||
export type PageInfo = {
|
||||
/** When paginating forwards, the cursor to continue. */
|
||||
endCursor: Maybe<Scalars['String']['output']>;
|
||||
/** Indicates whether more edges exist following the set defined by the clients arguments. */
|
||||
hasNextPage: Scalars['Boolean']['output'];
|
||||
/** Indicates whether more edges exist prior the set defined by the clients arguments. */
|
||||
hasPreviousPage: Scalars['Boolean']['output'];
|
||||
/** When paginating backwards, the cursor to continue. */
|
||||
startCursor: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type Person = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
externalUrl: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
name: LocalizationKey;
|
||||
};
|
||||
|
||||
export type PersonFilterInput = {
|
||||
and?: InputMaybe<Array<PersonFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
externalUrl?: InputMaybe<StringOperationFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
name?: InputMaybe<LocalizationKeyFilterInput>;
|
||||
or?: InputMaybe<Array<PersonFilterInput>>;
|
||||
};
|
||||
|
||||
export type PersonSortInput = {
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
externalUrl?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
name?: InputMaybe<LocalizationKeySortInput>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
jobs: Array<SchedulerJob>;
|
||||
novels: Maybe<NovelsConnection>;
|
||||
translationEngines: Array<TranslationEngineDescriptor>;
|
||||
translationRequests: Maybe<TranslationRequestsConnection>;
|
||||
users: Array<User>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryNovelsArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
order?: InputMaybe<Array<NovelSortInput>>;
|
||||
where?: InputMaybe<NovelFilterInput>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryTranslationEnginesArgs = {
|
||||
order?: InputMaybe<Array<TranslationEngineDescriptorSortInput>>;
|
||||
where?: InputMaybe<TranslationEngineDescriptorFilterInput>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryTranslationRequestsArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
order?: InputMaybe<Array<TranslationRequestSortInput>>;
|
||||
where?: InputMaybe<TranslationRequestFilterInput>;
|
||||
};
|
||||
|
||||
export type RegisterUserInput = {
|
||||
email: Scalars['String']['input'];
|
||||
inviterOAuthProviderId?: InputMaybe<Scalars['String']['input']>;
|
||||
oAuthProviderId: Scalars['String']['input'];
|
||||
username: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type RegisterUserPayload = {
|
||||
user: Maybe<User>;
|
||||
};
|
||||
|
||||
export type RunJobError = JobPersistenceError;
|
||||
|
||||
export type RunJobInput = {
|
||||
jobKey: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type RunJobPayload = {
|
||||
boolean: Maybe<Scalars['Boolean']['output']>;
|
||||
errors: Maybe<Array<RunJobError>>;
|
||||
};
|
||||
|
||||
export type ScheduleEventJobError = DuplicateNameError | FormatError;
|
||||
|
||||
export type ScheduleEventJobInput = {
|
||||
cronSchedule: Scalars['String']['input'];
|
||||
description: Scalars['String']['input'];
|
||||
eventData: Scalars['String']['input'];
|
||||
eventType: Scalars['String']['input'];
|
||||
key: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ScheduleEventJobPayload = {
|
||||
errors: Maybe<Array<ScheduleEventJobError>>;
|
||||
schedulerJob: Maybe<SchedulerJob>;
|
||||
};
|
||||
|
||||
export type SchedulerJob = {
|
||||
cronSchedule: Array<Scalars['String']['output']>;
|
||||
description: Scalars['String']['output'];
|
||||
jobData: Array<KeyValuePairOfStringAndString>;
|
||||
jobKey: JobKey;
|
||||
jobTypeName: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export const SortEnumType = {
|
||||
Asc: 'ASC',
|
||||
Desc: 'DESC'
|
||||
} as const;
|
||||
|
||||
export type SortEnumType = typeof SortEnumType[keyof typeof SortEnumType];
|
||||
export type Source = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
key: Scalars['String']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type SourceFilterInput = {
|
||||
and?: InputMaybe<Array<SourceFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
key?: InputMaybe<StringOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
name?: InputMaybe<StringOperationFilterInput>;
|
||||
or?: InputMaybe<Array<SourceFilterInput>>;
|
||||
url?: InputMaybe<StringOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type SourceSortInput = {
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
key?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
name?: InputMaybe<SortEnumType>;
|
||||
url?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export type StringOperationFilterInput = {
|
||||
and?: InputMaybe<Array<StringOperationFilterInput>>;
|
||||
contains?: InputMaybe<Scalars['String']['input']>;
|
||||
endsWith?: InputMaybe<Scalars['String']['input']>;
|
||||
eq?: InputMaybe<Scalars['String']['input']>;
|
||||
in?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
|
||||
ncontains?: InputMaybe<Scalars['String']['input']>;
|
||||
nendsWith?: InputMaybe<Scalars['String']['input']>;
|
||||
neq?: InputMaybe<Scalars['String']['input']>;
|
||||
nin?: InputMaybe<Array<InputMaybe<Scalars['String']['input']>>>;
|
||||
nstartsWith?: InputMaybe<Scalars['String']['input']>;
|
||||
or?: InputMaybe<Array<StringOperationFilterInput>>;
|
||||
startsWith?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export const TagType = {
|
||||
External: 'EXTERNAL',
|
||||
Genre: 'GENRE',
|
||||
System: 'SYSTEM',
|
||||
UserDefined: 'USER_DEFINED'
|
||||
} as const;
|
||||
|
||||
export type TagType = typeof TagType[keyof typeof TagType];
|
||||
export type TagTypeOperationFilterInput = {
|
||||
eq?: InputMaybe<TagType>;
|
||||
in?: InputMaybe<Array<TagType>>;
|
||||
neq?: InputMaybe<TagType>;
|
||||
nin?: InputMaybe<Array<TagType>>;
|
||||
};
|
||||
|
||||
export type TranslateTextInput = {
|
||||
from: Language;
|
||||
text: Scalars['String']['input'];
|
||||
to: Language;
|
||||
translationEngineKey: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type TranslateTextPayload = {
|
||||
translationResult: Maybe<TranslationResult>;
|
||||
};
|
||||
|
||||
export type TranslationEngine = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
id: Scalars['UnsignedInt']['output'];
|
||||
key: Scalars['String']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
};
|
||||
|
||||
export type TranslationEngineDescriptor = {
|
||||
displayName: Scalars['String']['output'];
|
||||
key: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type TranslationEngineDescriptorFilterInput = {
|
||||
and?: InputMaybe<Array<TranslationEngineDescriptorFilterInput>>;
|
||||
displayName?: InputMaybe<StringOperationFilterInput>;
|
||||
key?: InputMaybe<StringOperationFilterInput>;
|
||||
or?: InputMaybe<Array<TranslationEngineDescriptorFilterInput>>;
|
||||
};
|
||||
|
||||
export type TranslationEngineDescriptorSortInput = {
|
||||
displayName?: InputMaybe<SortEnumType>;
|
||||
key?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export type TranslationEngineFilterInput = {
|
||||
and?: InputMaybe<Array<TranslationEngineFilterInput>>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
id?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
key?: InputMaybe<StringOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
or?: InputMaybe<Array<TranslationEngineFilterInput>>;
|
||||
};
|
||||
|
||||
export type TranslationRequest = {
|
||||
billedCharacterCount: Scalars['UnsignedInt']['output'];
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
from: Language;
|
||||
id: Scalars['UUID']['output'];
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
originalText: Scalars['String']['output'];
|
||||
status: TranslationRequestStatus;
|
||||
to: Language;
|
||||
translatedText: Maybe<Scalars['String']['output']>;
|
||||
translationEngineKey: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type TranslationRequestFilterInput = {
|
||||
and?: InputMaybe<Array<TranslationRequestFilterInput>>;
|
||||
billedCharacterCount?: InputMaybe<UnsignedIntOperationFilterInputType>;
|
||||
createdTime?: InputMaybe<InstantFilterInput>;
|
||||
from?: InputMaybe<LanguageOperationFilterInput>;
|
||||
id?: InputMaybe<UuidOperationFilterInput>;
|
||||
lastUpdatedTime?: InputMaybe<InstantFilterInput>;
|
||||
or?: InputMaybe<Array<TranslationRequestFilterInput>>;
|
||||
originalText?: InputMaybe<StringOperationFilterInput>;
|
||||
status?: InputMaybe<TranslationRequestStatusOperationFilterInput>;
|
||||
to?: InputMaybe<LanguageOperationFilterInput>;
|
||||
translatedText?: InputMaybe<StringOperationFilterInput>;
|
||||
translationEngineKey?: InputMaybe<StringOperationFilterInput>;
|
||||
};
|
||||
|
||||
export type TranslationRequestSortInput = {
|
||||
billedCharacterCount?: InputMaybe<SortEnumType>;
|
||||
createdTime?: InputMaybe<SortEnumType>;
|
||||
from?: InputMaybe<SortEnumType>;
|
||||
id?: InputMaybe<SortEnumType>;
|
||||
lastUpdatedTime?: InputMaybe<SortEnumType>;
|
||||
originalText?: InputMaybe<SortEnumType>;
|
||||
status?: InputMaybe<SortEnumType>;
|
||||
to?: InputMaybe<SortEnumType>;
|
||||
translatedText?: InputMaybe<SortEnumType>;
|
||||
translationEngineKey?: InputMaybe<SortEnumType>;
|
||||
};
|
||||
|
||||
export const TranslationRequestStatus = {
|
||||
Failed: 'FAILED',
|
||||
Pending: 'PENDING',
|
||||
Success: 'SUCCESS'
|
||||
} as const;
|
||||
|
||||
export type TranslationRequestStatus = typeof TranslationRequestStatus[keyof typeof TranslationRequestStatus];
|
||||
export type TranslationRequestStatusOperationFilterInput = {
|
||||
eq?: InputMaybe<TranslationRequestStatus>;
|
||||
in?: InputMaybe<Array<TranslationRequestStatus>>;
|
||||
neq?: InputMaybe<TranslationRequestStatus>;
|
||||
nin?: InputMaybe<Array<TranslationRequestStatus>>;
|
||||
};
|
||||
|
||||
/** A connection to a list of items. */
|
||||
export type TranslationRequestsConnection = {
|
||||
/** A list of edges. */
|
||||
edges: Maybe<Array<TranslationRequestsEdge>>;
|
||||
/** A flattened list of the nodes. */
|
||||
nodes: Maybe<Array<TranslationRequest>>;
|
||||
/** Information to aid in pagination. */
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
/** An edge in a connection. */
|
||||
export type TranslationRequestsEdge = {
|
||||
/** A cursor for use in pagination. */
|
||||
cursor: Scalars['String']['output'];
|
||||
/** The item at the end of the edge. */
|
||||
node: TranslationRequest;
|
||||
};
|
||||
|
||||
export type TranslationResult = {
|
||||
billedCharacterCount: Scalars['UnsignedInt']['output'];
|
||||
from: Language;
|
||||
originalText: Scalars['String']['output'];
|
||||
status: TranslationRequestStatus;
|
||||
to: Language;
|
||||
translatedText: Maybe<Scalars['String']['output']>;
|
||||
translationEngineKey: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type UnsignedIntOperationFilterInputType = {
|
||||
eq?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
gt?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
gte?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
in?: InputMaybe<Array<InputMaybe<Scalars['UnsignedInt']['input']>>>;
|
||||
lt?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
lte?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
neq?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
ngt?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
ngte?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
nin?: InputMaybe<Array<InputMaybe<Scalars['UnsignedInt']['input']>>>;
|
||||
nlt?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
nlte?: InputMaybe<Scalars['UnsignedInt']['input']>;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
createdTime: Scalars['Instant']['output'];
|
||||
disabled: Scalars['Boolean']['output'];
|
||||
email: Scalars['String']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
inviter: Maybe<User>;
|
||||
lastUpdatedTime: Scalars['Instant']['output'];
|
||||
oAuthProviderId: Scalars['String']['output'];
|
||||
username: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type UuidOperationFilterInput = {
|
||||
eq?: InputMaybe<Scalars['UUID']['input']>;
|
||||
gt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
gte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
in?: InputMaybe<Array<InputMaybe<Scalars['UUID']['input']>>>;
|
||||
lt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
lte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
neq?: InputMaybe<Scalars['UUID']['input']>;
|
||||
ngt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
ngte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
nin?: InputMaybe<Array<InputMaybe<Scalars['UUID']['input']>>>;
|
||||
nlt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
nlte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
};
|
||||
|
||||
export type NovelsQueryVariables = Exact<{
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: { texts: Array<{ language: Language, text: string }> }, description: { texts: Array<{ language: Language, text: string }> }, coverImage: { originalPath: string, newPath: string | null } | null } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null };
|
||||
|
||||
|
||||
export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"texts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"description"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"texts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"language"}},{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"originalPath"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<NovelsQuery, NovelsQueryVariables>;
|
||||
21
fictionarchive-web-astro/src/lib/graphql/client.ts
Normal file
21
fictionarchive-web-astro/src/lib/graphql/client.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Client, cacheExchange, fetchExchange } from '@urql/core';
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '../auth/authStore';
|
||||
|
||||
export function createClient() {
|
||||
return new Client({
|
||||
url: import.meta.env.PUBLIC_GRAPHQL_URI,
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
fetchOptions: () => {
|
||||
const currentUser = get(user);
|
||||
return {
|
||||
headers: currentUser?.access_token
|
||||
? { Authorization: `Bearer ${currentUser.access_token}` }
|
||||
: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Singleton for use in components
|
||||
export const client = createClient();
|
||||
@@ -0,0 +1,31 @@
|
||||
query Novels($first: Int, $after: String) {
|
||||
novels(first: $first, after: $after) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
url
|
||||
name {
|
||||
texts {
|
||||
language
|
||||
text
|
||||
}
|
||||
}
|
||||
description {
|
||||
texts {
|
||||
language
|
||||
text
|
||||
}
|
||||
}
|
||||
coverImage {
|
||||
originalPath
|
||||
newPath
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
49
fictionarchive-web-astro/src/lib/graphql/urqlStore.ts
Normal file
49
fictionarchive-web-astro/src/lib/graphql/urqlStore.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { OperationResult, TypedDocumentNode } from '@urql/core';
|
||||
import { client } from './client';
|
||||
|
||||
export function queryStore<Data, Variables extends object>(
|
||||
query: TypedDocumentNode<Data, Variables>,
|
||||
variables: Variables
|
||||
) {
|
||||
const result = writable<OperationResult<Data> | null>(null);
|
||||
const fetching = writable(true);
|
||||
|
||||
async function execute(vars: Variables = variables) {
|
||||
fetching.set(true);
|
||||
const res = await client.query(query, vars).toPromise();
|
||||
result.set(res);
|
||||
fetching.set(false);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Initial fetch
|
||||
execute();
|
||||
|
||||
return {
|
||||
subscribe: result.subscribe,
|
||||
fetching: { subscribe: fetching.subscribe },
|
||||
refetch: execute,
|
||||
};
|
||||
}
|
||||
|
||||
export function mutationStore<Data, Variables extends object>(
|
||||
mutation: TypedDocumentNode<Data, Variables>
|
||||
) {
|
||||
const result = writable<OperationResult<Data> | null>(null);
|
||||
const fetching = writable(false);
|
||||
|
||||
async function execute(variables: Variables) {
|
||||
fetching.set(true);
|
||||
const res = await client.mutation(mutation, variables).toPromise();
|
||||
result.set(res);
|
||||
fetching.set(false);
|
||||
return res;
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: result.subscribe,
|
||||
fetching: { subscribe: fetching.subscribe },
|
||||
execute,
|
||||
};
|
||||
}
|
||||
13
fictionarchive-web-astro/src/lib/utils.ts
Normal file
13
fictionarchive-web-astro/src/lib/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
23
fictionarchive-web-astro/src/pages/404.astro
Normal file
23
fictionarchive-web-astro/src/pages/404.astro
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
export const prerender = true; // Static page
|
||||
|
||||
import AppLayout from '../layouts/AppLayout.astro';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../lib/components/ui/card';
|
||||
import { Button } from '../lib/components/ui/button';
|
||||
---
|
||||
|
||||
<AppLayout title="Not Found - FictionArchive">
|
||||
<div class="flex min-h-[50vh] items-center justify-center">
|
||||
<Card class="mx-auto max-w-md text-center shadow-md shadow-primary/10">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-4xl font-bold">404</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<p class="text-muted-foreground">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<Button href="/" variant="default">Go back home</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</AppLayout>
|
||||
10
fictionarchive-web-astro/src/pages/index.astro
Normal file
10
fictionarchive-web-astro/src/pages/index.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
export const prerender = true; // Static page
|
||||
|
||||
import AppLayout from '../layouts/AppLayout.astro';
|
||||
import NovelsPage from '../lib/components/NovelsPage.svelte';
|
||||
---
|
||||
|
||||
<AppLayout title="Novels - FictionArchive">
|
||||
<NovelsPage client:load />
|
||||
</AppLayout>
|
||||
11
fictionarchive-web-astro/src/pages/novels/[id].astro
Normal file
11
fictionarchive-web-astro/src/pages/novels/[id].astro
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
// SSR page (default in server mode, no prerender export needed)
|
||||
import AppLayout from '../../layouts/AppLayout.astro';
|
||||
import NovelDetailPage from '../../lib/components/NovelDetailPage.svelte';
|
||||
|
||||
const { id } = Astro.params;
|
||||
---
|
||||
|
||||
<AppLayout title="Novel Detail - FictionArchive">
|
||||
<NovelDetailPage novelId={id} client:load />
|
||||
</AppLayout>
|
||||
141
fictionarchive-web-astro/src/styles/global.css
Normal file
141
fictionarchive-web-astro/src/styles/global.css
Normal file
@@ -0,0 +1,141 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.75rem;
|
||||
/* Purple/Indigo theme matching original React app */
|
||||
--background: oklch(0.98 0.01 250);
|
||||
--foreground: oklch(0.2 0.04 260);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.2 0.04 260);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.2 0.04 260);
|
||||
--primary: oklch(0.55 0.25 270);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.95 0.02 270);
|
||||
--secondary-foreground: oklch(0.2 0.04 260);
|
||||
--muted: oklch(0.93 0.02 250);
|
||||
--muted-foreground: oklch(0.5 0.03 250);
|
||||
--accent: oklch(0.96 0.03 250);
|
||||
--accent-foreground: oklch(0.2 0.04 260);
|
||||
--destructive: oklch(0.6 0.24 25);
|
||||
--destructive-foreground: oklch(1 0 0);
|
||||
--border: oklch(0.92 0.02 250);
|
||||
--input: oklch(0.92 0.02 250);
|
||||
--ring: oklch(0.55 0.25 270);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: oklch(0.2 0.04 260);
|
||||
--sidebar-primary: oklch(0.55 0.25 270);
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.95 0.02 270);
|
||||
--sidebar-accent-foreground: oklch(0.2 0.04 260);
|
||||
--sidebar-border: oklch(0.92 0.02 250);
|
||||
--sidebar-ring: oklch(0.55 0.25 270);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.2 0.04 260);
|
||||
--foreground: oklch(0.98 0.01 250);
|
||||
--card: oklch(0.2 0.04 260);
|
||||
--card-foreground: oklch(0.98 0.01 250);
|
||||
--popover: oklch(0.2 0.04 260);
|
||||
--popover-foreground: oklch(0.98 0.01 250);
|
||||
--primary: oklch(0.7 0.2 270);
|
||||
--primary-foreground: oklch(0.2 0.04 260);
|
||||
--secondary: oklch(0.25 0.04 260);
|
||||
--secondary-foreground: oklch(0.98 0.01 250);
|
||||
--muted: oklch(0.25 0.04 260);
|
||||
--muted-foreground: oklch(0.65 0.02 250);
|
||||
--accent: oklch(0.25 0.04 260);
|
||||
--accent-foreground: oklch(0.98 0.01 250);
|
||||
--destructive: oklch(0.6 0.24 25);
|
||||
--destructive-foreground: oklch(0.98 0.01 250);
|
||||
--border: oklch(0.3 0.04 260);
|
||||
--input: oklch(0.3 0.04 260);
|
||||
--ring: oklch(0.7 0.2 270);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.2 0.04 260);
|
||||
--sidebar-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-primary: oklch(0.7 0.2 270);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-accent: oklch(0.25 0.04 260);
|
||||
--sidebar-accent-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-border: oklch(0.3 0.04 260);
|
||||
--sidebar-ring: oklch(0.7 0.2 270);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground min-h-screen antialiased;
|
||||
font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
/* Subtle gradient background matching original React app */
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 20%, oklch(0.95 0.05 270 / 50%), transparent 32%),
|
||||
radial-gradient(circle at 80% 10%, oklch(0.95 0.05 150 / 40%), transparent 25%),
|
||||
radial-gradient(circle at 50% 80%, oklch(0.95 0.05 25 / 30%), transparent 20%),
|
||||
linear-gradient(180deg, oklch(1 0 0 / 90%), oklch(0.98 0.01 250 / 95%));
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
/* Link styling */
|
||||
a:not([class]) {
|
||||
@apply text-primary font-medium hover:underline;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user