feat: setup drizzle (#594)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<script>
|
||||
import PageSignupCTA from '$lib/PageSignupCTA/index.svelte';
|
||||
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
|
||||
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blog | ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<PageHeader className="flex flex-col items-center justify-center text-center">
|
||||
<h1
|
||||
class="mx-auto text-4xl md:text-7xl lg:text-6xl font-bold text-slate-900 flex flex-col items-center"
|
||||
>
|
||||
<span>What's new on</span>
|
||||
<span class="text-blue-700 relative">ClassroomIO?</span>
|
||||
</h1>
|
||||
<p class="w-[90%] md:w-[60%] text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7">
|
||||
Get the latest news from ClassroomIO, including product updates, team announcements and more!
|
||||
</p>
|
||||
</PageHeader>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<ul class="flex flex-col items-start justify-start gap-2 w-[90%] md:w-[50%]">
|
||||
{#each data.posts as post}
|
||||
<li class="py-10 w-full border-b border-gray-200">
|
||||
<BlogListItem {post} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<PageSignupCTA
|
||||
header="Ready To Scale Your Training Efforts?"
|
||||
subText="It's free to sign up and start getting value out of the product."
|
||||
btnLabel="Sign me up"
|
||||
link="/signup"
|
||||
demo={false}
|
||||
/>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { BlogPost } from '$lib/types';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ fetch }) {
|
||||
const response = await fetch('api/blog');
|
||||
const posts: BlogPost[] = await response.json();
|
||||
return { posts };
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { ChevronLeft } from 'carbon-icons-svelte';
|
||||
import Chip from '$lib/Chip/Chip.svelte';
|
||||
import { formatDate } from '$lib/utils/formatDate';
|
||||
import BlogListItem from '$lib/Blog/BlogListItem.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<div class=" mt-[10%] md:mt-16">
|
||||
{#if data}
|
||||
<article class="py-16">
|
||||
<!-- Title -->
|
||||
<hgroup class="flex flex-col items-center justify-center text-center w-full">
|
||||
<p class="text-sm text-gray-500">{formatDate(data.meta.date)}</p>
|
||||
<p class="font-bold text-3xl py-2 text-center md:w-[60%]">{@html data.meta.title}</p>
|
||||
</hgroup>
|
||||
<main class="mx-auto max-w-screen-md px-4 lg:px-8">
|
||||
<div class="flex items-center justify-start gap-4 my-2 border-y border-gray-200 py-4">
|
||||
<img loading="lazy" src={data.meta.avatar} alt="avatar" class="w-10 h-10 rounded-full" />
|
||||
<span>
|
||||
<p class="font-semibold">{data.meta.author}</p>
|
||||
<p class="text-gray-500">{data.meta.role}</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Post -->
|
||||
<div class="prose pt-2 pb-4 border-b-2 border-gray-200">
|
||||
<svelte:component this={data.content} />
|
||||
<!-- Tags -->
|
||||
<div class="flex gap-2 py-4">
|
||||
{#each data.meta.tags as tag}
|
||||
<Chip label={tag} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data.relatedPosts.length > 0}
|
||||
<section class="mt-5">
|
||||
<p class="text-xl font-semibold">Related Posts</p>
|
||||
<ul class="flex items-start justify-start gap-3 overflow-x-scroll">
|
||||
{#each data.relatedPosts as post}
|
||||
<li class="py-10 min-w-[80%] sm:w-80 sm:min-w-0">
|
||||
<BlogListItem {post} isRecommended />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/blog"
|
||||
class="flex font-medium items-center gap-2 text-slate-700 text-lg underline my-10"
|
||||
>
|
||||
<ChevronLeft class=" w-6 h-6 font-medium" /> Back to all posts
|
||||
</a>
|
||||
</main>
|
||||
</article>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
article {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
:global(.prose a) {
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:global(.prose .gallery img) {
|
||||
max-height: 300px;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
:global(.prose .gallery) {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { BlogPost, RawBlogPost } from '$lib/types';
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load({ params, fetch }) {
|
||||
try {
|
||||
const response = await fetch('/api/blog');
|
||||
const posts: BlogPost[] = await response.json();
|
||||
|
||||
const blog: RawBlogPost = await import(`../../../blog/${params.slug}.md`);
|
||||
|
||||
return {
|
||||
content: blog.default,
|
||||
meta: blog.metadata,
|
||||
slug: params.slug,
|
||||
pageMetaTags: getPageMetaTags(blog),
|
||||
relatedPosts: getRelatedPosts(posts, blog.metadata.tags, params.slug)
|
||||
};
|
||||
} catch (e) {
|
||||
error(404, `Could not find ${params.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getRelatedPosts(posts: BlogPost[], tags: string[], slug: string) {
|
||||
const related = posts
|
||||
.filter((post) => post.tags.some((tag) => tags.includes(tag)) && post.slug !== slug)
|
||||
.map((post) => ({
|
||||
...post,
|
||||
rank: post.tags.filter((tag) => tags.includes(tag)).length
|
||||
}))
|
||||
.sort((a, b) => b.rank - a.rank);
|
||||
|
||||
const topPosts = related.filter((post) => post.rank > 1);
|
||||
|
||||
if (topPosts.length > 2) {
|
||||
return topPosts.sort(() => Math.random() - 0.5).slice(0, 2);
|
||||
}
|
||||
|
||||
return related.slice(0, 2);
|
||||
}
|
||||
|
||||
function getPageMetaTags(blog: RawBlogPost): MetaTagsProps {
|
||||
return Object.freeze({
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
openGraph: {
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
images: [
|
||||
{
|
||||
url: blog.metadata.imageUrl,
|
||||
alt: blog.metadata.title,
|
||||
width: 280,
|
||||
height: 200,
|
||||
secureUrl: blog.metadata.imageUrl,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
handle: '@classroomio',
|
||||
site: '@classroomio',
|
||||
cardType: 'summary_large_image' as const,
|
||||
title: blog.metadata.title,
|
||||
description: blog.metadata.description,
|
||||
image: blog.metadata.imageUrl,
|
||||
imageAlt: `${blog.metadata.title} OG`
|
||||
}
|
||||
}) satisfies MetaTagsProps;
|
||||
}
|
||||
Reference in New Issue
Block a user