Add Backend API

add backend api and add some element of the webpage
This commit is contained in:
jasinco 2024-11-25 07:02:06 +08:00
parent ec388f1b47
commit 0a754c17a3
21 changed files with 649 additions and 104 deletions

3
justfile Normal file
View File

@ -0,0 +1,3 @@
genproto:
npx protoc --ts_out ./src/lib/protobuf --proto_path ./src/lib/protobuf/definitions ./src/lib/protobuf/definitions/niming.proto

View File

@ -34,6 +34,8 @@
"packageManager": "yarn@4.2.2+sha224.1e50daf19e5e249a025569752c60b88005fddf57d10fcde5fc68b88f", "packageManager": "yarn@4.2.2+sha224.1e50daf19e5e249a025569752c60b88005fddf57d10fcde5fc68b88f",
"dependencies": { "dependencies": {
"@iconify-json/mingcute": "^1.2.1", "@iconify-json/mingcute": "^1.2.1",
"@protobuf-ts/plugin": "^2.9.4",
"axios": "^1.7.7",
"dompurify": "^3.1.7", "dompurify": "^3.1.7",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",
"isomorphic-dompurify": "^2.16.0", "isomorphic-dompurify": "^2.16.0",

View File

@ -34,6 +34,7 @@
--ring: 224 71.4% 4.1%; --ring: 224 71.4% 4.1%;
--radius: 0.5rem; --radius: 0.5rem;
} }
.dark { .dark {
@ -49,7 +50,7 @@
--card: 35 88% 72%; --card: 35 88% 72%;
--card-foreground: 248 13% 36%; --card-foreground: 248 13% 36%;
--border: 215 27.9% 16.9%; --border: 248 13% 36%;
--input: 215 27.9% 16.9%; --input: 215 27.9% 16.9%;
--primary: 248deg 25% 18%; --primary: 248deg 25% 18%;
@ -85,12 +86,56 @@
} }
@font-face { @font-face {
font-family: "GenRyuMin2TW"; font-family: "PingFang";
src: src:
url("/GenRyuMin2TW-M.woff2") format("woff2"); local('PingFang Bold'), src("/PingFang Bold.woff2") type("woff2");
font-weight: bold;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Medium'), src("/PingFang Medium.woff2") type("woff2");
font-weight: normal;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Heavy'), src("/PingFang Heavy.woff2") type("woff2");
font-weight: bolder;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Light'), src("/PingFang Light.woff2") type("woff2");
font-weight: lighter;
}
@font-face {
font-family: "PingFang";
src:
local('PingFang Regular'), src("/PingFang Regular.woff2") type("woff2");
font-weight: normal;
} }
body { body {
font-family: "GenRyuMin2TW"; font-family: "PingFang";
font-weight: 400;
overflow-x: hidden; overflow-x: hidden;
} }
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background-color: hsl(var(--muted))
}
::-webkit-scrollbar-thumb {
background-color: hsl(248deg, 13%, 36%);
border-radius: 10px;
}

63
src/lib/api.ts Normal file
View File

@ -0,0 +1,63 @@
import { Fetch, Post, PostResponse } from '$lib/protobuf/niming';
import axios from 'axios';
export const ResultEnum = {
Ok: 1,
Err: 2
} as const;
export type ResultEnum = typeof ResultEnum[keyof typeof ResultEnum];
export interface Result<T> {
kind: ResultEnum;
message: string
enclose?: T;
}
export class API {
page: number
constructor() {
this.page = 0
}
async send_post(content: string, refer: bigint | undefined, files: File[] | undefined) {
let readed: Uint8Array[] = [];
files?.forEach((e) => {
e.arrayBuffer().then((arrbuf) => {
readed.push(new Uint8Array(arrbuf));
});
});
let ctx = Post.create({ content: content, files: readed });
if (refer !== undefined) {
ctx.ref = refer;
}
let result: Result<PostResponse> = { kind: ResultEnum.Err, message: "" };
await axios
.post('/article/', Post.toBinary(ctx), {
headers: { 'Content-Type': 'application/x-protobuf' },
responseType: "arraybuffer"
})
.then((e) => {
result.kind = ResultEnum.Ok;
result.enclose = PostResponse.fromBinary(e.data);
})
.catch((err) => {
if (err.response) {
result.message = err.response.data.error;
}
});
return result;
}
async fetch() {
let result: Result<Fetch> = { kind: ResultEnum.Err, message: "" };
axios.get("/article/list", { responseType: "arraybuffer" }).then((e) => {
result.kind = ResultEnum.Ok;
result.enclose = Fetch.fromBinary(e.data);
}).catch((e) => {
});
this.page++;
return result;
}
}
let api_inst = new API();
export default api_inst;

