diff --git a/src/routes/heroes/+page.svelte b/src/routes/heroes/+page.svelte index 046b20450..503874a7f 100644 --- a/src/routes/heroes/+page.svelte +++ b/src/routes/heroes/+page.svelte @@ -471,22 +471,10 @@ .footer-wrapper { overflow: hidden; - > img { - top: -100px; - inline-size: 1700px; - max-inline-size: none; - max-block-size: none; - } - @media (max-width: 1024px) { .aw-hero { padding-block-start: 5rem; } - - > img { - top: -300px; - left: -400px; - } } .aw-hero { diff --git a/src/routes/threads/(assets)/bg-green.svg b/src/routes/threads/(assets)/bg-green.svg new file mode 100644 index 000000000..7fe816488 --- /dev/null +++ b/src/routes/threads/(assets)/bg-green.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/routes/threads/(assets)/bg-red.svg b/src/routes/threads/(assets)/bg-red.svg new file mode 100644 index 000000000..53593fa44 --- /dev/null +++ b/src/routes/threads/(assets)/bg-red.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/routes/threads/(assets)/empty-state.png b/src/routes/threads/(assets)/empty-state.png new file mode 100644 index 000000000..5733a1b47 Binary files /dev/null and b/src/routes/threads/(assets)/empty-state.png differ diff --git a/src/routes/threads/+layout.ts b/src/routes/threads/+layout.ts new file mode 100644 index 000000000..d43d0cd2a --- /dev/null +++ b/src/routes/threads/+layout.ts @@ -0,0 +1 @@ +export const prerender = false; diff --git a/src/routes/threads/+page.svelte b/src/routes/threads/+page.svelte new file mode 100644 index 000000000..b470829d1 --- /dev/null +++ b/src/routes/threads/+page.svelte @@ -0,0 +1,245 @@ + + + + + {title} + + + + + + + + + + + + + + + + + + + + + + + + + Threads + + + + + + + + {#each tags as tag} + + toggleTag(tag)} + > + {tag} + + + {/each} + + + + + + + + + + + {#if threads.length} + + Found {query.length ? threads.length : '600+'} results. + + {/if} + + + {#each threads as thread (thread.$id)} + + {:else} + + + No support threads found + { + query = ''; + handleSearch(''); + }}>Clear search + + {/each} + + + + + + + + + + diff --git a/src/routes/threads/+page.ts b/src/routes/threads/+page.ts new file mode 100644 index 000000000..1a8c1ee6d --- /dev/null +++ b/src/routes/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/threads/PreFooter.svelte b/src/routes/threads/PreFooter.svelte new file mode 100644 index 000000000..b128455d6 --- /dev/null +++ b/src/routes/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/threads/TagsDropdown.svelte b/src/routes/threads/TagsDropdown.svelte new file mode 100644 index 000000000..44a3f1d9a --- /dev/null +++ b/src/routes/threads/TagsDropdown.svelte @@ -0,0 +1,113 @@ + + + + + More + + + + {#if open} + + + {#each tags as tag} + {@const checked = selectedTags?.includes(tag)} + + { + e.preventDefault(); + toggleTag(tag); + }} + > + + {#if checked} + + {/if} + + {tag} + + + {/each} + + + {/if} + + + diff --git a/src/routes/threads/ThreadCard.svelte b/src/routes/threads/ThreadCard.svelte new file mode 100644 index 000000000..f6972e36f --- /dev/null +++ b/src/routes/threads/ThreadCard.svelte @@ -0,0 +1,68 @@ + + +{#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/threads/[id]/+page.server.ts b/src/routes/threads/[id]/+page.server.ts new file mode 100644 index 000000000..1985134c2 --- /dev/null +++ b/src/routes/threads/[id]/+page.server.ts @@ -0,0 +1,22 @@ +import { error } from '@sveltejs/kit'; +import { getRelatedThreads, getThread, getThreadMessages } 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); + + return { + ...thread, + related, + messages, + }; + } catch (e) { + throw error(404, 'Thread not found'); + } +}; diff --git a/src/routes/threads/[id]/+page.svelte b/src/routes/threads/[id]/+page.svelte new file mode 100644 index 000000000..97b76d39b --- /dev/null +++ b/src/routes/threads/[id]/+page.svelte @@ -0,0 +1,254 @@ + + + + + {title} + + + + + + + + + + + + + + + + Back + + {data.title} + + + + {data.vote_count} + + {#each data.tags ?? [] as tag} + + {tag} + + {/each} + + + + + + View on Discord + + + + + + + {#each data.messages ?? [] as message, i} + {@const isFirst = i === 0} + + {#if isFirst} + + + TL;DR + + {data.tldr} + + {/if} + + {/each} + + Reply + + Reply to this thread by joining our Discord + + + + Reply on Discord + + + + + {#if data.related.length} + Recommended threads + {/if} + + {#each data.related as thread} + + + + + {thread.title.length > 40 + ? thread.title.slice(0, 40) + '...' + : thread.title} + + + + {thread.content.length > 160 + ? thread.content.slice(0, 160) + '...' + : thread.content} + + + + {/each} + + + + + + + + + + + + + diff --git a/src/routes/threads/[id]/CodeRenderer.svelte b/src/routes/threads/[id]/CodeRenderer.svelte new file mode 100644 index 000000000..065285daf --- /dev/null +++ b/src/routes/threads/[id]/CodeRenderer.svelte @@ -0,0 +1,120 @@ + + +{#if insideMultiCode} + {#if $selected === language} + + {@html result} + {/if} +{:else} + + + + {#if platformMap[language]} + + {platformMap[language]} + + {/if} + + + + + + + + + + {copyText} + + + + + + + + + {@html result} + + +{/if} + + diff --git a/src/routes/threads/[id]/LinkRenderer.svelte b/src/routes/threads/[id]/LinkRenderer.svelte new file mode 100644 index 000000000..db46f1e43 --- /dev/null +++ b/src/routes/threads/[id]/LinkRenderer.svelte @@ -0,0 +1,11 @@ + + +{text} diff --git a/src/routes/threads/[id]/MessageCard.svelte b/src/routes/threads/[id]/MessageCard.svelte new file mode 100644 index 000000000..75b5a190a --- /dev/null +++ b/src/routes/threads/[id]/MessageCard.svelte @@ -0,0 +1,87 @@ + + + + + + + + + {message.author} + + + {formatTimestamp(message.timestamp)} + + + + + + + + + diff --git a/src/routes/threads/helpers.ts b/src/routes/threads/helpers.ts new file mode 100644 index 000000000..079c49418 --- /dev/null +++ b/src/routes/threads/helpers.ts @@ -0,0 +1,115 @@ +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) { + let query = [ + q ? Query.search('search_meta', q) : undefined + ]; + + tags = tags?.filter(Boolean).map((tag) => tag.toLowerCase()) ?? []; + + if (tags.length > 0) { + query = [...query, Query.search('tags', tags.join(','))]; + } + + const data = await databases.listDocuments( + PUBLIC_APPWRITE_DB_MAIN_ID, + PUBLIC_APPWRITE_COL_THREADS_ID, + query.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, limit: number = 3) { + const tags = thread.tags?.filter(Boolean) ?? []; + const relatedThreads = await getThreads({ q: null, tags, allTags: false }); + + return relatedThreads.filter(({ $id }) => $id !== thread.$id).slice(0, limit); +} + +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[]).sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); +} \ No newline at end of file diff --git a/src/routes/threads/types.ts b/src/routes/threads/types.ts new file mode 100644 index 000000000..98e9d6257 --- /dev/null +++ b/src/routes/threads/types.ts @@ -0,0 +1,38 @@ +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; + 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 000000000..0c6787c9a --- /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 fb3d136d9..691f2fd30 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 1df9e90f2..7fab354b8 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 12dc5ecf0..f5be36d82 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 e2c3d5b90..1148fe3a2 100644 --- a/src/scss/6-elements/_link.scss +++ b/src/scss/6-elements/_link.scss @@ -31,5 +31,9 @@ &.is-inline { text-decoration: underline; } + + &.is-secondary { + --p-link-color-text-default: var(--aw-color-secondary); + } } diff --git a/src/scss/_10-utilities.scss b/src/scss/_10-utilities.scss index 07a746a55..74d47857c 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 8316dd0e4..472fd466a 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -59,4 +59,4 @@ const config = { } } }; -export default config; +export default config; \ No newline at end of file diff --git a/teams-labels.png b/teams-labels.png deleted file mode 100644 index 6dfc07152..000000000 Binary files a/teams-labels.png and /dev/null differ