mirror of
https://codeberg.org/Freedium-cfd/web.git
synced 2026-03-11 09:04:37 +00:00
fix(new-freedium): bug fixes, settings restructure, minor UI fixes, overlay loader implement
This commit is contained in:
parent
286691ea87
commit
39b539a31c
17 changed files with 392 additions and 206 deletions
|
|
@ -1,4 +1,12 @@
|
|||
from freedium_library.__init__ import __VERSION__
|
||||
from freedium_library.api.container import APIContainer
|
||||
|
||||
__NAME__ = "Freedium Library API"
|
||||
|
||||
api_container = APIContainer()
|
||||
api_container.wire(
|
||||
modules=["freedium_library.api.settings", "freedium_library.api.main"]
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["__NAME__", "__VERSION__"]
|
||||
|
|
|
|||
|
|
@ -10,17 +10,23 @@ from freedium_library.api.settings import ApplicationSettings
|
|||
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
container = APIContainer()
|
||||
container.wire(modules=["freedium_library.api.settings"])
|
||||
api_container = APIContainer()
|
||||
|
||||
settings = ApplicationSettings(container=container)
|
||||
config = container.config()
|
||||
settings = ApplicationSettings(container=api_container)
|
||||
config = api_container.config()
|
||||
|
||||
if config.DISABLED_DOCS:
|
||||
logger.warning(f"Documentation is disabled: {config.DISABLED_DOCS}")
|
||||
settings.disable_docs()
|
||||
|
||||
app = FastAPI(**settings.to_dict(), lifespan=lifespan) # type: ignore
|
||||
app = FastAPI(
|
||||
title=settings.title,
|
||||
version=settings.version,
|
||||
openapi_url=settings.openapi_url,
|
||||
docs_url=settings.docs_url,
|
||||
redoc_url=settings.redoc_url,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
register_router(app, settings.prefix_path)
|
||||
register_error_handler(app)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,25 @@
|
|||
# freedium-library/src/freedium_library/api/config.py
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from freedium_library.utils.meta.pydantic import BaseConfig, BaseSettingsConfigDict
|
||||
|
||||
|
||||
class ServerConfig(BaseConfig):
|
||||
model_config = BaseSettingsConfigDict(env_prefix="SERVER_")
|
||||
|
||||
host: str = Field(default="0.0.0.0")
|
||||
port: int = Field(default=7080)
|
||||
reload: bool = Field(default=False)
|
||||
workers: Optional[int] = Field(default=None)
|
||||
|
||||
|
||||
class APIConfig(BaseConfig):
|
||||
model_config = BaseSettingsConfigDict(env_prefix="API_")
|
||||
|
||||
DISABLED_DOCS: bool = Field(default=False)
|
||||
PREFIX_PATH: str = Field(default="/api")
|
||||
HOST: str = Field(default="0.0.0.0")
|
||||
PORT: int = Field(default=7080)
|
||||
MAX_WORKERS: int = Field(default=10)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from dependency_injector import containers, providers
|
||||
|
||||
from freedium_library.api.config import APIConfig
|
||||
from freedium_library.api.config import APIConfig, ServerConfig
|
||||
|
||||
|
||||
class APIContainer(containers.DeclarativeContainer):
|
||||
config = providers.Singleton(APIConfig)
|
||||
server_config = providers.Singleton(ServerConfig)
|
||||
|
|
|
|||
|
|
@ -2,23 +2,52 @@ import os
|
|||
from typing import Optional
|
||||
|
||||
import uvicorn
|
||||
from dependency_injector.wiring import Provide, inject
|
||||
from loguru import logger
|
||||
|
||||
from freedium_library.api.config import APIConfig, ServerConfig
|
||||
from freedium_library.api.container import APIContainer
|
||||
|
||||
|
||||
def calculate_workers(
|
||||
requested_workers: Optional[int] = None, max_workers: int = 10
|
||||
) -> int:
|
||||
if requested_workers is not None:
|
||||
return requested_workers
|
||||
|
||||
cpu_count = os.cpu_count() or 1
|
||||
workers = min(cpu_count + 1, max_workers)
|
||||
|
||||
if workers == 1:
|
||||
logger.warning("Only one worker, this is not recommended for production.")
|
||||
elif workers == max_workers:
|
||||
logger.warning(
|
||||
f"Using hardcoded maximum workers ({max_workers}), consider passing a lower number."
|
||||
)
|
||||
else:
|
||||
logger.info(f"Using {workers} workers based on CPU count.")
|
||||
|
||||
return workers
|
||||
|
||||
|
||||
@inject
|
||||
def start_server(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 7080,
|
||||
reload: bool = False,
|
||||
workers: Optional[int] = None,
|
||||
server_config: Optional[ServerConfig] = Provide[APIContainer.server_config],
|
||||
config: APIConfig = Provide[APIContainer.config],
|
||||
) -> None:
|
||||
if workers is None:
|
||||
workers = (os.cpu_count() or 1) + 1
|
||||
logger.warning(f"No workers specified, using CPU count {workers}")
|
||||
if server_config is None:
|
||||
logger.warning("No server config provided, using defaults.")
|
||||
server_config = ServerConfig(
|
||||
host=config.HOST,
|
||||
port=config.PORT,
|
||||
)
|
||||
|
||||
workers = calculate_workers(server_config.workers, config.MAX_WORKERS)
|
||||
|
||||
uvicorn.run(
|
||||
"freedium_library.api.app:app",
|
||||
host=host,
|
||||
port=port,
|
||||
reload=reload,
|
||||
host=server_config.host,
|
||||
port=server_config.port,
|
||||
reload=server_config.reload,
|
||||
workers=workers,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@ from freedium_library.api.container import APIContainer
|
|||
|
||||
|
||||
class ApplicationSettings(BaseModel):
|
||||
title: str = Field(default=f"{__NAME__} API Service")
|
||||
version: str = Field(default=__VERSION__)
|
||||
title: str = Field(default=f"{__NAME__} API Service", frozen=True)
|
||||
version: str = Field(default=__VERSION__, frozen=True)
|
||||
prefix_path: str = Field(default="/api")
|
||||
openapi_url: Optional[str] = None
|
||||
docs_url: Optional[str] = None
|
||||
redoc_url: Optional[str] = None
|
||||
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
@inject
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -41,6 +39,3 @@ class ApplicationSettings(BaseModel):
|
|||
self.openapi_url = None
|
||||
self.docs_url = None
|
||||
self.redoc_url = None
|
||||
|
||||
def to_dict(self) -> dict[str, str | None]:
|
||||
return self.model_dump()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import click
|
||||
|
||||
from freedium_library.api.config import ServerConfig
|
||||
from freedium_library.api.main import start_server
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option("--host", default="0.0.0.0", help="Host address to bind to")
|
||||
|
|
@ -11,6 +14,9 @@ import click
|
|||
default=False,
|
||||
)
|
||||
def cli(host: str, port: int, hot_reload: bool):
|
||||
from freedium_library.api.main import start_server
|
||||
|
||||
start_server(host=host, port=port, reload=hot_reload)
|
||||
server_config = ServerConfig(
|
||||
host=host,
|
||||
port=port,
|
||||
reload=hot_reload,
|
||||
)
|
||||
start_server(server_config=server_config)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2,6 +2,7 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body,
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import DrawerOverlay from "./drawer-overlay.svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
import DrawerOverlay from './drawer-overlay.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
type $$Props = DrawerPrimitive.ContentProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
|
|
@ -13,12 +13,12 @@
|
|||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
class={cn(
|
||||
"bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border",
|
||||
'bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border',
|
||||
className
|
||||
)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<div class="bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full"></div>
|
||||
<div class="bg-primary mx-auto mt-4 h-2 w-[100px] rounded-full"></div>
|
||||
<slot />
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPrimitive.Portal>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
type $$Props = DrawerPrimitive.OverlayProps;
|
||||
|
||||
export let el: $$Props["el"] = undefined;
|
||||
let className: $$Props["class"] = undefined;
|
||||
export let el: $$Props['el'] = undefined;
|
||||
let className: $$Props['class'] = undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Overlay
|
||||
bind:el
|
||||
class={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
class={cn('fixed inset-0 z-50 bg-black/80', className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import Header from '$lib/elements/Header.svelte';
|
||||
import { formatDate } from '$lib/utils/dateFormatter';
|
||||
import ImageZoom from '$lib/elements/ImageZoom.svelte';
|
||||
|
|
@ -10,118 +11,174 @@
|
|||
|
||||
$: ({ article, content, loading } = data);
|
||||
$: contentLoaded = !loading && !!content;
|
||||
$: error = data.error;
|
||||
$: showSkeleton = !error && !contentLoaded;
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (!error) return '';
|
||||
|
||||
switch (error.code) {
|
||||
case 'ARTICLE_NOT_FOUND':
|
||||
return "We couldn't find the article you're looking for.";
|
||||
case 'RENDER_ERROR':
|
||||
return 'There was a problem preparing this article.';
|
||||
case 'COMPILE_ERROR':
|
||||
return 'There was a problem processing the article content.';
|
||||
default:
|
||||
return error.message || 'An unexpected error occurred.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{article.title} - Freedium</title>
|
||||
<title>{error ? 'Freedium' : data?.article?.title || 'Freedium'} - Freedium</title>
|
||||
<meta name="description" content="Read about the latest updates to UploadThing" />
|
||||
</svelte:head>
|
||||
|
||||
<Header />
|
||||
|
||||
<main class="max-w-5xl px-4 py-8 mx-auto">
|
||||
<nav class="flex items-center justify-between gap-2 mb-4 text-center">
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center justify-center transition bg-white rounded-full shadow-md text-primary hover:text-primary/90 group size-8 shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20"
|
||||
>
|
||||
<span class="icon-[heroicons--arrow-left-20-solid] size-6" />
|
||||
</a>
|
||||
<a href="/" class="font-bold text-primary hover:text-primary/90"> Original article</a>
|
||||
</nav>
|
||||
|
||||
<div class="lg:flex lg:space-x-8">
|
||||
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
{#if !contentLoaded}
|
||||
<Skeleton class="w-full h-96" />
|
||||
<div class="p-6 bg-gray-50 dark:bg-zinc-800">
|
||||
<Skeleton class="w-32 h-4 mb-2" />
|
||||
<Skeleton class="w-full h-10 mb-4" />
|
||||
<div class="flex items-center">
|
||||
<Skeleton class="w-12 h-12 mr-4 rounded-full" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="w-40 h-4" />
|
||||
<Skeleton class="w-32 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-3/4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if article.postImage}
|
||||
<ImageZoom
|
||||
src={article.postImage}
|
||||
alt="Post cover image"
|
||||
class="object-cover w-full h-auto min-h-96"
|
||||
/>
|
||||
{/if}
|
||||
<header class="p-6 bg-gray-50 dark:bg-zinc-800">
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">{formatDate(article.date)}</p>
|
||||
<h1 class="mb-4 text-4xl font-bold text-gray-900 dark:text-white">{article.title}</h1>
|
||||
<div class="flex items-center">
|
||||
<img src={article.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{article.author.name}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">{article.author.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="p-6 {article.postImage ? '' : 'pt-0'} dark:text-gray-300">
|
||||
<div class="prose max-w-none">
|
||||
{#if content}
|
||||
{@html content}
|
||||
{:else}
|
||||
<p>Error loading content</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<main class="flex-1 w-full h-full max-w-5xl px-4 py-8">
|
||||
<nav class="flex items-center justify-between gap-2 mb-4 text-center">
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center justify-center transition bg-white rounded-full shadow-md text-primary hover:text-primary/90 group size-8 shadow-zinc-800/5 ring-1 ring-zinc-900/5 dark:border dark:border-zinc-700/50 dark:bg-zinc-800 dark:ring-0 dark:ring-white/10 dark:hover:border-zinc-700 dark:hover:ring-white/20"
|
||||
>
|
||||
<span class="icon-[heroicons--arrow-left-20-solid] size-6" />
|
||||
</a>
|
||||
{#if article?.url}
|
||||
<a href={article.url} class="font-bold text-primary hover:text-primary/90">
|
||||
Original article
|
||||
</a>
|
||||
{/if}
|
||||
</article>
|
||||
</nav>
|
||||
|
||||
<aside class="order-first mt-7 lg:mt-0 lg:min-w-80 lg:order-none">
|
||||
{#if !contentLoaded}
|
||||
<div class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
<Skeleton class="w-32 h-6 mb-4" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-3/4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<nav
|
||||
aria-labelledby="toc-heading"
|
||||
class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900 lg:sticky lg:top-36"
|
||||
{#if error}
|
||||
<div class="p-6 text-center bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
<h1
|
||||
class="mb-4 text-2xl font-bold {error.status === 404 ? 'text-amber-600' : 'text-red-600'}"
|
||||
>
|
||||
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Contents
|
||||
</h2>
|
||||
{#if article.tableOfContents && article.tableOfContents.length > 0}
|
||||
<ul class="space-y-2">
|
||||
{#each article.tableOfContents as item}
|
||||
<li>
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
class="transition-colors text-zinc-800 hover:text-zinc-900 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="dark:text-gray-300">No table of contents available</p>
|
||||
{/if}
|
||||
</nav>
|
||||
{/if}
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
{error.status === 404 ? 'Article Not Found' : 'Error Loading Article'}
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
{getErrorMessage(error)}
|
||||
</p>
|
||||
{#if error.details && process.env.NODE_ENV === 'development'}
|
||||
<pre class="p-4 mt-4 overflow-auto text-sm bg-gray-100 dark:bg-zinc-800">
|
||||
{error.details}
|
||||
</pre>
|
||||
{/if}
|
||||
<div class="mt-6 space-x-4">
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block px-4 py-2 text-white rounded-md bg-primary hover:bg-primary/90"
|
||||
>
|
||||
Return Home
|
||||
</a>
|
||||
<button
|
||||
class="inline-block px-4 py-2 border rounded-md border-primary text-primary hover:bg-primary/10"
|
||||
on:click={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if showSkeleton}
|
||||
<div class="lg:flex lg:space-x-8">
|
||||
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
<Skeleton class="w-full h-96" />
|
||||
<div class="p-6 bg-gray-50 dark:bg-zinc-800">
|
||||
<Skeleton class="w-32 h-4 mb-2" />
|
||||
<Skeleton class="w-full h-10 mb-4" />
|
||||
<div class="flex items-center">
|
||||
<Skeleton class="w-12 h-12 mr-4 rounded-full" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="w-40 h-4" />
|
||||
<Skeleton class="w-32 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-3/4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<Footer />
|
||||
<aside class="order-first mt-7 lg:mt-0 lg:min-w-80 lg:order-none">
|
||||
<div class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
<Skeleton class="w-32 h-6 mb-4" />
|
||||
<div class="space-y-2">
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-full h-4" />
|
||||
<Skeleton class="w-3/4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="lg:flex lg:space-x-8">
|
||||
<article class="flex-grow overflow-hidden bg-white rounded-lg shadow-lg dark:bg-zinc-900">
|
||||
{#if article.postImage}
|
||||
<ImageZoom
|
||||
src={article.postImage}
|
||||
alt="Post cover image"
|
||||
class="object-cover w-full h-auto min-h-96"
|
||||
/>
|
||||
{/if}
|
||||
<header class="p-6 bg-gray-50 dark:bg-zinc-800">
|
||||
<p class="mb-2 text-gray-600 dark:text-gray-400">{formatDate(article.date)}</p>
|
||||
<h1 class="mb-4 text-4xl font-bold text-gray-900 dark:text-white">{article.title}</h1>
|
||||
<div class="flex items-center">
|
||||
<img src={article.author.avatar} alt="" class="w-12 h-12 mr-4 rounded-full" />
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900 dark:text-white">{article.author.name}</p>
|
||||
<p class="text-gray-600 dark:text-gray-400">{article.author.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="p-6 {article.postImage ? '' : 'pt-0'} dark:text-gray-300">
|
||||
<div class="prose max-w-none">
|
||||
{#if content}
|
||||
{@html content}
|
||||
{:else}
|
||||
<p>Error loading content</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="order-first mt-7 lg:mt-0 lg:min-w-80 lg:order-none">
|
||||
<nav
|
||||
aria-labelledby="toc-heading"
|
||||
class="w-full p-4 bg-white rounded-lg shadow-lg dark:bg-zinc-900 lg:sticky lg:top-36"
|
||||
>
|
||||
<h2 id="toc-heading" class="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Contents
|
||||
</h2>
|
||||
{#if article.tableOfContents && article.tableOfContents.length > 0}
|
||||
<ul class="space-y-2">
|
||||
{#each article.tableOfContents as item}
|
||||
<li>
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
class="transition-colors text-zinc-800 hover:text-zinc-900 dark:text-gray-300 dark:hover:text-white"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="dark:text-gray-300">No table of contents available</p>
|
||||
{/if}
|
||||
</nav>
|
||||
</aside>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
24
new-web/src/lib/elements/ProgressOverlay.svelte
Normal file
24
new-web/src/lib/elements/ProgressOverlay.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { navigating } from '$app/stores';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const MIN_DURATION = 400;
|
||||
const isVisible = writable(false);
|
||||
|
||||
$: {
|
||||
if ($navigating) {
|
||||
$isVisible = true;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
$isVisible = false;
|
||||
}, MIN_DURATION);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $isVisible}
|
||||
<div class="fixed inset-0 z-40 bg-black/30 dark:bg-black/40" transition:fade={{ duration: 300 }}>
|
||||
<div class="fixed z-50 top-4 right-4"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -102,62 +102,66 @@
|
|||
</div>
|
||||
</Drawer.Trigger>
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<Drawer.Content>
|
||||
<Drawer.Header class="text-left">
|
||||
<Drawer.Title class="text-lg font-semibold tracking-tight">Report a Problem</Drawer.Title>
|
||||
<Drawer.Description class="text-foreground-alt">
|
||||
Please describe the problem you're experiencing. We'll look into it as soon as possible.
|
||||
</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<div class="px-4">
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 pt-7">
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
<Label.Root class="text-sm font-medium">Problem Type</Label.Root>
|
||||
<RadioGroup.Root bind:value={problemType} class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="ui_problem" id="ui-problem-mobile" />
|
||||
<Label.Root for="ui-problem-mobile">UI problem</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="article_not_full" id="article-not-full-mobile" />
|
||||
<Label.Root for="article-not-full-mobile">Article is not full</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="suggestion" id="suggestion-mobile" />
|
||||
<Label.Root for="suggestion-mobile">Suggestion</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="vulnerability" id="vulnerability-mobile" />
|
||||
<Label.Root for="vulnerability-mobile">Vulnerability (XSS, etc.)</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="other" id="other-mobile" />
|
||||
<Label.Root for="other-mobile">Other</Label.Root>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
<Drawer.Content class="max-h-[90dvh] flex flex-col">
|
||||
<div class="flex-1 mb-5 overflow-y-auto">
|
||||
<Drawer.Header class="text-left">
|
||||
<Drawer.Title class="text-lg font-semibold tracking-tight"
|
||||
>Report a Problem</Drawer.Title
|
||||
>
|
||||
<Drawer.Description class="text-foreground-alt">
|
||||
Please describe the problem you're experiencing. We'll look into it as soon as
|
||||
possible.
|
||||
</Drawer.Description>
|
||||
</Drawer.Header>
|
||||
<div class="px-4 space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col items-start space-y-2">
|
||||
<Label.Root class="text-sm font-medium">Problem Type</Label.Root>
|
||||
<RadioGroup.Root bind:value={problemType} class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="ui_problem" id="ui-problem-mobile" />
|
||||
<Label.Root for="ui-problem-mobile">UI problem</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="article_not_full" id="article-not-full-mobile" />
|
||||
<Label.Root for="article-not-full-mobile">Article is not full</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="suggestion" id="suggestion-mobile" />
|
||||
<Label.Root for="suggestion-mobile">Suggestion</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="vulnerability" id="vulnerability-mobile" />
|
||||
<Label.Root for="vulnerability-mobile">Vulnerability (XSS, etc.)</Label.Root>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="other" id="other-mobile" />
|
||||
<Label.Root for="other-mobile">Other</Label.Root>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<div class="flex flex-col items-start space-y-2">
|
||||
<Label.Root for="problemDescription-mobile" class="text-sm font-medium"
|
||||
>Problem Description</Label.Root
|
||||
>
|
||||
<Textarea
|
||||
id="problemDescription-mobile"
|
||||
bind:value={problemDescription}
|
||||
placeholder="Describe the problem you're experiencing..."
|
||||
rows={12}
|
||||
/>
|
||||
<p class="mt-2 text-sm text-foreground-alt">
|
||||
The current opened page will be automatically attached to your report.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<Label.Root for="problemDescription-mobile" class="text-sm font-medium"
|
||||
>Problem Description</Label.Root
|
||||
>
|
||||
<Textarea
|
||||
id="problemDescription-mobile"
|
||||
bind:value={problemDescription}
|
||||
placeholder="Describe the problem you're experiencing..."
|
||||
rows={12}
|
||||
/>
|
||||
<p class="mt-2 text-sm text-foreground-alt">
|
||||
The current opened page will be automatically attached to your report.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center gap-2">
|
||||
<Drawer.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Drawer.Close>
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer.Footer class="p-4">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Drawer.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Drawer.Close>
|
||||
<Button type="submit">Submit</Button>
|
||||
</div>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</form>
|
||||
</Drawer.Root>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import ProgressOverlay from '$lib/elements/ProgressOverlay.svelte';
|
||||
</script>
|
||||
|
||||
<ProgressOverlay />
|
||||
<Toaster position="top-right" expand={true} />
|
||||
<slot />
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@ const CODE_ATTRIBUTES = {
|
|||
"data-ms-editor": "false",
|
||||
};
|
||||
|
||||
let highlighterInstance = null;
|
||||
|
||||
async function getHighlighter() {
|
||||
if (!highlighterInstance) {
|
||||
highlighterInstance = await createHighlighter(HIGHLIGHT_CONFIG);
|
||||
await highlighterInstance.loadLanguage("javascript", "typescript");
|
||||
}
|
||||
return highlighterInstance;
|
||||
}
|
||||
|
||||
const MOCK_ARTICLE = {
|
||||
title: "UploadThing is 5x Faster",
|
||||
date: "2024-09-13T12:00:00Z",
|
||||
|
|
@ -75,8 +85,7 @@ function createCodeCopyButton(code, toggleMs = 3000) {
|
|||
}
|
||||
|
||||
async function createHighlightedCode(code, lang = "text") {
|
||||
const highlighter = await createHighlighter(HIGHLIGHT_CONFIG);
|
||||
await highlighter.loadLanguage("javascript", "typescript");
|
||||
const highlighter = await getHighlighter();
|
||||
|
||||
const lightHtml = highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
|
|
@ -115,9 +124,36 @@ async function createHighlightedCode(code, lang = "text") {
|
|||
`;
|
||||
}
|
||||
|
||||
const ErrorCodes = {
|
||||
ARTICLE_NOT_FOUND: "ARTICLE_NOT_FOUND",
|
||||
RENDER_ERROR: "RENDER_ERROR",
|
||||
COMPILE_ERROR: "COMPILE_ERROR",
|
||||
INTERNAL_ERROR: "INTERNAL_ERROR",
|
||||
};
|
||||
|
||||
export async function load({ params }) {
|
||||
let transformed = null;
|
||||
try {
|
||||
transformed = await render("medium");
|
||||
} catch (err) {
|
||||
console.error("Failed to render article:", err);
|
||||
}
|
||||
|
||||
if (!transformed) {
|
||||
return {
|
||||
slug: params.slug,
|
||||
loading: false,
|
||||
content: null,
|
||||
article: null,
|
||||
error: {
|
||||
status: 404,
|
||||
message: "Article not found",
|
||||
code: ErrorCodes.ARTICLE_NOT_FOUND,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const transformed = await render("medium");
|
||||
const { code } = await compile(transformed.text, {
|
||||
highlight: {
|
||||
highlighter: createHighlightedCode,
|
||||
|
|
@ -129,21 +165,23 @@ export async function load({ params }) {
|
|||
loading: false,
|
||||
content: code,
|
||||
article: MOCK_ARTICLE,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load article:", error);
|
||||
} catch (compileError) {
|
||||
return {
|
||||
slug: params.slug,
|
||||
loading: false,
|
||||
content: null,
|
||||
article: {
|
||||
title: "Article Not Found",
|
||||
date: new Date().toISOString(),
|
||||
author: { name: "Unknown", role: "", avatar: "" },
|
||||
postImage: null,
|
||||
tableOfContents: [],
|
||||
article: null,
|
||||
error: {
|
||||
status: 500,
|
||||
message: "Failed to compile article content",
|
||||
code: ErrorCodes.COMPILE_ERROR,
|
||||
details:
|
||||
process.env.NODE_ENV === "development"
|
||||
? compileError.message
|
||||
: undefined,
|
||||
},
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ url_for_test = {
|
|||
"https://www.freedium.cfd/c81e00f6320d",
|
||||
}
|
||||
|
||||
blacklist_url = {"51e23c5a2aac"}
|
||||
blacklist_url = {"51e23c5a2aac", "e65e737baa29"}
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
|||
Loading…
Reference in a new issue