diff --git a/src/routes/api/test/+server.ts b/src/routes/api/test/+server.ts
new file mode 100644
index 0000000000..0c5d113eef
--- /dev/null
+++ b/src/routes/api/test/+server.ts
@@ -0,0 +1,6 @@
+import { json } from '@sveltejs/kit';
+
+export async function POST({ request }) {
+ const { a, b } = await request.json();
+ return json(a + b);
+}
diff --git a/src/routes/api/tldr/+server.ts b/src/routes/api/tldr/+server.ts
new file mode 100644
index 0000000000..88ff7d5cf8
--- /dev/null
+++ b/src/routes/api/tldr/+server.ts
@@ -0,0 +1,32 @@
+import OpenAI from 'openai';
+import { OpenAIStream, StreamingTextResponse } from 'ai';
+import { OPENAI_API_KEY } from '$env/static/private';
+
+// Create an OpenAI API client (that's edge friendly!)
+const openai = new OpenAI({
+ apiKey: OPENAI_API_KEY
+});
+
+// Set the runtime to edge for best performance
+export const config = {
+ runtime: 'edge'
+};
+
+export async function POST({ request }) {
+ const { prompt } = await request.json();
+
+ // Ask OpenAI for a streaming completion given the prompt
+ const response = await openai.completions.create({
+ model: 'text-davinci-003',
+ stream: true,
+ temperature: 0.6,
+ max_tokens: 300,
+ prompt: `Create a TL;DR for the following support thread. If there's a solution, be sure to include it.
+ Do not include the term "TL;DR" in your response.: ${prompt}`
+ });
+
+ // Convert the response into a friendly text-stream
+ const stream = OpenAIStream(response);
+ // Respond with the stream
+ return new StreamingTextResponse(stream);
+}
diff --git a/src/routes/discord/+page.server.js b/src/routes/discord/+page.server.js
deleted file mode 100644
index 58cfac3705..0000000000
--- a/src/routes/discord/+page.server.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from '@sveltejs/kit';
-
-export function load() {
- throw redirect(301, 'https://discord.gg/GSeTUeA');
-}
\ No newline at end of file
diff --git a/src/routes/support-threads/(assets)/bg-green.svg b/src/routes/support-threads/(assets)/bg-green.svg
new file mode 100644
index 0000000000..7fe8164883
--- /dev/null
+++ b/src/routes/support-threads/(assets)/bg-green.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/src/routes/support-threads/(assets)/bg-red.svg b/src/routes/support-threads/(assets)/bg-red.svg
new file mode 100644
index 0000000000..53593fa448
--- /dev/null
+++ b/src/routes/support-threads/(assets)/bg-red.svg
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/src/routes/support-threads/(assets)/empty-state.png b/src/routes/support-threads/(assets)/empty-state.png
new file mode 100644
index 0000000000..5733a1b472
Binary files /dev/null and b/src/routes/support-threads/(assets)/empty-state.png differ
diff --git a/src/routes/support-threads/+layout.ts b/src/routes/support-threads/+layout.ts
new file mode 100644
index 0000000000..d43d0cd2a5
--- /dev/null
+++ b/src/routes/support-threads/+layout.ts
@@ -0,0 +1 @@
+export const prerender = false;
diff --git a/src/routes/support-threads/+page.svelte b/src/routes/support-threads/+page.svelte
new file mode 100644
index 0000000000..a62d9c2ca7
--- /dev/null
+++ b/src/routes/support-threads/+page.svelte
@@ -0,0 +1,255 @@
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each tags as tag}
+ -
+
+
+ {/each}
+ -
+
+
+
+
+
+
+
+
+
+ {#if threads.length}
+
+ Found {query.length ? threads.length : '600+'} results.
+
+ {/if}
+
+
+ {#each threads as thread (thread.$id)}
+
+ {:else}
+
+
+ No support threads found
+
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/support-threads/+page.ts b/src/routes/support-threads/+page.ts
new file mode 100644
index 0000000000..1a8c1ee6d8
--- /dev/null
+++ b/src/routes/support-threads/+page.ts
@@ -0,0 +1,13 @@
+import { getThreads } from './helpers.js';
+
+export async function load({ url }) {
+ const tagsParam = url.searchParams.get('tags');
+
+ return {
+ threads: await getThreads({
+ q: url.searchParams.get('q'),
+ tags: tagsParam ? tagsParam.split(',') : undefined,
+ allTags: true
+ })
+ };
+}
diff --git a/src/routes/support-threads/PreFooter.svelte b/src/routes/support-threads/PreFooter.svelte
new file mode 100644
index 0000000000..89d2dd1771
--- /dev/null
+++ b/src/routes/support-threads/PreFooter.svelte
@@ -0,0 +1,72 @@
+
+
+
+
Need Support?
+
+
+
Join our Discord
+
+ Get community support by joining our Discord server
+
+
+
+ Join Discord
+
+
+
+
Get premium support
+
+ Become a pro user and get email support from our team
+
+
+ Learn more
+
+
+
+
+
+
+
diff --git a/src/routes/support-threads/TagsDropdown.svelte b/src/routes/support-threads/TagsDropdown.svelte
new file mode 100644
index 0000000000..eac486df34
--- /dev/null
+++ b/src/routes/support-threads/TagsDropdown.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+
+ {#if open}
+
+ {/if}
+
+
+
diff --git a/src/routes/support-threads/ThreadCard.svelte b/src/routes/support-threads/ThreadCard.svelte
new file mode 100644
index 0000000000..ce67edbaa5
--- /dev/null
+++ b/src/routes/support-threads/ThreadCard.svelte
@@ -0,0 +1,64 @@
+
+
+{#key highlightTerms}
+
+
+
+ {thread.title}
+
+
+
+
+
+ {thread.content.length > 200 ? thread.content.slice(0, 200) + '...' : thread.content}
+
+
+
+
+ {#each thread.tags ?? [] as tag}
+ -
+
{tag}
+
+ {/each}
+
+
+
+
+ {thread.message_count}
+
+
+
+{/key}
+
+
diff --git a/src/routes/support-threads/[id]/+page.server.ts b/src/routes/support-threads/[id]/+page.server.ts
new file mode 100644
index 0000000000..27a6d45f1e
--- /dev/null
+++ b/src/routes/support-threads/[id]/+page.server.ts
@@ -0,0 +1,30 @@
+import { random } from '$lib/utils/random.js';
+import { error } from '@sveltejs/kit';
+import { getRelatedThreads, getThread, getThreadMessages, getThreadTldr } from '../helpers.js';
+
+export const prerender = false;
+
+export const load = async ({ params }) => {
+ const id = params.id;
+
+ try {
+ const thread = await getThread(id);
+ const related = await getRelatedThreads(thread);
+ const messages = await getThreadMessages(id);
+
+ const upvotes = random(1, 60);
+
+ return {
+ ...thread,
+ related,
+ upvotes,
+ messages,
+ streamed: {
+ tldr: getThreadTldr(thread)
+ }
+ };
+ } catch (e) {
+ console.log(e);
+ throw error(404, 'Thread not found');
+ }
+};
diff --git a/src/routes/support-threads/[id]/+page.svelte b/src/routes/support-threads/[id]/+page.svelte
new file mode 100644
index 0000000000..0679a64f4c
--- /dev/null
+++ b/src/routes/support-threads/[id]/+page.svelte
@@ -0,0 +1,283 @@
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each data.messages ?? [] as message, i}
+ {@const isFirst = i === 0}
+
+ {#if isFirst}
+
+
+ TL;DR
+
+ {#if data.tldr}
+ {data.tldr}
+ {:else}
+ {#await data.streamed.tldr}
+
+ {#each { length: 3 } as _, i}
+
+ {/each}
+
+ {:then res}
+ {res}
+ {/await}
+ {/if}
+
+ {/if}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/support-threads/[id]/CodeRenderer.svelte b/src/routes/support-threads/[id]/CodeRenderer.svelte
new file mode 100644
index 0000000000..c823e98d1b
--- /dev/null
+++ b/src/routes/support-threads/[id]/CodeRenderer.svelte
@@ -0,0 +1,119 @@
+
+
+{#if insideMultiCode}
+ {#if $selected === language}
+
+ {@html result}
+ {/if}
+{:else}
+
+
+
+
+ {@html result}
+
+
+{/if}
+
+
diff --git a/src/routes/support-threads/[id]/LinkRenderer.svelte b/src/routes/support-threads/[id]/LinkRenderer.svelte
new file mode 100644
index 0000000000..db46f1e437
--- /dev/null
+++ b/src/routes/support-threads/[id]/LinkRenderer.svelte
@@ -0,0 +1,11 @@
+
+
+
{text}
diff --git a/src/routes/support-threads/[id]/MessageCard.svelte b/src/routes/support-threads/[id]/MessageCard.svelte
new file mode 100644
index 0000000000..75b5a190a3
--- /dev/null
+++ b/src/routes/support-threads/[id]/MessageCard.svelte
@@ -0,0 +1,87 @@
+
+
+
+
+
diff --git a/src/routes/support-threads/helpers.ts b/src/routes/support-threads/helpers.ts
new file mode 100644
index 0000000000..9b1ad66bb4
--- /dev/null
+++ b/src/routes/support-threads/helpers.ts
@@ -0,0 +1,123 @@
+import {
+ PUBLIC_APPWRITE_COL_MESSAGES_ID,
+ PUBLIC_APPWRITE_COL_THREADS_ID,
+ PUBLIC_APPWRITE_DB_MAIN_ID,
+ PUBLIC_APPWRITE_FN_TLDR_ID
+} from '$env/static/public';
+import { databases, functions } from '$lib/appwrite';
+import { Query } from 'appwrite';
+import type { DiscordMessage, DiscordThread } from './types';
+
+type Ranked
= {
+ data: T;
+ rank: number; // Percentage of query words found, from 0 to 1
+};
+
+type FilterThreadsArgs = {
+ threads: DiscordThread[];
+ q?: string | null;
+ tags?: string[];
+ allTags?: boolean;
+};
+
+export function filterThreads({ q, threads: threadDocs, tags, allTags }: FilterThreadsArgs) {
+ const threads = tags
+ ? threadDocs.filter((thread) => {
+ const lowercaseTags = thread.tags?.map((tag) => tag.toLowerCase());
+ if (allTags) {
+ return tags?.every((tag) => lowercaseTags?.includes(tag.toLowerCase()));
+ } else {
+ return tags?.some((tag) => lowercaseTags?.includes(tag.toLowerCase()));
+ }
+ })
+ : threadDocs;
+
+ if (!q) return threads;
+
+ const queryWords = q.toLowerCase().split(/\s+/);
+ const rankPerWord = 1 / queryWords.length;
+ const res: Ranked[] = [];
+
+ threads.forEach((item) => {
+ const foundWords = new Set();
+
+ Object.values(item).forEach((value) => {
+ const stringified = JSON.stringify(value).toLowerCase();
+
+ queryWords.forEach((word) => {
+ if (stringified.includes(word)) {
+ foundWords.add(word);
+ }
+ });
+ });
+
+ const rank = foundWords.size * rankPerWord;
+
+ if (rank > 0) {
+ res.push({
+ data: item,
+ rank
+ });
+ }
+ });
+
+ return res.sort((a, b) => b.rank - a.rank).map(({ data }) => data);
+}
+
+type GetThreadsArgs = Omit;
+
+export async function getThreads({ q, tags, allTags }: GetThreadsArgs) {
+ tags = tags?.filter(Boolean).map((tag) => tag.toLowerCase()) ?? [];
+
+ const data = await databases.listDocuments(
+ PUBLIC_APPWRITE_DB_MAIN_ID,
+ PUBLIC_APPWRITE_COL_THREADS_ID,
+ [
+ q ? Query.search('search_meta', q) : undefined
+ // tags ? Query.equal('tags', tags) : undefined
+ ].filter(Boolean) as string[]
+ );
+
+ const threadDocs = data.documents as unknown as DiscordThread[];
+ return filterThreads({ threads: threadDocs, q, tags, allTags });
+}
+
+export async function getThread($id: string) {
+ return (await databases.getDocument(
+ PUBLIC_APPWRITE_DB_MAIN_ID,
+ PUBLIC_APPWRITE_COL_THREADS_ID,
+ $id
+ )) as unknown as DiscordThread;
+}
+
+export async function getRelatedThreads(thread: DiscordThread) {
+ const tags = thread.tags?.filter(Boolean) ?? [];
+ const relatedThreads = await getThreads({ q: null, tags, allTags: false });
+
+ return relatedThreads.filter(({ $id }) => $id !== thread.$id);
+}
+
+export async function getThreadMessages(threadId: string) {
+ const data = await databases.listDocuments(
+ PUBLIC_APPWRITE_DB_MAIN_ID,
+ PUBLIC_APPWRITE_COL_MESSAGES_ID,
+ [Query.equal('threadId', threadId)].filter(Boolean) as string[]
+ );
+
+ return data.documents as unknown as DiscordMessage[];
+}
+
+export async function getThreadTldr(thread: DiscordThread) {
+ if (thread.tldr) return thread.tldr;
+
+ const execution = await functions.createExecution(
+ PUBLIC_APPWRITE_FN_TLDR_ID,
+ JSON.stringify({ thread: thread.$id }),
+ false,
+ '/',
+ 'POST'
+ );
+ const { tldr } = JSON.parse(execution.responseBody);
+
+ return tldr;
+}
diff --git a/src/routes/support-threads/types.ts b/src/routes/support-threads/types.ts
new file mode 100644
index 0000000000..29b882af37
--- /dev/null
+++ b/src/routes/support-threads/types.ts
@@ -0,0 +1,39 @@
+import type { Models } from 'appwrite';
+
+export type MockThread = {
+ id: string;
+ username?: string;
+ title: string;
+ text: string;
+ replies: MockMessage[];
+};
+
+export interface DiscordMessage extends Pick {
+ threadId: string;
+ author: string;
+ author_avatar: string;
+ message: string;
+ role?: string;
+ /* `UTC` timestamp */
+ timestamp: string;
+}
+
+export interface DiscordThread extends Pick {
+ discord_id: string;
+ author: string;
+ tags?: string[];
+ author_avatar: string;
+ seo_description?: string;
+ content: string;
+ title: string;
+ search_meta?: string;
+ // messages?: DiscordMessage[];
+ tldr: string;
+ vote_count: number;
+ message_count: number;
+}
+
+export type MockMessage = {
+ username?: string;
+ text: string;
+};
diff --git a/src/scss/6-elements/_btn-tag.scss b/src/scss/6-elements/_btn-tag.scss
new file mode 100644
index 0000000000..0c6787c9a9
--- /dev/null
+++ b/src/scss/6-elements/_btn-tag.scss
@@ -0,0 +1,38 @@
+@use '../abstract' as *;
+
+.#{$p}-btn-tag {
+ --p-tag-text-color: var(--aw-color-primary);
+ --p-tag-bg-color: var(--aw-color-greyscale-100);
+ --p-tag-border-color: var(--p-tag-bg-color);
+
+
+ color: hsl(var(--p-tag-text-color));
+ background-color: hsl(var(--p-tag-bg-color));
+ border: 1px solid hsl(var(--p-tag-border-color));
+
+ padding-block: pxToRem(4);
+ padding-inline: pxToRem(8);
+ border-radius: pxToRem(12);
+ font-size: var(--aw-font-size-micro);
+ line-height: var(--aw-line-height-tiny);
+
+ #{$theme-dark} & {
+ --p-tag-bg-color: var(--aw-color-greyscale-750);
+
+ &:where(:hover) {
+ --p-tag-bg-color: var(--aw-color-greyscale-700);
+ }
+
+ &:where(:active) {
+ --p-tag-bg-color: var(--aw-color-greyscale-800);
+ }
+
+ &:where(.is-selected) {
+ --p-tag-border-color: var(--aw-color-white);
+ }
+
+ &:where(:disabled) {
+ --p-tag-bg-color: var(--aw-color-greyscale-800);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/scss/6-elements/_container.scss b/src/scss/6-elements/_container.scss
index fb3d136d92..691f2fd306 100644
--- a/src/scss/6-elements/_container.scss
+++ b/src/scss/6-elements/_container.scss
@@ -9,6 +9,7 @@
@media #{$break1} { padding-inline: pxToRem(20); }
}
+
.#{$p}-main-section {
> * { padding-block:pxToRem(24); }
>:first-child { padding-block-start:0; }
diff --git a/src/scss/6-elements/_icon-button.scss b/src/scss/6-elements/_icon-button.scss
index 1df9e90f20..7fab354b85 100644
--- a/src/scss/6-elements/_icon-button.scss
+++ b/src/scss/6-elements/_icon-button.scss
@@ -1,7 +1,8 @@
@use '../abstract' as *;
.#{$p}-icon-button {
- display: block;
+ display: flex;
+ gap: pxToRem(4);
position: relative;
block-size:pxToRem(28);
inline-size:pxToRem(28);
@@ -10,10 +11,17 @@
border-radius: pxToRem(8);
> [class*='icon'] {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
+ position: relative;
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+ &.is-more-content {
+ inline-size:fit-content; padding:pxToRem(4); line-height:pxToRem(18);
+ > [class*='icon'] { inline-size:pxToRem(16); }
}
diff --git a/src/scss/6-elements/_index.scss b/src/scss/6-elements/_index.scss
index 12dc5ecf03..f5be36d828 100644
--- a/src/scss/6-elements/_index.scss
+++ b/src/scss/6-elements/_index.scss
@@ -12,6 +12,7 @@
@forward "badges";
@forward "numeric-badge";
@forward "tag";
+@forward "btn-tag";
@forward "inline-tag";
@forward "card";
@forward "lists";
diff --git a/src/scss/6-elements/_link.scss b/src/scss/6-elements/_link.scss
index e2c3d5b905..59f2b03e20 100644
--- a/src/scss/6-elements/_link.scss
+++ b/src/scss/6-elements/_link.scss
@@ -31,5 +31,11 @@
&.is-inline {
text-decoration: underline;
}
+
+ &.is-secondary {
+ --p-link-color-text-default: var(--aw-color-secondary);
+ // --p-link-color-text-hover: var(--aw-color-secondary) / 0.75;
+ // --p-link-color-text-active:var(--aw-color-secondary) / 0.5;
+ }
}
diff --git a/src/scss/_10-utilities.scss b/src/scss/_10-utilities.scss
index 07a746a55c..74d47857cc 100644
--- a/src/scss/_10-utilities.scss
+++ b/src/scss/_10-utilities.scss
@@ -47,6 +47,7 @@
.#{$p}-u-margin-inline-32-negative { margin-inline:pxToRem(-32); }
.#{$p}-u-margin-block-0 { margin-block:0; }
+.#{$p}-u-margin-block-80 { margin-block:pxToRem(80); }
.#{$p}-u-margin-block-start-40 { margin-block-start:pxToRem(40); }
.#{$p}-u-margin-block-start-40-mobile { @media #{$break1} {margin-block-start:pxToRem(40);} }
diff --git a/svelte.config.js b/svelte.config.js
index 8316dd0e45..02cc35da4b 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -6,6 +6,7 @@ import { markdoc } from 'svelte-markdoc-preprocess';
import sequence from 'svelte-sequential-preprocessor';
import staticAdapter from '@sveltejs/adapter-static';
import nodeAdapter from '@sveltejs/adapter-node';
+import vercelAdapter from '@sveltejs/adapter-vercel';
function absolute(path) {
return join(dirname(fileURLToPath(import.meta.url)), path);
@@ -13,7 +14,8 @@ function absolute(path) {
const isVercel = process.env.VERCEL === '1';
-const adapter = isVercel ? staticAdapter() : nodeAdapter();
+const adapter = isVercel ? vercelAdapter() : nodeAdapter();
+// const adapter = vercel
/** @type {import('@sveltejs/kit').Config}*/
const config = {
@@ -44,7 +46,8 @@ const config = {
adapter,
files: {
hooks: {
- server: isVercel ? undefined : './src/hooks/server.ts'
+ // server: isVercel ? undefined : './src/hooks/server.ts'
+ server: './src/hooks/server.ts'
}
},
alias: {