mirror of https://github.com/sveltejs/svelte
chore: prepare legacy site deploy (#13987)
This basically removes everything but the home page and docs/tutorial. More specifically: - add old tutorial to nav - strips out implementations of repl and blog, these links go to the current blog/repl instead - removes examples - adds deprecation banner - link to current sitepull/13988/head
parent
484588a923
commit
6d4c5cc870
@ -1,16 +0,0 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { get_examples_data } from '../src/lib/server/examples/index.js';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
|
||||
const examples_data = await get_examples_data(
|
||||
fileURLToPath(new URL('../../../documentation/examples', import.meta.url))
|
||||
);
|
||||
|
||||
try {
|
||||
await mkdir(new URL('../src/lib/generated/', import.meta.url), { recursive: true });
|
||||
} catch {}
|
||||
|
||||
writeFile(
|
||||
new URL('../src/lib/generated/examples-data.js', import.meta.url),
|
||||
`export default ${JSON.stringify(examples_data)}`
|
||||
);
|
@ -1,84 +0,0 @@
|
||||
// @ts-check
|
||||
import { extractFrontmatter } from '@sveltejs/site-kit/markdown';
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
import { render_content } from '../renderer.js';
|
||||
import { get_sections } from '../docs/index.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').BlogData} blog_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export async function get_processed_blog_post(blog_data, slug) {
|
||||
for (const post of blog_data) {
|
||||
if (post.slug === slug) {
|
||||
return {
|
||||
...post,
|
||||
content: await render_content(post.file, post.content)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const BLOG_NAME_REGEX = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
||||
|
||||
/** @returns {Promise<import('./types').BlogData>} */
|
||||
export async function get_blog_data(base = CONTENT_BASE_PATHS.BLOG) {
|
||||
const { readdir, readFile } = await import('node:fs/promises');
|
||||
|
||||
/** @type {import('./types').BlogData} */
|
||||
const blog_posts = [];
|
||||
|
||||
for (const file of (await readdir(base)).reverse()) {
|
||||
if (!BLOG_NAME_REGEX.test(file)) continue;
|
||||
|
||||
const { date, date_formatted, slug } = get_date_and_slug(file);
|
||||
const { metadata, body } = extractFrontmatter(await readFile(`${base}/${file}`, 'utf-8'));
|
||||
const authors = metadata.author.split(',').map((author) => author.trim());
|
||||
const authorUrls = metadata.authorURL.split(',').map((author) => author.trim());
|
||||
|
||||
blog_posts.push({
|
||||
date,
|
||||
date_formatted,
|
||||
content: body,
|
||||
description: metadata.description,
|
||||
draft: metadata.draft === 'true',
|
||||
slug,
|
||||
title: metadata.title,
|
||||
file,
|
||||
authors: authors.map((author, i) => ({
|
||||
name: author,
|
||||
url: authorUrls[i]
|
||||
})),
|
||||
sections: await get_sections(body)
|
||||
});
|
||||
}
|
||||
|
||||
return blog_posts;
|
||||
}
|
||||
|
||||
/** @param {import('./types').BlogData} blog_data */
|
||||
export function get_blog_list(blog_data) {
|
||||
return blog_data.map(({ slug, date, title, description, draft }) => ({
|
||||
slug,
|
||||
date,
|
||||
title,
|
||||
description,
|
||||
draft
|
||||
}));
|
||||
}
|
||||
|
||||
/** @param {string} filename */
|
||||
function get_date_and_slug(filename) {
|
||||
const match = BLOG_NAME_REGEX.exec(filename);
|
||||
if (!match) throw new Error(`Invalid filename for blog: '${filename}'`);
|
||||
|
||||
const [, date, slug] = match;
|
||||
const [y, m, d] = date.split('-');
|
||||
const date_formatted = `${months[+m - 1]} ${+d} ${y}`;
|
||||
|
||||
return { date, date_formatted, slug };
|
||||
}
|
||||
|
||||
const months = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
|
@ -1,27 +0,0 @@
|
||||
import type { Section } from '../docs/types';
|
||||
|
||||
export interface BlogPost {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
date_formatted: string;
|
||||
slug: string;
|
||||
file: string;
|
||||
authors: {
|
||||
name: string;
|
||||
url?: string;
|
||||
}[];
|
||||
draft: boolean;
|
||||
content: string;
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
export type BlogData = BlogPost[];
|
||||
|
||||
export interface BlogPostSummary {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
draft: boolean;
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
import { CONTENT_BASE_PATHS } from '../../../constants.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').ExamplesData} examples_data
|
||||
* @param {string} slug
|
||||
*/
|
||||
export function get_example(examples_data, slug) {
|
||||
for (const section of examples_data) {
|
||||
for (const example of section.examples) {
|
||||
if (example.slug === slug) {
|
||||
return example;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('./types').ExamplesData>}
|
||||
*/
|
||||
export async function get_examples_data(base = CONTENT_BASE_PATHS.EXAMPLES) {
|
||||
const { readdir, stat, readFile } = await import('node:fs/promises');
|
||||
|
||||
const examples = [];
|
||||
|
||||
for (const subdir of await readdir(base)) {
|
||||
/** @type {import('./types').ExamplesDatum} */
|
||||
const section = {
|
||||
title: '', // Initialise with empty
|
||||
slug: subdir.split('-').slice(1).join('-'),
|
||||
examples: []
|
||||
};
|
||||
|
||||
if (!((await stat(`${base}/${subdir}`)).isDirectory() || subdir.endsWith('meta.json')))
|
||||
continue;
|
||||
|
||||
if (!subdir.endsWith('meta.json'))
|
||||
section.title =
|
||||
JSON.parse(await readFile(`${base}/${subdir}/meta.json`, 'utf-8')).title ?? 'Embeds';
|
||||
|
||||
for (const section_dir of await readdir(`${base}/${subdir}`)) {
|
||||
const match = /\d{2}-(.+)/.exec(section_dir);
|
||||
if (!match) continue;
|
||||
|
||||
const slug = match[1];
|
||||
|
||||
const example_base_dir = `${base}/${subdir}/${section_dir}`;
|
||||
|
||||
// Get title for
|
||||
const example_title = JSON.parse(
|
||||
await readFile(`${example_base_dir}/meta.json`, 'utf-8')
|
||||
).title;
|
||||
|
||||
/**
|
||||
* @type {Array<{
|
||||
* name: string;
|
||||
* type: string;
|
||||
* content: string;
|
||||
* }>}
|
||||
*/
|
||||
const files = [];
|
||||
for (const file of (await readdir(example_base_dir)).filter(
|
||||
(file) => !file.endsWith('meta.json')
|
||||
)) {
|
||||
const type = file.split('.').at(-1);
|
||||
if (!type) {
|
||||
throw new Error(`Could not determine type from ${file}`);
|
||||
}
|
||||
files.push({
|
||||
name: file,
|
||||
type,
|
||||
content: await readFile(`${example_base_dir}/${file}`, 'utf-8')
|
||||
});
|
||||
}
|
||||
|
||||
section.examples.push({ title: example_title, slug, files });
|
||||
}
|
||||
|
||||
examples.push(section);
|
||||
}
|
||||
|
||||
return examples;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./types').ExamplesData} examples_data
|
||||
* @returns {import('./types').ExamplesList}
|
||||
*/
|
||||
export function get_examples_list(examples_data) {
|
||||
return examples_data.map((section) => ({
|
||||
title: section.title,
|
||||
examples: section.examples.map((example) => ({
|
||||
title: example.title,
|
||||
slug: example.slug
|
||||
}))
|
||||
}));
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
export interface ExamplesDatum {
|
||||
title: string;
|
||||
slug: string;
|
||||
examples: {
|
||||
title: string;
|
||||
slug: string;
|
||||
files: {
|
||||
content: string;
|
||||
type: string;
|
||||
name: string;
|
||||
}[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export type ExamplesData = ExamplesDatum[];
|
||||
|
||||
export interface Example {
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface ExampleSection {
|
||||
title: string;
|
||||
examples: Example[];
|
||||
}
|
||||
|
||||
export type ExamplesList = ExampleSection[];
|
@ -1,12 +0,0 @@
|
||||
import * as session from '$lib/db/session';
|
||||
|
||||
/** @type {import('@sveltejs/adapter-vercel').Config} */
|
||||
export const config = {
|
||||
runtime: 'nodejs18.x' // see https://github.com/sveltejs/svelte/pull/9136
|
||||
};
|
||||
|
||||
export async function load({ request }) {
|
||||
return {
|
||||
user: await session.from_cookie(request.headers.get('cookie'))
|
||||
};
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<script>
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
setContext('app', {
|
||||
login: () => {
|
||||
const login_window = window.open(
|
||||
`${window.location.origin}/auth/login`,
|
||||
'login',
|
||||
'width=600,height=400'
|
||||
);
|
||||
|
||||
window.addEventListener('message', function handler(event) {
|
||||
if (event.data.source !== 'svelte-auth') return;
|
||||
login_window.close();
|
||||
window.removeEventListener('message', handler);
|
||||
invalidateAll();
|
||||
});
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
const r = await fetch(`/auth/logout`);
|
||||
if (r.ok) invalidateAll();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
@ -1,20 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
|
||||
export async function load({ url, parent }) {
|
||||
let gists = [];
|
||||
let next = null;
|
||||
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
const { user } = await parent();
|
||||
|
||||
if (user) {
|
||||
const offset_param = url.searchParams.get('offset');
|
||||
const offset = offset_param ? parseInt(offset_param) : 0;
|
||||
const search = url.searchParams.get('search');
|
||||
|
||||
({ gists, next } = await gist.list(user, { offset, search }));
|
||||
}
|
||||
|
||||
return { user, gists, next, search };
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import * as session from '$lib/db/session';
|
||||
import * as gist from '$lib/db/gist';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) return new Response(undefined, { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
await gist.destroy(user.id, body.ids);
|
||||
|
||||
return new Response(undefined, { status: 204 });
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load({ url }) {
|
||||
const query = url.searchParams;
|
||||
const gist = query.get('gist');
|
||||
const example = query.get('example');
|
||||
const version = query.get('version');
|
||||
const vim = query.get('vim');
|
||||
|
||||
// redirect to v2 REPL if appropriate
|
||||
if (version && /^[^>]?[12]/.test(version)) {
|
||||
redirect(302, `https://v2.svelte.dev/repl?${query}`);
|
||||
}
|
||||
|
||||
const id = gist || example || 'hello-world';
|
||||
// we need to filter out null values
|
||||
const q = new URLSearchParams();
|
||||
if (version) q.set('version', version);
|
||||
if (vim) q.set('vim', vim);
|
||||
redirect(301, `/repl/${id}?${q}`);
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export function load({ data, url }) {
|
||||
// initialize vim with the search param
|
||||
const vim_search_params = url.searchParams.get('vim');
|
||||
let vim = vim_search_params !== null && vim_search_params !== 'false';
|
||||
// when in the browser check if there's a local storage entry and eventually override
|
||||
// vim if there's not a search params otherwise update the local storage
|
||||
if (browser) {
|
||||
const vim_local_storage = window.localStorage.getItem('svelte:vim-enabled');
|
||||
if (vim_search_params !== null) {
|
||||
window.localStorage.setItem('svelte:vim-enabled', vim.toString());
|
||||
} else if (vim_local_storage) {
|
||||
vim = vim_local_storage !== 'false';
|
||||
}
|
||||
}
|
||||
return { ...data, vim };
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ fetch, params, url }) {
|
||||
const res = await fetch(`/repl/api/${params.id}.json`);
|
||||
|
||||
if (!res.ok) {
|
||||
error(/** @type {import('@sveltejs/kit').NumericRange<400, 599>} */(res.status));
|
||||
}
|
||||
|
||||
const gist = await res.json();
|
||||
|
||||
return {
|
||||
gist,
|
||||
version: url.searchParams.get('version') || '4'
|
||||
};
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { afterNavigate, goto, replaceState } from '$app/navigation';
|
||||
import Repl from '@sveltejs/repl';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { mapbox_setup } from '../../../../config.js';
|
||||
import AppControls from './AppControls.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let version = data.version;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
let repl;
|
||||
let name = data.gist.name;
|
||||
let zen_mode = false;
|
||||
let modified_count = 0;
|
||||
|
||||
function update_query_string(version) {
|
||||
const params = [];
|
||||
|
||||
if (version !== 'latest') params.push(`version=${version}`);
|
||||
|
||||
const url =
|
||||
params.length > 0 ? `/repl/${data.gist.id}?${params.join('&')}` : `/repl/${data.gist.id}`;
|
||||
|
||||
history.replaceState({}, 'x', url);
|
||||
}
|
||||
|
||||
$: if (typeof history !== 'undefined') update_query_string(version);
|
||||
|
||||
onMount(() => {
|
||||
if (data.version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${data.version || '4'}/package.json`)
|
||||
.then((r) => r.json())
|
||||
.then((pkg) => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
repl?.set({
|
||||
files: data.gist.components
|
||||
});
|
||||
});
|
||||
|
||||
function handle_fork(event) {
|
||||
console.log('> handle_fork', event);
|
||||
goto(`/repl/${event.detail.gist.id}?version=${version}`);
|
||||
}
|
||||
|
||||
function handle_change(event) {
|
||||
modified_count = event.detail.files.filter((c) => c.modified).length;
|
||||
}
|
||||
|
||||
$: svelteUrl =
|
||||
browser && version === 'local'
|
||||
? `${location.origin}/repl/local`
|
||||
: `https://unpkg.com/svelte@${version}`;
|
||||
|
||||
$: relaxed = data.gist.relaxed || (data.user && data.user.id === data.gist.owner);
|
||||
|
||||
$: vim = data.vim;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{name} • REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="Description" content="Interactive Svelte playground" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}">
|
||||
<AppControls
|
||||
user={data.user}
|
||||
gist={data.gist}
|
||||
{repl}
|
||||
bind:name
|
||||
bind:zen_mode
|
||||
bind:modified_count
|
||||
on:forked={handle_fork}
|
||||
/>
|
||||
|
||||
{#if browser}
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
{svelteUrl}
|
||||
{relaxed}
|
||||
{vim}
|
||||
injectedJS={mapbox_setup}
|
||||
showModified
|
||||
showAst
|
||||
on:change={handle_change}
|
||||
on:add={handle_change}
|
||||
on:remove={handle_change}
|
||||
previewTheme={$theme.current}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: relative;
|
||||
height: calc(100% - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
height: calc(100dvh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
--app-controls-h: 5.6rem;
|
||||
--pane-controls-h: 4.2rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--sk-back-1);
|
||||
padding: var(--app-controls-h) 0 0 0;
|
||||
/* margin: 0 calc(var(--side-nav) * -1); */
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
|
||||
|
||||
.repl-outer :global(.tab-content),
|
||||
.repl-outer :global(.tab-content.visible) {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
.repl-outer :global(.tab-content) {
|
||||
visibility: hidden;
|
||||
}
|
||||
.repl-outer :global(.tab-content.visible) {
|
||||
visibility: visible;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.zen-mode {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 111;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,339 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import UserMenu from './UserMenu.svelte';
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import * as doNotZip from 'do-not-zip';
|
||||
import downloadBlob from './downloadBlob.js';
|
||||
import { enter } from '$lib/utils/events.js';
|
||||
import { isMac } from '$lib/utils/compat.js';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { login } = getContext('app');
|
||||
|
||||
export let user;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
export let repl;
|
||||
export let gist;
|
||||
export let name;
|
||||
export let zen_mode;
|
||||
export let modified_count;
|
||||
|
||||
let saving = false;
|
||||
let downloading = false;
|
||||
let justSaved = false;
|
||||
let justForked = false;
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise((f) => setTimeout(f, ms));
|
||||
}
|
||||
|
||||
$: canSave = user && gist && gist.owner === user.id;
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
async function fork(intentWasSave) {
|
||||
saving = true;
|
||||
|
||||
const { files } = repl.toJSON();
|
||||
|
||||
try {
|
||||
const r = await fetch(`/repl/create.json`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: files.map((file) => ({
|
||||
name: `${file.name}.${file.type}`,
|
||||
source: file.source
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (r.status < 200 || r.status >= 300) {
|
||||
const { error } = await r.json();
|
||||
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||
}
|
||||
|
||||
const gist = await r.json();
|
||||
dispatch('forked', { gist });
|
||||
|
||||
modified_count = 0;
|
||||
repl.markSaved();
|
||||
|
||||
if (intentWasSave) {
|
||||
justSaved = true;
|
||||
await wait(600);
|
||||
justSaved = false;
|
||||
} else {
|
||||
justForked = true;
|
||||
await wait(600);
|
||||
justForked = false;
|
||||
}
|
||||
} catch (err) {
|
||||
if (navigator.onLine) {
|
||||
alert(err.message);
|
||||
} else {
|
||||
alert(`It looks like you're offline! Find the internet and try again`);
|
||||
}
|
||||
}
|
||||
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!user) {
|
||||
alert('Please log in before saving your app');
|
||||
return;
|
||||
}
|
||||
if (saving) return;
|
||||
|
||||
if (!canSave) {
|
||||
fork(true);
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
|
||||
try {
|
||||
// Send all files back to API
|
||||
// ~> Any missing files are considered deleted!
|
||||
const { files } = repl.toJSON();
|
||||
|
||||
const r = await fetch(`/repl/save/${gist.id}.json`, {
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: files.map((file) => ({
|
||||
name: `${file.name}.${file.type}`,
|
||||
source: file.source
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (r.status < 200 || r.status >= 300) {
|
||||
const { error } = await r.json();
|
||||
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||
}
|
||||
|
||||
modified_count = 0;
|
||||
repl.markSaved();
|
||||
justSaved = true;
|
||||
await wait(600);
|
||||
justSaved = false;
|
||||
} catch (err) {
|
||||
if (navigator.onLine) {
|
||||
alert(err.message);
|
||||
} else {
|
||||
alert(`It looks like you're offline! Find the internet and try again`);
|
||||
}
|
||||
}
|
||||
|
||||
saving = false;
|
||||
}
|
||||
|
||||
async function download() {
|
||||
downloading = true;
|
||||
|
||||
const { files: components, imports } = repl.toJSON();
|
||||
|
||||
const files = await (await fetch('/svelte-app.json')).json();
|
||||
|
||||
if (imports.length > 0) {
|
||||
const idx = files.findIndex(({ path }) => path === 'package.json');
|
||||
const pkg = JSON.parse(files[idx].data);
|
||||
const { devDependencies } = pkg;
|
||||
imports.forEach((mod) => {
|
||||
const match = /^(@[^/]+\/)?[^@/]+/.exec(mod);
|
||||
devDependencies[match[0]] = 'latest';
|
||||
});
|
||||
pkg.devDependencies = devDependencies;
|
||||
files[idx].data = JSON.stringify(pkg, null, ' ');
|
||||
}
|
||||
|
||||
files.push(
|
||||
...components.map((component) => ({
|
||||
path: `src/${component.name}.${component.type}`,
|
||||
data: component.source
|
||||
}))
|
||||
);
|
||||
files.push({
|
||||
path: `src/main.js`,
|
||||
data: `import App from './App.svelte';
|
||||
|
||||
var app = new App({
|
||||
target: document.body
|
||||
});
|
||||
|
||||
export default app;`
|
||||
});
|
||||
|
||||
downloadBlob(doNotZip.toBlob(files), 'svelte-app.zip');
|
||||
|
||||
downloading = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div class="app-controls">
|
||||
<input
|
||||
bind:value={name}
|
||||
on:focus={(e) => e.target.select()}
|
||||
use:enter={(e) => /** @type {HTMLInputElement} */ (e.target).blur()}
|
||||
/>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="icon" on:click={() => (zen_mode = !zen_mode)} title="fullscreen editor">
|
||||
{#if zen_mode}
|
||||
<Icon name="close" />
|
||||
{:else}
|
||||
<Icon name="maximize" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={downloading} on:click={download} title="download zip file">
|
||||
<Icon name="download" />
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={saving || !user} on:click={() => fork(false)} title="fork">
|
||||
{#if justForked}
|
||||
<Icon name="check" />
|
||||
{:else}
|
||||
<Icon name="git-branch" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled={saving || !user} on:click={save} title="save">
|
||||
{#if justSaved}
|
||||
<Icon name="check" />
|
||||
{:else}
|
||||
<Icon name="save" />
|
||||
{#if modified_count}
|
||||
<div class="badge">{modified_count}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if user}
|
||||
<UserMenu {user} />
|
||||
{:else}
|
||||
<button class="icon" on:click|preventDefault={login}>
|
||||
<Icon name="log-in" />
|
||||
<span> Log in to save</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--app-controls-h);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem var(--sk-page-padding-side);
|
||||
background-color: var(--sk-back-4);
|
||||
color: var(--sk-text-1);
|
||||
white-space: nowrap;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
text-align: right;
|
||||
margin-right: 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transform: translateY(0.1rem);
|
||||
display: inline-block;
|
||||
padding: 0.2em;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
color: var(--sk-text-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon:hover,
|
||||
.icon:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.icon:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.icon[title^='fullscreen'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
margin: 0 0.2em 0 0.4rem;
|
||||
padding-top: 0.2em;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
input:hover {
|
||||
border-bottom: 1px solid currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
input:focus {
|
||||
border-bottom: 1px solid currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #ff3e00;
|
||||
border-radius: 100%;
|
||||
font-size: 10px;
|
||||
padding: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.icon[title^='fullscreen'] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
button span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,134 +0,0 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { Icon } from '@sveltejs/site-kit/components';
|
||||
import { click_outside, focus_outside } from '@sveltejs/site-kit/actions';
|
||||
const { logout } = getContext('app');
|
||||
|
||||
export let user;
|
||||
|
||||
let showMenu = false;
|
||||
let name;
|
||||
|
||||
$: name = user.github_name || user.github_login;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="user"
|
||||
use:focus_outside={() => (showMenu = false)}
|
||||
use:click_outside={() => (showMenu = false)}
|
||||
>
|
||||
<button
|
||||
on:click={() => (showMenu = !showMenu)}
|
||||
aria-expanded={showMenu}
|
||||
class="trigger"
|
||||
aria-label={name}
|
||||
>
|
||||
<span class="name">{name}</span>
|
||||
<img alt="" src={user.github_avatar_url} />
|
||||
<Icon name={showMenu ? 'chevron-up' : 'chevron-down'} />
|
||||
</button>
|
||||
|
||||
{#if showMenu}
|
||||
<div class="menu">
|
||||
<a href="/apps">Your saved apps</a>
|
||||
<button on:click={logout}>Log out</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.user {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0em 0 0 0.3rem;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
outline-offset: 2px;
|
||||
transform: translateY(0.1rem);
|
||||
--opacity: 0.7;
|
||||
}
|
||||
|
||||
.trigger:hover,
|
||||
.trigger:focus-visible,
|
||||
.trigger[aria-expanded='true'] {
|
||||
--opacity: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 1;
|
||||
display: none;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.name,
|
||||
.trigger :global(.icon) {
|
||||
display: none;
|
||||
opacity: var(--opacity);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.2rem;
|
||||
transform: translateY(-0.1rem);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
width: calc(100% + 1.6rem);
|
||||
min-width: 10em;
|
||||
top: 3rem;
|
||||
right: -1.6rem;
|
||||
background-color: var(--sk-back-2);
|
||||
padding: 0.8rem 1.6rem;
|
||||
z-index: 99;
|
||||
text-align: left;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu button,
|
||||
.menu a {
|
||||
background-color: transparent;
|
||||
font-family: var(--sk-font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
padding: 0.4rem 0;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
color: var(--sk-text-2);
|
||||
}
|
||||
|
||||
.menu button:hover,
|
||||
.menu button:focus-visible,
|
||||
.menu a:hover,
|
||||
.menu a:focus-visible {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.user {
|
||||
padding: 0em 0 0 1.6rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.name,
|
||||
.trigger :global(.icon) {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
/**
|
||||
* @param {Blob} blob
|
||||
* @param {string} filename
|
||||
*/
|
||||
export default (blob, filename) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
link.remove();
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ fetch, params, url }) {
|
||||
const res = await fetch(`/repl/api/${params.id}.json`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw error(/** @type {any} */ (res.status));
|
||||
}
|
||||
|
||||
const gist = await res.json();
|
||||
|
||||
return {
|
||||
gist,
|
||||
version: url.searchParams.get('version') || '4'
|
||||
};
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import Repl from '@sveltejs/repl';
|
||||
import { mapbox_setup } from '../../../../../config.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
let version = data.version;
|
||||
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
let repl;
|
||||
|
||||
function update_query_string(version) {
|
||||
const params = [];
|
||||
|
||||
if (version !== 'latest') params.push(`version=${version}`);
|
||||
|
||||
const url =
|
||||
params.length > 0
|
||||
? `/repl/${data.gist.id}/embed?${params.join('&')}`
|
||||
: `/repl/${data.gist.id}/embed`;
|
||||
|
||||
history.replaceState({}, 'x', url);
|
||||
}
|
||||
|
||||
$: if (typeof history !== 'undefined') update_query_string(version);
|
||||
|
||||
onMount(() => {
|
||||
if (data.version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${data.version || '4'}/package.json`)
|
||||
.then((r) => r.json())
|
||||
.then((pkg) => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
afterNavigate(() => {
|
||||
repl?.set({
|
||||
files: data.gist.components
|
||||
});
|
||||
});
|
||||
|
||||
$: svelteUrl =
|
||||
browser && version === 'local'
|
||||
? `${location.origin}/repl/local`
|
||||
: `https://unpkg.com/svelte@${version}`;
|
||||
|
||||
$: relaxed = data.gist.relaxed || (data.user && data.user.id === data.gist.owner);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.gist.name} • REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="{data.gist.name} • REPL • Svelte" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="Description" content="Interactive Svelte playground" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="repl-outer">
|
||||
{#if browser}
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
{svelteUrl}
|
||||
{relaxed}
|
||||
injectedJS={mapbox_setup}
|
||||
showModified
|
||||
showAst
|
||||
previewTheme={$theme.current}
|
||||
embedded
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--sk-back-1);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
--pane-controls-h: 4.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
@ -1,93 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { client } from '$lib/db/client.js';
|
||||
import * as gist from '$lib/db/gist.js';
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = 'auto';
|
||||
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/;
|
||||
|
||||
|
||||
/** @param {import('$lib/server/examples/types').ExamplesData[number]['examples'][number]['files'][number][]} files */
|
||||
function munge(files) {
|
||||
return files
|
||||
.map((file) => {
|
||||
const dot = file.name.lastIndexOf('.');
|
||||
let name = file.name.slice(0, dot);
|
||||
let type = file.name.slice(dot + 1);
|
||||
|
||||
if (type === 'html') type = 'svelte';
|
||||
// @ts-expect-error what is file.source? by @PuruVJ
|
||||
return { name, type, source: file.source ?? file.content ?? '' };
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.name === 'App' && a.type === 'svelte') return -1;
|
||||
if (b.name === 'App' && b.type === 'svelte') return 1;
|
||||
|
||||
if (a.type !== b.type) return a.type === 'svelte' ? -1 : 1;
|
||||
|
||||
return a.name < b.name ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET({ params }) {
|
||||
// Currently, these pages(that are in examples/) are prerendered. To avoid making any FS requests,
|
||||
// We prerender examples pages during build time. That means, when something like `/repl/hello-world.json`
|
||||
// is accessed, this function won't be run at all, as it will be served from the filesystem
|
||||
|
||||
const example = get_example(examples_data, params.id);
|
||||
if (example) {
|
||||
return json({
|
||||
id: params.id,
|
||||
name: example.title,
|
||||
owner: null,
|
||||
relaxed: false, // TODO is this right? EDIT: It was example.relaxed before, which no example return to my knowledge. By @PuruVJ
|
||||
components: munge(example.files)
|
||||
});
|
||||
}
|
||||
|
||||
if (dev && !client) {
|
||||
// in dev with no local Supabase configured, proxy to production
|
||||
// this lets us at least load saved REPLs
|
||||
const res = await fetch(`https://svelte.dev/repl/api/${params.id}.json`);
|
||||
|
||||
// returning the response directly results in a bizarre
|
||||
// content encoding error, so we create a new one
|
||||
return new Response(await res.text(), {
|
||||
status: res.status,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!UUID_REGEX.test(params.id)) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
const app = await gist.read(params.id);
|
||||
|
||||
if (!app) {
|
||||
error(404, 'not found');
|
||||
}
|
||||
|
||||
return json({
|
||||
id: params.id,
|
||||
name: app.name,
|
||||
// @ts-ignore
|
||||
owner: app.userid,
|
||||
relaxed: false,
|
||||
// @ts-expect-error app.files has a `source` property
|
||||
components: munge(app.files)
|
||||
});
|
||||
}
|
||||
|
||||
export async function entries() {
|
||||
const { get_examples_list } = await import('$lib/server/examples/index.js');
|
||||
|
||||
return get_examples_list(examples_data)
|
||||
.map(({ examples }) => examples)
|
||||
.flatMap((val) => val.map(({ slug }) => ({ id: slug })));
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
import * as session from '$lib/db/session';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) error(401);
|
||||
|
||||
const body = await request.json();
|
||||
const result = await gist.create(user, body);
|
||||
|
||||
// normalize id
|
||||
result.id = result.id.replace(/-/g, '');
|
||||
|
||||
return json(result, {
|
||||
status: 201
|
||||
});
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load({ url }) {
|
||||
if (!url.searchParams.has('gist')) {
|
||||
throw redirect(301, '/repl/hello-world/embed');
|
||||
} else {
|
||||
const searchParamsWithoutGist = new URLSearchParams(url.searchParams);
|
||||
searchParamsWithoutGist.delete('gist');
|
||||
throw redirect(
|
||||
301,
|
||||
`/repl/${url.searchParams.get('gist')}/embed?${searchParamsWithoutGist.toString()}`
|
||||
);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const local_svelte_path = env.LOCAL_SVELTE_PATH || '../../../svelte';
|
||||
|
||||
export async function GET({ params: { path } }) {
|
||||
if (import.meta.env.PROD || ('/' + path).includes('/.')) {
|
||||
return new Response(undefined, { status: 403 });
|
||||
}
|
||||
|
||||
const { readFile } = await import('node:fs/promises');
|
||||
|
||||
return new Response(await readFile(`${local_svelte_path}/${path}`), {
|
||||
headers: { 'Content-Type': 'text/javascript' }
|
||||
});
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import * as gist from '$lib/db/gist';
|
||||
import * as session from '$lib/db/session';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
// TODO reimplement as an action
|
||||
export async function PUT({ params, request }) {
|
||||
const user = await session.from_cookie(request.headers.get('cookie'));
|
||||
if (!user) error(401, 'Unauthorized');
|
||||
|
||||
const body = await request.json();
|
||||
await gist.update(user, params.id, body);
|
||||
|
||||
return new Response(undefined, { status: 204 });
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load() {
|
||||
return {
|
||||
posts: get_blog_list(await get_blog_data())
|
||||
};
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ params }) {
|
||||
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
|
||||
|
||||
if (!post) error(404);
|
||||
|
||||
// forgive me — terrible hack necessary to get diffs looking sensible
|
||||
// on the `runes` blog post
|
||||
post.content = post.content.replace(/( )+/gm, (match) => ' '.repeat(match.length / 4));
|
||||
|
||||
return {
|
||||
post
|
||||
};
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { copy_code_descendants } from '@sveltejs/site-kit/actions';
|
||||
import { DocsOnThisPage, setupDocsHovers } from '@sveltejs/site-kit/docs';
|
||||
|
||||
export let data;
|
||||
|
||||
setupDocsHovers();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.post.title}</title>
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={data.post.title} />
|
||||
<meta name="twitter:description" content={data.post.description} />
|
||||
<meta name="Description" content={data.post.description} />
|
||||
|
||||
<meta name="twitter:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
|
||||
<meta name="og:image" content="https://svelte.dev/blog/{$page.params.slug}/card.png" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="content">
|
||||
<article class="post listify text" use:copy_code_descendants>
|
||||
<h1>{data.post.title}</h1>
|
||||
<p class="standfirst">{data.post.description}</p>
|
||||
|
||||
<p class="byline">
|
||||
{#each data.post.authors as author, i}
|
||||
{@const show_comma = data.post.authors.length > 2 && i < data.post.authors.length - 1}
|
||||
{@const show_and = i === data.post.authors.length - 2}
|
||||
<svelte:element this={author.url ? 'a' : 'span'} href={author.url}
|
||||
>{author.name}</svelte:element
|
||||
>{#if show_comma}, {/if}
|
||||
{#if show_and}and {/if}
|
||||
{/each}
|
||||
<time datetime={data.post.date}>{data.post.date_formatted}</time>
|
||||
</p>
|
||||
|
||||
<DocsOnThisPage
|
||||
details={{
|
||||
content: '',
|
||||
file: '',
|
||||
path: `/blog/${data.post.slug}`,
|
||||
sections: data.post.sections,
|
||||
slug: data.post.slug,
|
||||
title: data.post.title
|
||||
}}
|
||||
orientation="inline"
|
||||
/>
|
||||
|
||||
{@html data.post.content}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.post {
|
||||
padding: var(--sk-page-padding-top) var(--sk-page-padding-side) 6rem var(--sk-page-padding-side);
|
||||
max-width: var(--sk-page-main-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
font-size: var(--sk-text-s);
|
||||
color: var(--sk-text-3);
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.byline {
|
||||
margin: 0 0 1rem 0;
|
||||
padding: 1.6rem 0 0 0;
|
||||
border-top: var(--sk-thick-border-width) solid #6767785b;
|
||||
font-size: var(--sk-text-xs);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post :global(figure) {
|
||||
margin: 1.6rem 0 3.2rem 0;
|
||||
}
|
||||
|
||||
.post :global(figure) :global(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.post :global(figcaption) {
|
||||
color: var(--sk-theme-2);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post :global(video) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(aside) {
|
||||
float: right;
|
||||
margin: 0 0 1em 1em;
|
||||
width: 16rem;
|
||||
color: var(--sk-theme-2);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.post :global(.max) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(iframe) {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
margin: 2em 0;
|
||||
border-radius: var(--sk-border-radius);
|
||||
border: 0.8rem solid var(--sk-theme-2);
|
||||
}
|
||||
|
||||
@media (min-width: 910px) {
|
||||
.post :global(.max) {
|
||||
width: calc(100vw - 2 * var(--sk-page-padding-side));
|
||||
margin: 0 calc(var(--sk-page-main-width) / 2 - 50vw);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post :global(.max) > :global(*) {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.post :global(iframe) {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 2em auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,51 +0,0 @@
|
||||
import { get_blog_data, get_processed_blog_post } from '$lib/server/blog/index.js';
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import satori from 'satori';
|
||||
import { html as toReactNode } from 'satori-html';
|
||||
import Card from './Card.svelte';
|
||||
import OverpassRegular from './Overpass-Regular.ttf';
|
||||
|
||||
const height = 630;
|
||||
const width = 1200;
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET({ params }) {
|
||||
const post = await get_processed_blog_post(await get_blog_data(), params.slug);
|
||||
|
||||
if (!post) error(404);
|
||||
|
||||
// @ts-ignore
|
||||
const result = Card.render({ post });
|
||||
const element = toReactNode(`${result.html}<style>${result.css.code}</style>`);
|
||||
|
||||
const svg = await satori(element, {
|
||||
fonts: [
|
||||
{
|
||||
name: 'Overpass',
|
||||
data: Buffer.from(OverpassRegular),
|
||||
style: 'normal',
|
||||
weight: 400
|
||||
}
|
||||
],
|
||||
height,
|
||||
width
|
||||
});
|
||||
|
||||
const resvg = new Resvg(svg, {
|
||||
fitTo: {
|
||||
mode: 'width',
|
||||
value: width
|
||||
}
|
||||
});
|
||||
|
||||
const image = resvg.render();
|
||||
|
||||
return new Response(image.asPng(), {
|
||||
headers: {
|
||||
'content-type': 'image/png',
|
||||
'cache-control': 'public, max-age=600' // cache for 10 minutes
|
||||
}
|
||||
});
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<img src="https://sveltejs.github.io/assets/artwork/svelte-machine.png" alt="Svelte Machine" />
|
||||
|
||||
<div class="text">
|
||||
<h1>{post.title}</h1>
|
||||
<p class="date">{post.date_formatted}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'Overpass';
|
||||
background: white;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
width: 125%;
|
||||
height: 100%;
|
||||
top: 5%;
|
||||
left: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 80px;
|
||||
width: 55%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 72px;
|
||||
margin: 0;
|
||||
color: #222;
|
||||
font-weight: 400;
|
||||
line-height: 80px;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
color: #555;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
Binary file not shown.
@ -1,72 +0,0 @@
|
||||
import { get_blog_data, get_blog_list } from '$lib/server/blog/index.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
|
||||
|
||||
/** @param {string} str */
|
||||
function formatPubdate(str) {
|
||||
const [y, m, d] = str.split('-');
|
||||
return `${d} ${months[+m]} ${y} 12:00 +0000`;
|
||||
}
|
||||
|
||||
/** @param {string} html */
|
||||
function escapeHTML(html) {
|
||||
/** @type {{ [key: string]: string }} */
|
||||
const chars = {
|
||||
'"': 'quot',
|
||||
"'": '#39',
|
||||
'&': 'amp',
|
||||
'<': 'lt',
|
||||
'>': 'gt'
|
||||
};
|
||||
|
||||
return html.replace(/["'&<>]/g, (c) => `&${chars[c]};`);
|
||||
}
|
||||
|
||||
/** @param {import('$lib/server/blog/types').BlogPostSummary[]} posts */
|
||||
const get_rss = (posts) =>
|
||||
`
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0">
|
||||
|
||||
<channel>
|
||||
<title>Svelte blog</title>
|
||||
<link>https://svelte.dev/blog</link>
|
||||
<description>News and information about the magical disappearing UI framework</description>
|
||||
<image>
|
||||
<url>https://svelte.dev/favicon.png</url>
|
||||
<title>Svelte</title>
|
||||
<link>https://svelte.dev/blog</link>
|
||||
</image>
|
||||
${posts
|
||||
.filter((post) => !post.draft)
|
||||
.map(
|
||||
(post) => `
|
||||
<item>
|
||||
<title>${escapeHTML(post.title)}</title>
|
||||
<link>https://svelte.dev/blog/${post.slug}</link>
|
||||
<description>${escapeHTML(post.description)}</description>
|
||||
<pubDate>${formatPubdate(post.date)}</pubDate>
|
||||
</item>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</channel>
|
||||
|
||||
</rss>
|
||||
`
|
||||
.replace(/>[^\S]+/gm, '>')
|
||||
.replace(/[^\S]+</gm, '<')
|
||||
.trim();
|
||||
|
||||
export async function GET() {
|
||||
const posts = get_blog_list(await get_blog_data());
|
||||
|
||||
return new Response(get_rss(posts), {
|
||||
headers: {
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
|
||||
'Content-Type': 'application/rss+xml'
|
||||
}
|
||||
});
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
export function load() {
|
||||
redirect(dev ? 307 : 308, '/docs');
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function load() {
|
||||
redirect(301, 'examples/hello-world');
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function load({ params }) {
|
||||
const examples_list = get_examples_list(examples_data);
|
||||
const example = get_example(examples_data, params.slug);
|
||||
|
||||
return {
|
||||
examples_list,
|
||||
example,
|
||||
slug: params.slug
|
||||
};
|
||||
}
|
@ -1,128 +0,0 @@
|
||||
<!-- FIXME sometimes it adds a trailing slash when landing -->
|
||||
<script>
|
||||
// @ts-check
|
||||
import { navigating } from '$app/stores';
|
||||
import ScreenToggle from '$lib/components/ScreenToggle.svelte';
|
||||
import Repl from '@sveltejs/repl';
|
||||
import { theme } from '@sveltejs/site-kit/stores';
|
||||
import { mapbox_setup, svelteUrl } from '../../../config';
|
||||
import TableOfContents from './TableOfContents.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
/** @type {number} */
|
||||
let width;
|
||||
let offset = 1;
|
||||
/** @type {import('@sveltejs/repl').default} */
|
||||
let repl;
|
||||
|
||||
const clone = (file) => ({
|
||||
name: file.name.replace(/.\w+$/, ''),
|
||||
type: file.type,
|
||||
source: file.content
|
||||
});
|
||||
|
||||
$: mobile = width < 768; // note: same as per media query below
|
||||
/** @type {'columns' | 'rows'} */
|
||||
$: replOrientation = mobile || width > 1080 ? 'columns' : 'rows';
|
||||
|
||||
$: repl && repl.set({ files: data.example.files.map(clone) });
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.example?.title} {data.example?.title ? '•' : ''} Svelte Examples</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte examples" />
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps" />
|
||||
<meta name="Description" content="Interactive example Svelte apps" />
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="visually-hidden">Examples</h1>
|
||||
<div class="examples-container" bind:clientWidth={width}>
|
||||
<div class="viewport offset-{offset}">
|
||||
<TableOfContents
|
||||
sections={data.examples_list}
|
||||
active_section={data.example?.slug}
|
||||
isLoading={!!$navigating}
|
||||
/>
|
||||
<div class="repl-container" class:loading={$navigating}>
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
{svelteUrl}
|
||||
orientation={replOrientation}
|
||||
fixed={mobile}
|
||||
relaxed
|
||||
injectedJS={mapbox_setup}
|
||||
previewTheme={$theme.current}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if mobile}
|
||||
<ScreenToggle bind:offset labels={['index', 'input', 'output']} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.examples-container {
|
||||
position: relative;
|
||||
height: calc(100vh - var(--sk-nav-height) - var(--sk-banner-bottom-height));
|
||||
overflow: hidden;
|
||||
padding: 0 0 42px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
display: grid;
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
grid-template-columns: 33.333% 66.666%;
|
||||
transition: transform 0.3s;
|
||||
grid-auto-rows: 100%;
|
||||
}
|
||||
|
||||
.repl-container.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
|
||||
|
||||
.repl-container :global(.tab-content),
|
||||
.repl-container :global(.tab-content.visible) {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
.repl-container :global(.tab-content) {
|
||||
visibility: hidden;
|
||||
}
|
||||
.repl-container :global(.tab-content.visible) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.offset-1 {
|
||||
transform: translate(-33.333%, 0);
|
||||
}
|
||||
.offset-2 {
|
||||
transform: translate(-66.666%, 0);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.examples-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
/* TODO */
|
||||
grid-template-columns: 36rem auto;
|
||||
grid-auto-rows: 100%;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.offset-1,
|
||||
.offset-2 {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,123 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let sections = [];
|
||||
export let active_section = null;
|
||||
export let isLoading = false;
|
||||
|
||||
let active_el;
|
||||
|
||||
onMount(() => {
|
||||
active_el.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
</script>
|
||||
|
||||
<ul class="examples-toc">
|
||||
{#each sections as section}
|
||||
<!-- Avoid embeds -->
|
||||
{#if section.title !== 'Embeds'}
|
||||
<li>
|
||||
<span class="section-title">{section.title}</span>
|
||||
|
||||
{#each section.examples as example}
|
||||
<div class="row" class:active={example.slug === active_section} class:loading={isLoading}>
|
||||
<a
|
||||
href="/examples/{example.slug}"
|
||||
class="row"
|
||||
class:active={example.slug === active_section}
|
||||
class:loading={isLoading}
|
||||
>
|
||||
<img
|
||||
class="thumbnail"
|
||||
alt="{example.title} thumbnail"
|
||||
src="/examples/thumbnails/{example.slug}.jpg"
|
||||
/>
|
||||
|
||||
<span>{example.title}</span>
|
||||
</a>
|
||||
{#if example.slug === active_section}
|
||||
<a bind:this={active_el} href="/repl/{example.slug}" class="repl-link">REPL</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.examples-toc {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--sk-back-4);
|
||||
background-color: var(--sk-back-3);
|
||||
color: var(--sk-text-2);
|
||||
padding: 3rem 3rem 0 3rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.examples-toc li {
|
||||
display: block;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 4.8rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
padding: 0 0 0.8rem 0;
|
||||
font: 400 var(--sk-text-xs) var(--sk-font);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0.2rem 3rem;
|
||||
margin: 0 -3rem;
|
||||
}
|
||||
|
||||
div.active {
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.active.loading {
|
||||
background: rgba(0, 0, 0, 0.1) calc(100% - 3rem) 47% no-repeat url(/icons/loading.svg);
|
||||
background-size: 1em 1em;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
color: var(--sk-text-2);
|
||||
border-bottom: none;
|
||||
font-size: 1.6rem;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--sk-text-1);
|
||||
}
|
||||
|
||||
.repl-link {
|
||||
flex: 0 1 auto;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
background-color: #fff;
|
||||
object-fit: contain;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 2px;
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.13);
|
||||
margin: 0.2em 0.5em 0.2em 0;
|
||||
}
|
||||
</style>
|
@ -1,8 +0,0 @@
|
||||
// @ts-check
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
import { get_examples_list } from '$lib/server/examples/index.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET = () => {
|
||||
return json(get_examples_list(examples_data));
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import examples_data from '$lib/generated/examples-data.js';
|
||||
import { get_example, get_examples_list } from '$lib/server/examples/index.js';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const GET = ({ params }) => {
|
||||
const examples = new Set(
|
||||
get_examples_list(examples_data)
|
||||
.map((category) => category.examples)
|
||||
.flat()
|
||||
.map((example) => example.slug)
|
||||
);
|
||||
|
||||
if (!examples.has(params.slug)) error(404, 'Example not found');
|
||||
|
||||
return json(get_example(examples_data, params.slug));
|
||||
};
|
||||
|
||||
export async function entries() {
|
||||
const examples_list = get_examples_list(examples_data);
|
||||
|
||||
return examples_list
|
||||
.map(({ examples }) => examples)
|
||||
.flatMap((val) => val.map(({ slug }) => ({ slug })));
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export function GET() {
|
||||
redirect(308, '/docs/faq');
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export function load() {
|
||||
redirect(
|
||||
307,
|
||||
'https://docs.google.com/document/d/1IA9Z5rcIm_KRxvh_L42d2NDdYRHZ72MfszhyJrsmf5A'
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 2.1 KiB |
Loading…
Reference in new issue