View File

@ -1 +1,2 @@
// place files you want to import through the `$lib` alias in this folder. // place files you want to import through the `$lib` alias in this folder.
export * from './textmark'

1
src/lib/markdown.css Normal file
View File

@ -0,0 +1 @@
@import '$lib/hljs/rose-pine.css'

View File

@ -0,0 +1,29 @@
syntax = "proto3";
// This is for posting a paragraph.
message Post {
string content = 1;
// reply to a post, like a mail chat.
optional int64 ref = 2;
repeated bytes files = 3;
}
// The response of the posting, defining what should return.
message PostResponse {
string hash = 1;
uint64 id = 2;
}
message Fetch {
message Message {
uint64 id = 1;
string content = 2;
// reply to a post, like a mail chat.
optional uint64 ref = 3;
// request files through /article/file/<id> with MIME type.
// See it as a BLOB url;
repeated uint64 files_id = 4;
}
// Several post info
repeated Message posts = 1;
}

327
src/lib/protobuf/niming.ts Normal file
View File

@ -0,0 +1,327 @@
// @generated by protobuf-ts 2.9.4
// @generated from protobuf file "niming.proto" (syntax proto3)
// tslint:disable
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
/**
* This is for posting a paragraph.
*
* @generated from protobuf message Post
*/
export interface Post {
/**
* @generated from protobuf field: string content = 1;
*/
content: string;
/**
* reply to a post, like a mail chat.
*
* @generated from protobuf field: optional int64 ref = 2;
*/
ref?: bigint;
/**
* @generated from protobuf field: repeated bytes files = 3;
*/
files: Uint8Array[];
}
/**
* The response of the posting, defining what should return.
*
* @generated from protobuf message PostResponse
*/
export interface PostResponse {
/**
* @generated from protobuf field: string hash = 1;
*/
hash: string;
/**
* @generated from protobuf field: uint64 id = 2;
*/
id: bigint;
}
/**
* @generated from protobuf message Fetch
*/
export interface Fetch {
/**
* Several post info
*
* @generated from protobuf field: repeated Fetch.Message posts = 1;
*/
posts: Fetch_Message[];
}
/**
* @generated from protobuf message Fetch.Message
*/
export interface Fetch_Message {
/**
* @generated from protobuf field: uint64 id = 1;
*/
id: bigint;
/**
* @generated from protobuf field: string content = 2;
*/
content: string;
/**
* reply to a post, like a mail chat.
*
* @generated from protobuf field: optional uint64 ref = 3;
*/
ref?: bigint;
/**
* request files through /article/file/<id> with MIME type.
* See it as a BLOB url;
*
* @generated from protobuf field: repeated uint64 files_id = 4;
*/
filesId: bigint[];
}
// @generated message type with reflection information, may provide speed optimized methods
class Post$Type extends MessageType<Post> {
constructor() {
super("Post", [
{ no: 1, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "ref", kind: "scalar", opt: true, T: 3 /*ScalarType.INT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 3, name: "files", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 12 /*ScalarType.BYTES*/ }
]);
}
create(value?: PartialMessage<Post>): Post {
const message = globalThis.Object.create((this.messagePrototype!));
message.content = "";
message.files = [];
if (value !== undefined)
reflectionMergePartial<Post>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Post): Post {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string content */ 1:
message.content = reader.string();
break;
case /* optional int64 ref */ 2:
message.ref = reader.int64().toBigInt();
break;
case /* repeated bytes files */ 3:
message.files.push(reader.bytes());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Post, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string content = 1; */
if (message.content !== "")
writer.tag(1, WireType.LengthDelimited).string(message.content);
/* optional int64 ref = 2; */
if (message.ref !== undefined)
writer.tag(2, WireType.Varint).int64(message.ref);
/* repeated bytes files = 3; */
for (let i = 0; i < message.files.length; i++)
writer.tag(3, WireType.LengthDelimited).bytes(message.files[i]);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Post
*/
export const Post = new Post$Type();
// @generated message type with reflection information, may provide speed optimized methods
class PostResponse$Type extends MessageType<PostResponse> {
constructor() {
super("PostResponse", [
{ no: 1, name: "hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
]);
}
create(value?: PartialMessage<PostResponse>): PostResponse {
const message = globalThis.Object.create((this.messagePrototype!));
message.hash = "";
message.id = 0n;
if (value !== undefined)
reflectionMergePartial<PostResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PostResponse): PostResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string hash */ 1:
message.hash = reader.string();
break;
case /* uint64 id */ 2:
message.id = reader.uint64().toBigInt();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: PostResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string hash = 1; */
if (message.hash !== "")
writer.tag(1, WireType.LengthDelimited).string(message.hash);
/* uint64 id = 2; */
if (message.id !== 0n)
writer.tag(2, WireType.Varint).uint64(message.id);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message PostResponse
*/
export const PostResponse = new PostResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Fetch$Type extends MessageType<Fetch> {
constructor() {
super("Fetch", [
{ no: 1, name: "posts", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Fetch_Message }
]);
}
create(value?: PartialMessage<Fetch>): Fetch {
const message = globalThis.Object.create((this.messagePrototype!));
message.posts = [];
if (value !== undefined)
reflectionMergePartial<Fetch>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch): Fetch {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* repeated Fetch.Message posts */ 1:
message.posts.push(Fetch_Message.internalBinaryRead(reader, reader.uint32(), options));
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Fetch, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* repeated Fetch.Message posts = 1; */
for (let i = 0; i < message.posts.length; i++)
Fetch_Message.internalBinaryWrite(message.posts[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Fetch
*/
export const Fetch = new Fetch$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Fetch_Message$Type extends MessageType<Fetch_Message> {
constructor() {
super("Fetch.Message", [
{ no: 1, name: "id", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 2, name: "content", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "ref", kind: "scalar", opt: true, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 4, name: "files_id", kind: "scalar", repeat: 1 /*RepeatType.PACKED*/, T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ }
]);
}
create(value?: PartialMessage<Fetch_Message>): Fetch_Message {
const message = globalThis.Object.create((this.messagePrototype!));
message.id = 0n;
message.content = "";
message.filesId = [];
if (value !== undefined)
reflectionMergePartial<Fetch_Message>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Fetch_Message): Fetch_Message {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* uint64 id */ 1:
message.id = reader.uint64().toBigInt();
break;
case /* string content */ 2:
message.content = reader.string();
break;
case /* optional uint64 ref */ 3:
message.ref = reader.uint64().toBigInt();
break;
case /* repeated uint64 files_id */ 4:
if (wireType === WireType.LengthDelimited)
for (let e = reader.int32() + reader.pos; reader.pos < e;)
message.filesId.push(reader.uint64().toBigInt());
else
message.filesId.push(reader.uint64().toBigInt());
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: Fetch_Message, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* uint64 id = 1; */
if (message.id !== 0n)
writer.tag(1, WireType.Varint).uint64(message.id);
/* string content = 2; */
if (message.content !== "")
writer.tag(2, WireType.LengthDelimited).string(message.content);
/* optional uint64 ref = 3; */
if (message.ref !== undefined)
writer.tag(3, WireType.Varint).uint64(message.ref);
/* repeated uint64 files_id = 4; */
if (message.filesId.length) {
writer.tag(4, WireType.LengthDelimited).fork();
for (let i = 0; i < message.filesId.length; i++)
writer.uint64(message.filesId[i]);
writer.join();
}
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message Fetch.Message
*/
export const Fetch_Message = new Fetch_Message$Type();

43
src/lib/textmark.ts Normal file
View File

@ -0,0 +1,43 @@
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
import DOMPurify from "isomorphic-dompurify";
export default class TextMark {
marked: Marked
constructor() {
this.marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, _) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
this.marked = this.marked.use({
async: true,
gfm: true,
pedantic: false,
hooks: {
postprocess(e) {
return DOMPurify.sanitize(e)
}
}
})
}
async parse(pure_mark: boolean, content: string): Promise<string> {
if (!pure_mark) {
let processed = "";
content.split("\n").forEach((e) => {
processed += e + " \n"
})
content = processed;
}
return this.marked.parse(content);
}
}

View File

@ -3,7 +3,7 @@
let { children } = $props(); let { children } = $props();
</script> </script>
<div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 bg-background"> <div class="sticky top-0 z-10 grid h-16 w-full grid-cols-4 text-nowrap bg-background">
<div class="col-span-3 grid grid-cols-4 text-4xl"> <div class="col-span-3 grid grid-cols-4 text-4xl">
<a class="m-auto" href="/">中工匿名</a> <a class="m-auto" href="/">中工匿名</a>
</div> </div>

View File

@ -2,16 +2,25 @@
import MessageCard from './message_card.svelte'; import MessageCard from './message_card.svelte';
import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill'; import MingcuteAddCircleFill from '~icons/mingcute/add-circle-fill';
import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte'; import Overlay, { toggle_overlay, get_overlay } from './overlay.svelte';
import DOMPurify from 'isomorphic-dompurify'; import Marker, { get_content, files, clear_content } from './marker.svelte';
import Marker from './marker.svelte'; import api_inst, { ResultEnum, type Result } from '$lib/api';
let value: string; let timer = false;
const overlay = () => { const send = () => {
if (!get_overlay()) { if (!timer) {
} else { timer = true;
} api_inst.send_post(get_content(), undefined, files).then((result: Result<null>) => {
alert(result.message);
if (result.kind == ResultEnum.Ok) {
clear_content();
toggle_overlay(); toggle_overlay();
}
setTimeout(() => {
timer = false;
}, 5000);
});
}
}; };
</script> </script>
@ -29,14 +38,17 @@ NormalText
" "
/> />
</div> </div>
<button id="add" onclick={overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button> <button id="add" onclick={toggle_overlay} class="text-3xl"> <MingcuteAddCircleFill /> </button>
<Overlay> <Overlay>
<div class="flex w-4/6 flex-col rounded-lg bg-primary px-5 py-3" style="height:60dvh;"> <div
class="flex flex-col rounded-lg bg-primary px-5 py-3 md:w-4/6 lg:w-1/2"
style="height:60dvh;"
>
<div class="relative h-6 w-full"> <div class="relative h-6 w-full">
<button class="absolute left-0" onclick={overlay}>取消</button> <button class="absolute left-0" onclick={toggle_overlay}>取消</button>
<button class="absolute right-0">發送</button> <button class="absolute right-0" onclickcapture={send}>發送</button>
</div> </div>
<Marker text={value} height="h-[calc(100%-1.5rem)]" /> <Marker />
</div> </div>
</Overlay> </Overlay>

View File

@ -1,15 +1,32 @@
<script lang="ts"> <script module lang="ts">
let { text, height }: { text: string; height: string } = $props(); let content: string = $state<string>('');
import { Textarea } from '$lib/components/ui/textarea';
import { Marked } from 'marked'; let dyn_files = $state<FileList>();
import DOMPurify from 'isomorphic-dompurify';
import type { KeyboardEventHandler } from 'svelte/elements'; let files: Array<File> = [];
import { markedHighlight } from 'marked-highlight'; const get_content = () => {
import hljs from 'highlight.js'; let length = dyn_files == undefined ? 0 : dyn_files.length;
import '$lib/hljs/rose-pine.css'; for (let i = 0; i < length; i++) {
let item = dyn_files?.item(i);
if (item != null) {
files.push(item);
}
}
return content;
};
const clear_content = () => {
content = '';
files.splice(0, files.length);
};
export { files, get_content, clear_content };
</script>
<script lang="ts">
import { Textarea } from '$lib/components/ui/textarea';
import type { KeyboardEventHandler } from 'svelte/elements';
import MingcutePicFill from '~icons/mingcute/pic-fill';
let window_size = $state(0);
let value = $state('');
const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => { const key_scrap: KeyboardEventHandler<HTMLTextAreaElement> = (event: KeyboardEvent) => {
let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement; let ct: HTMLTextAreaElement = event.currentTarget as HTMLTextAreaElement;
if (event.key == 'Tab') { if (event.key == 'Tab') {
@ -20,73 +37,28 @@
ct.selectionStart = ct.selectionEnd = start + 1; ct.selectionStart = ct.selectionEnd = start + 1;
} }
}; };
// marked
const marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
</script> </script>
<svelte:window bind:innerWidth={window_size} /> <div class="flex h-full w-full flex-col justify-items-center gap-3 pt-[3%]">
<div class="{height} w-full"> <Textarea
<div class="h-[4%] w-full"></div> class="mx-auto h-[80%] w-full resize-none bg-background text-base"
{#if window_size < 360} onkeydowncapture={key_scrap}
<div> placeholder="寫一些東西..."
<Textarea class="h-[96%] w-full resize-none" /> bind:value={content}
/>
<hr />
<div class="w-full">
<input
id="enclosed"
type="file"
accept=".gif,.png,.jpeg,.jpg,.webm,.heic,.heif,.mp4,.webp"
multiple
class="hidden"
bind:files={dyn_files}
/>
<label for="enclosed" class="mx-auto w-fit"><MingcutePicFill class="text-xl" /> </label>
</div> </div>
{:else}
<div class="grid h-[96%] w-full grid-cols-2 gap-5">
<textarea
tabindex="0"
class="resize-none rounded-lg bg-background p-1 text-foreground focus:outline-none focus:ring focus:ring-primary-foreground"
bind:value
onkeydown={key_scrap}
></textarea>
<div class="inner_card">
{@html marked.parse(value).toString()}
</div>
</div>
{/if}
</div> </div>
<style> <style>
.inner_card :global {
max-width: 100%;
overflow-y: scroll;
a {
@apply text-cyan-500;
}
p:has(code) {
overflow-x: scroll;
padding: 10px 0px;
code {
/* @apply bg-amber-600; */
text-wrap: nowrap;
}
}
p {
word-break: keep-all;
overflow-wrap: break-word;
}
pre {
/* @apply bg-amber-600; */
overflow-x: scroll;
}
h1 {
font-size: 1.3rem;
}
h2 {
font-size: 1.2rem;
}
h3 {
font-size: 1.1rem;
}
}
</style> </style>

View File

@ -1,22 +1,57 @@
<script lang="ts"> <script lang="ts">
let { content }: { content: string } = $props(); let { content }: { content: string } = $props();
import { marked } from 'marked';
import * as Dialog from '$lib/components/ui/dialog'; import * as Dialog from '$lib/components/ui/dialog';
import { Marked } from 'marked';
import DOMPurify from 'isomorphic-dompurify';
import { markedHighlight } from 'marked-highlight';
import hljs from 'highlight.js';
//TODO Need sanitizer //TODO Need sanitizer
const marked = new Marked(
markedHighlight({
emptyLangClass: 'hljs',
langPrefix: 'hljs language-',
highlight(code, lang, info) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
marked.use({
hooks: {
postprocess: (text) => {
return DOMPurify.sanitize(text);
}
}
});
let parsed = marked.parse(content, {
async: true,
gfm: true,
pedantic: false
});
</script> </script>
<div <Dialog.Root>
class="card_eff flex max-h-80 max-w-64 flex-col overflow-y-hidden bg-amber-400 px-5 font-bold text-slate-900" <Dialog.Trigger
class="max-h-80 max-w-64 overflow-y-hidden bg-amber-400 text-slate-900 drop-shadow-[5px_10px_#f38f0f]"
> >
<div class="inner_card"> <div class="inner_card">
{@html marked(content)} {#await parsed}
</div> loading
{:then item}
{@html item}
{/await}
</div> </div>
</Dialog.Trigger>
<Dialog.Content>
{#await parsed}
loading
{:then item}
{@html item}
{/await}
</Dialog.Content>
</Dialog.Root>
<style> <style>
.card_eff {
filter: drop-shadow(5px 10px #f38f0f);
}
.inner_card :global { .inner_card :global {
a { a {
@apply text-cyan-500; @apply text-cyan-500;
@ -25,5 +60,8 @@
@apply bg-amber-600; @apply bg-amber-600;
} }
mask-image: linear-gradient(0deg, transparent, white 50%); mask-image: linear-gradient(0deg, transparent, white 50%);
height: 100%;
padding: 0px 5%;
text-align: left;
} }
</style> </style>

View File

@ -21,6 +21,7 @@
<div <div
class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' + class={'fixed left-0 top-0 z-20 h-dvh w-dvw ' +
(overlay ? 'overlay_blur grid place-items-center' : 'hidden')} (overlay ? 'overlay_blur grid place-items-center' : 'hidden')}
role="dialog"
> >
{@render children()} {@render children()}
</div> </div>

Binary file not shown.

BIN
static/PingFang Bold.woff2 Normal file

Binary file not shown.

BIN
static/PingFang Heavy.woff2 Normal file

Binary file not shown.

BIN
static/PingFang Light.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,5 +3,13 @@ import { defineConfig } from 'vite';
import Icons from 'unplugin-icons/vite' import Icons from 'unplugin-icons/vite'
export default defineConfig({ export default defineConfig({
plugins: [sveltekit(), Icons({ compiler: 'svelte' })] plugins: [sveltekit(), Icons({ compiler: 'svelte' })],
server: {
proxy: {
'/article': {
target: "http://10.16.20.17:5000",
changeOrigin: true,
}
}
}
}); });