Remove site - new home at sveltejs/sites (#6994)
@ -1,7 +0,0 @@
|
||||
/*
|
||||
!/Dockerfile
|
||||
!/package.json
|
||||
!/package-lock.json
|
||||
!/build
|
||||
!/static
|
||||
!/content
|
@ -1,13 +0,0 @@
|
||||
NODE_ENV=
|
||||
|
||||
PORT=3000
|
||||
BASEURL=http://localhost:3000
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
VITE_MAPBOX_ACCESS_TOKEN=
|
||||
|
||||
PGHOST=localhost
|
||||
PGPORT=5432
|
||||
PGUSER=username
|
||||
PGPASSWORD=password
|
||||
PGDATABASE=database_name
|
@ -1,58 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
rules: {
|
||||
indent: [2, 'tab', { SwitchCase: 1 }],
|
||||
semi: [2, 'always'],
|
||||
'keyword-spacing': [2, { before: true, after: true }],
|
||||
'space-before-blocks': [2, 'always'],
|
||||
'no-mixed-spaces-and-tabs': [2, 'smart-tabs'],
|
||||
'no-cond-assign': 0,
|
||||
'no-unused-vars': 2,
|
||||
'object-shorthand': [2, 'always'],
|
||||
'no-const-assign': 2,
|
||||
'no-class-assign': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-var': 2,
|
||||
'no-unreachable': 2,
|
||||
'valid-typeof': 2,
|
||||
'quote-props': [2, 'as-needed'],
|
||||
'one-var': [2, 'never'],
|
||||
'prefer-arrow-callback': 2,
|
||||
'prefer-const': [2, { destructuring: 'all' }],
|
||||
'arrow-spacing': 2,
|
||||
'no-inner-declarations': 0,
|
||||
'require-atomic-updates': 0
|
||||
},
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
mocha: true
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings'
|
||||
],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
processor: 'svelte3/svelte3'
|
||||
}
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 9,
|
||||
sourceType: 'module'
|
||||
},
|
||||
settings: {
|
||||
'import/core-modules': ['svelte'],
|
||||
'svelte3/compiler': (() => {
|
||||
try {
|
||||
return require('svelte/compiler');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
}
|
||||
};
|
@ -1 +0,0 @@
|
||||
#!include:.dockerignore
|
@ -1,22 +0,0 @@
|
||||
# IMPORTANT: Don't use this Dockerfile in your own projects without also looking at the .dockerignore file.
|
||||
# Without an appropriate .dockerignore, this Dockerfile will copy a large number of unneeded files into your image.
|
||||
|
||||
FROM mhart/alpine-node:14
|
||||
|
||||
# install dependencies
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --production
|
||||
|
||||
###
|
||||
# Only copy over the Node pieces we need
|
||||
# ~> Saves 35MB
|
||||
###
|
||||
FROM mhart/alpine-node:slim-14
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=0 /app .
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "build"]
|
@ -1,22 +0,0 @@
|
||||
HASH := `git rev-parse --short HEAD`
|
||||
|
||||
SERVICE := svelte-website
|
||||
PROJECT := svelte-dev
|
||||
|
||||
IMAGE := gcr.io/$(PROJECT)/$(SERVICE):$(HASH)
|
||||
|
||||
sveltekit:
|
||||
@echo "\n~> updating template & contributors list"
|
||||
@npm run update
|
||||
@echo "\n~> building SvelteKit app"
|
||||
@npm run build
|
||||
|
||||
|
||||
docker:
|
||||
@echo "\n~> building docker image"
|
||||
@gcloud builds submit --project $(PROJECT) -t $(IMAGE)
|
||||
|
||||
|
||||
deploy: sveltekit docker
|
||||
@echo "\n~> deploying $(SERVICE) to Cloud Run servers"
|
||||
@gcloud run deploy $(SERVICE) --project $(PROJECT) --allow-unauthenticated --platform managed --region us-central1 --image $(IMAGE) --memory=512Mi
|
@ -1,84 +0,0 @@
|
||||
## Running locally
|
||||
|
||||
Set up the site sub-project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sveltejs/svelte.git
|
||||
cd site
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
and navigate to [localhost:3000](http://localhost:3000).
|
||||
|
||||
The first time you run the site locally, it will update the list of Contributors and REPL dependencies. After this it won't run again unless you force it by running:
|
||||
|
||||
```bash
|
||||
npm run update
|
||||
```
|
||||
|
||||
## Running using the local copy of Svelte
|
||||
|
||||
By default, the REPL will fetch the most recent version of Svelte from https://unpkg.com/svelte. When running the site locally, you can also use your local copy of Svelte.
|
||||
|
||||
To produce the proper browser-compatible UMD build of the compiler, you will need to run `npm run build` (or `npm run dev`) in the root of this repository with the `PUBLISH` environment variable set to any non-empty string:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/sveltejs/svelte.git
|
||||
cd svelte
|
||||
npm ci
|
||||
PUBLISH=1 npm run build
|
||||
cd site
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Then visit the REPL at [localhost:3000/repl?version=local](http://localhost:3000/repl?version=local). Please note that the local REPL only works with `npm run dev` and not when building the site for production usage.
|
||||
|
||||
## REPL GitHub integration
|
||||
|
||||
In order for the REPL's GitHub integration to work properly when running locally, you will need to:
|
||||
- [create a GitHub OAuth app](https://github.com/settings/developers):
|
||||
- set `Authorization callback URL` to `http://localhost:3000/auth/callback`;
|
||||
- set `Application name` as you like, and `Homepage URL` as `http://localhost:3000/`;
|
||||
- create the app and take note of `Client ID` and `Client Secret`
|
||||
- in this repo, create `site/.env` containing:
|
||||
```
|
||||
GITHUB_CLIENT_ID=[your app's Client ID]
|
||||
GITHUB_CLIENT_SECRET=[your app's Client Secret]
|
||||
BASEURL=http://localhost:3000
|
||||
```
|
||||
|
||||
## Building the site
|
||||
|
||||
To build the website, run `npm run build`. The output can be found in `build`.
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run using `npm run test`.
|
||||
|
||||
|
||||
## Linking `@sveltejs/site-kit` and `@sveltejs/svelte-repl`
|
||||
|
||||
This site depends on `@sveltejs/site-kit` (a collection of styles, components and icons used in common by *.svelte.dev websites), and `@sveltejs/svelte-repl`.
|
||||
|
||||
In order to work on features that depend on those packages, you need to [link](https://docs.npmjs.com/cli/link) their repositories:
|
||||
|
||||
- `cd <somewhere>`
|
||||
- `git clone https://github.com/sveltejs/site-kit`
|
||||
- `git clone https://github.com/sveltejs/svelte-repl`
|
||||
- `cd <somewhere>/site-kit`
|
||||
- `npm link`
|
||||
- `cd <somewhere>/svelte-repl`
|
||||
- `npm link`
|
||||
- `cd <svelte-repo>/site`
|
||||
- `npm link @sveltejs/site-kit`
|
||||
- `npm link @sveltejs/svelte-repl`
|
||||
|
||||
|
||||
|
||||
## Translating the API docs
|
||||
|
||||
Anchors are automatically generated using headings in the documentation and by default (for the english language) they are latinised to make sure the URL is always conforming to RFC3986.
|
||||
|
||||
If we need to translate the API documentation to a language using unicode chars, we can setup this app to export the correct anchors by setting up `SLUG_PRESERVE_UNICODE` to `true` in `config.js`.
|
@ -1,2 +0,0 @@
|
||||
export const SLUG_PRESERVE_UNICODE = false;
|
||||
export const SLUG_SEPARATOR = '_';
|
@ -1,15 +0,0 @@
|
||||
version: "3.0"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.4
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=${PGDATABASE}
|
||||
- POSTGRES_USER=${PGUSER}
|
||||
- POSTGRES_PASSWORD=${PGPASSWORD}
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- "4000:8080"
|
@ -1,25 +0,0 @@
|
||||
exports.up = DB => {
|
||||
DB.sql(`
|
||||
create table if not exists users (
|
||||
id serial primary key,
|
||||
uid character varying(255) not null unique,
|
||||
name character varying(255),
|
||||
username character varying(255) not null,
|
||||
avatar text,
|
||||
github_token character varying(255),
|
||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp with time zone
|
||||
);
|
||||
|
||||
create unique index if not exists users_pkey ON users(id int4_ops);
|
||||
create unique index if not exists users_uid_key ON users(uid text_ops);
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = DB => {
|
||||
DB.sql(`
|
||||
drop table if exists users cascade;
|
||||
drop index if exists users_uid_key;
|
||||
drop index if exists users_pkey;
|
||||
`);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
exports.up = DB => {
|
||||
DB.sql(`
|
||||
create table if not exists gists (
|
||||
id serial primary key,
|
||||
uid uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
user_id integer REFERENCES users(id) not null,
|
||||
name character varying(255) not null,
|
||||
files json not null,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp with time zone
|
||||
);
|
||||
|
||||
create unique index if not exists gists_pkey ON gists(id int4_ops);
|
||||
create index if not exists gists_user_id_key ON gists(user_id int4_ops);
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = DB => {
|
||||
DB.sql(`
|
||||
drop table if exists gists cascade;
|
||||
drop index if exists gists_user_id_key;
|
||||
drop index if exists gists_pkey;
|
||||
`);
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
exports.up = DB => {
|
||||
DB.sql(`
|
||||
create table if not exists sessions (
|
||||
uid uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id integer REFERENCES users(id) not null,
|
||||
expiry timestamp without time zone DEFAULT now() + interval '1 year'
|
||||
);
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = DB => {
|
||||
DB.sql(`
|
||||
drop table if exists sessions;
|
||||
`);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "svelte.dev",
|
||||
"version": "1.0.0",
|
||||
"description": "Docs and examples for Svelte",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node scripts/update.js && npm run copy-workers && svelte-kit dev",
|
||||
"copy-workers": "node scripts/copy-workers.js",
|
||||
"migrate": "node-pg-migrate -r dotenv/config",
|
||||
"build": "node scripts/update.js && npm run copy-workers && svelte-kit build",
|
||||
"update": "node scripts/update.js --force=true",
|
||||
"preview": "svelte-kit preview",
|
||||
"start": "node build",
|
||||
"test": "uvu test",
|
||||
"deploy": "make deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie": "^0.4.0",
|
||||
"devalue": "^2.0.0",
|
||||
"do-not-zip": "^1.0.0",
|
||||
"flru": "^1.0.2",
|
||||
"httpie": "^1.1.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"marked": "^4.0.6",
|
||||
"pg": "^8.7.1",
|
||||
"prism-svelte": "^0.4.3",
|
||||
"prismjs": "^1.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sindresorhus/slugify": "^0.9.1",
|
||||
"@sveltejs/adapter-node": "next",
|
||||
"@sveltejs/kit": "next",
|
||||
"@sveltejs/site-kit": "^1.4.1",
|
||||
"@sveltejs/svelte-repl": "^0.4.1",
|
||||
"degit": "^2.1.4",
|
||||
"dotenv": "^10.0.0",
|
||||
"jimp": "^0.8.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-pg-migrate": "^6.0.0",
|
||||
"shelljs": "^0.8.3",
|
||||
"svelte": "^3.39.0",
|
||||
"uvu": "^0.5.2"
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import sh from 'shelljs';
|
||||
|
||||
sh.rm('-rf', 'static/workers');
|
||||
sh.cp('-r', 'node_modules/@sveltejs/svelte-repl/workers', 'static');
|
@ -1,67 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import puppeteer from 'puppeteer';
|
||||
import Jimp from 'jimp';
|
||||
import c from 'kleur';
|
||||
|
||||
const slugs = [];
|
||||
|
||||
fs.readdirSync(`content/examples`).forEach(group_dir => {
|
||||
fs.readdirSync(`content/examples/${group_dir}`).filter(file => file !== 'meta.json').map(example_dir => {
|
||||
const slug = example_dir.replace(/^\d+-/, '');
|
||||
slugs.push(slug);
|
||||
});
|
||||
});
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
defaultViewport: {
|
||||
width: 400 * 10 / 4,
|
||||
height: 400 + 42,
|
||||
deviceScaleFactor: 2
|
||||
}
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
for (const slug of slugs) {
|
||||
try {
|
||||
const output_file = `static/examples/thumbnails/${slug}.jpg`;
|
||||
if (fs.existsSync(output_file)) {
|
||||
console.log(c.gray(`skipping ${slug}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(slug);
|
||||
await page.goto(`http://localhost:3000/repl/embed?example=${slug}`);
|
||||
|
||||
await page.waitForSelector('iframe.inited[title=Result]');
|
||||
await page.waitFor(1500);
|
||||
const iframe = await page.$('iframe.inited[title=Result]');
|
||||
const buffer = await iframe.screenshot();
|
||||
|
||||
const image = await Jimp.read(buffer);
|
||||
console.log(image.bitmap.width, image.bitmap.height);
|
||||
image.crop(3, 3, image.bitmap.width - 6, image.bitmap.height - 6);
|
||||
image.autocrop();
|
||||
// image.scale(0.25);
|
||||
|
||||
if (image.bitmap.width > 200 || image.bitmap.height > 200) {
|
||||
const scale = Math.min(
|
||||
200 / image.bitmap.width,
|
||||
200 / image.bitmap.height
|
||||
);
|
||||
|
||||
image.scale(scale);
|
||||
}
|
||||
|
||||
await image.quality(75).write(output_file);
|
||||
} catch (err) {
|
||||
console.log(c.bold().red(`failed to screenshot ${slug}`));
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main();
|
@ -1,77 +0,0 @@
|
||||
import sander from 'sander';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(`${__dirname}/../..`);
|
||||
|
||||
function extract_frontmatter(markdown) {
|
||||
const match = /---\r?\n([\s\S]+?)\r?\n---/.exec(markdown);
|
||||
const frontMatter = match[1];
|
||||
const content = markdown.slice(match[0].length);
|
||||
|
||||
const metadata = {};
|
||||
frontMatter.split('\n').forEach(pair => {
|
||||
const colonIndex = pair.indexOf(':');
|
||||
metadata[pair.slice(0, colonIndex).trim()] = pair
|
||||
.slice(colonIndex + 1)
|
||||
.trim();
|
||||
});
|
||||
|
||||
return { metadata, content };
|
||||
}
|
||||
|
||||
const tutorial_sections = sander.readdirSync(`content/tutorial`).map(section_dir => {
|
||||
const section_slug = section_dir.replace(/^\d+-/, '');
|
||||
|
||||
const meta = JSON.parse(sander.readFileSync(`content/tutorial/${section_dir}/meta.json`, { encoding: 'utf-8' }));
|
||||
|
||||
const chapters = sander.readdirSync(`content/tutorial/${section_dir}`).map(chapter_dir => {
|
||||
const app_dir = `content/tutorial/${section_dir}/${chapter_dir}/app-b`;
|
||||
if (!sander.existsSync(app_dir)) return;
|
||||
|
||||
const markdown = sander.readFileSync(`content/tutorial/${section_dir}/${chapter_dir}/text.md`, { encoding: 'utf-8' });
|
||||
const { metadata } = extract_frontmatter(markdown);
|
||||
|
||||
const chapter_slug = chapter_dir.replace(/^\d+-/, '');
|
||||
|
||||
return {
|
||||
slug: chapter_slug,
|
||||
title: metadata.title,
|
||||
files: sander.readdirSync(app_dir).map(name => {
|
||||
return {
|
||||
name,
|
||||
source: sander.readFileSync(`${app_dir}/${name}`, { encoding: 'utf-8' })
|
||||
};
|
||||
})
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return {
|
||||
slug: section_slug,
|
||||
title: meta.title,
|
||||
chapters
|
||||
};
|
||||
}).filter(section => section.chapters.length > 0);
|
||||
|
||||
const pad = i => i < 10 ? `0${i}` : i;
|
||||
|
||||
tutorial_sections.forEach((section, i) => {
|
||||
const section_dir = `${pad(i)}-${section.slug}`;
|
||||
|
||||
sander.writeFileSync(`content/examples/${section_dir}/meta.json`, JSON.stringify({
|
||||
title: section.title
|
||||
}, null, '\t'));
|
||||
|
||||
section.chapters.forEach((chapter, i) => {
|
||||
const chapter_dir = `${pad(i)}-${chapter.slug}`;
|
||||
|
||||
sander.writeFileSync(`content/examples/${section_dir}/${chapter_dir}/meta.json`, JSON.stringify({
|
||||
title: chapter.title
|
||||
}, null, '\t'));
|
||||
|
||||
chapter.files.forEach(file => {
|
||||
sander.writeFileSync(`content/examples/${section_dir}/${chapter_dir}/${file.name}`, file.source);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,68 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import Jimp from 'jimp';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(__dirname);
|
||||
|
||||
const outputFile = `../src/routes/_contributors.js`;
|
||||
if (!force && fs.existsSync(outputFile)) {
|
||||
console.info(`[update/contributors] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const base = `https://api.github.com/repos/sveltejs/svelte/contributors`;
|
||||
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
|
||||
|
||||
const MAX = 20;
|
||||
const SIZE = 128;
|
||||
|
||||
async function main() {
|
||||
const contributors = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
const res = await fetch(`${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}&per_page=100&page=${page++}`);
|
||||
const list = await res.json();
|
||||
|
||||
if (list.length === 0) break;
|
||||
|
||||
contributors.push(...list);
|
||||
}
|
||||
|
||||
const authors = contributors
|
||||
.filter(({ login }) => !login.includes('[bot]'))
|
||||
.sort((a, b) => b.contributions - a.contributions)
|
||||
.slice(0, MAX);
|
||||
|
||||
const sprite = new Jimp(SIZE * authors.length, SIZE);
|
||||
|
||||
for (let i = 0; i < authors.length; i += 1) {
|
||||
const author = authors[i];
|
||||
console.log(`${i + 1} / ${authors.length}: ${author.login}`);
|
||||
|
||||
const image_data = await fetch(author.avatar_url);
|
||||
const buffer = await image_data.arrayBuffer();
|
||||
|
||||
const image = await Jimp.read(buffer);
|
||||
image.resize(SIZE, SIZE);
|
||||
|
||||
sprite.composite(image, i * SIZE, 0);
|
||||
}
|
||||
|
||||
await sprite.quality(80).write(`../static/contributors.jpg`);
|
||||
// TODO: Optimizing the static/contributors.jpg image should probably get automated as well
|
||||
console.log('remember to additionally optimize the resulting /static/contributors.jpg image file via e.g. https://squoosh.app ');
|
||||
|
||||
const str = `[\n\t${authors.map(a => `'${a.login}'`).join(',\n\t')}\n]`;
|
||||
|
||||
fs.writeFileSync(outputFile, `export default ${str};`);
|
||||
}
|
||||
|
||||
main();
|
@ -1,65 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
import Jimp from 'jimp';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(__dirname);
|
||||
|
||||
const outputFile = `../src/routes/_donors.js`;
|
||||
if (!force && fs.existsSync(outputFile)) {
|
||||
console.info(`[update/donors] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const MAX = 20;
|
||||
const SIZE = 128;
|
||||
|
||||
async function main() {
|
||||
const res = await fetch('https://opencollective.com/svelte/members/all.json');
|
||||
const donors = await res.json();
|
||||
|
||||
const unique = new Map();
|
||||
donors.forEach(d => unique.set(d.profile, d));
|
||||
|
||||
let backers = [...unique.values()]
|
||||
.filter(({ role }) => role === 'BACKER')
|
||||
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
|
||||
.slice(0, 3 * MAX);
|
||||
|
||||
const included = [];
|
||||
for (let i = 0; included.length < MAX; i += 1) {
|
||||
const backer = backers[i];
|
||||
console.log(`${included.length + 1} / ${MAX}: ${backer.name}`);
|
||||
|
||||
try {
|
||||
const image_data = await fetch(backer.image);
|
||||
const buffer = await image_data.arrayBuffer();
|
||||
const image = await Jimp.read(buffer);
|
||||
image.resize(SIZE, SIZE);
|
||||
included.push({ backer, image });
|
||||
} catch( err) {
|
||||
console.log(`Skipping ${backer.name}: no image data`);
|
||||
}
|
||||
}
|
||||
|
||||
const sprite = new Jimp(SIZE * included.length, SIZE);
|
||||
for (let i = 0; i < included.length; i += 1) {
|
||||
sprite.composite(included[i].image, i * SIZE, 0);
|
||||
}
|
||||
|
||||
await sprite.quality(80).write(`../static/donors.jpg`);
|
||||
// TODO: Optimizing the static/donors.jpg image should probably get automated as well
|
||||
console.log('remember to additionally optimize the resulting /static/donors.jpg image file via e.g. https://squoosh.app ');
|
||||
|
||||
const str = `[\n\t${included.map(a => `${JSON.stringify(a.backer.name)}`).join(',\n\t')}\n]`;
|
||||
|
||||
fs.writeFileSync(outputFile, `export default ${str};`);
|
||||
}
|
||||
|
||||
main();
|
@ -1,9 +0,0 @@
|
||||
import sh from 'shelljs';
|
||||
|
||||
sh.env['FORCE_UPDATE'] = process.argv.includes('--force=true');
|
||||
|
||||
Promise.all([
|
||||
sh.exec('node ./scripts/get_contributors.js'),
|
||||
sh.exec('node ./scripts/get_donors.js'),
|
||||
sh.exec('node ./scripts/update_template.js')
|
||||
]);
|
@ -1,36 +0,0 @@
|
||||
import sh from 'shelljs';
|
||||
import fs from 'fs';
|
||||
import path, { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const force = process.env.FORCE_UPDATE === 'true';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
sh.cd(path.join(__dirname, '..'));
|
||||
|
||||
const outputFile = 'static/svelte-app.json';
|
||||
if (!force && fs.existsSync(outputFile)) {
|
||||
console.info(`[update/template] ${outputFile} exists. Skipping`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// fetch svelte app
|
||||
sh.rm('-rf','scripts/svelte-app');
|
||||
sh.exec('npx degit sveltejs/template scripts/svelte-app');
|
||||
|
||||
// remove src (will be recreated client-side) and node_modules
|
||||
sh.rm('-rf', 'scripts/svelte-app/src');
|
||||
sh.rm('-rf', 'scripts/svelte-app/node_modules');
|
||||
|
||||
// build svelte-app.json
|
||||
const appPath = 'scripts/svelte-app';
|
||||
const files = [];
|
||||
|
||||
for (const path of sh.find(appPath).filter(p => fs.lstatSync(p).isFile()) ) {
|
||||
const bytes = fs.readFileSync(path);
|
||||
const string = bytes.toString();
|
||||
const data = bytes.compare(Buffer.from(string)) === 0 ? string : [...bytes];
|
||||
files.push({ path: path.slice(appPath.length + 1), data });
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputFile, JSON.stringify(files));
|
@ -1,26 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang='en' class="theme-default typo-default">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<meta name='theme-color' content='#ff3e00'>
|
||||
|
||||
<base href='/'>
|
||||
|
||||
<link rel='stylesheet' href='global.css'>
|
||||
<link rel='stylesheet' href='prism.css'>
|
||||
<link rel='manifest' href='manifest.json'>
|
||||
<link rel='icon' type='image/png' href='favicon.png'>
|
||||
|
||||
<meta name='twitter:card' content='summary_large_image'>
|
||||
<meta name='twitter:site' content='@sveltejs'>
|
||||
<meta name='twitter:creator' content='@sveltejs'>
|
||||
<meta name='twitter:image' content='https://svelte.dev/images/twitter-card.png'>
|
||||
<meta name='og:image' content='https://svelte.dev/images/twitter-card.png'>
|
||||
|
||||
%svelte.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id='svelte'>%svelte.body%</div>
|
||||
</body>
|
||||
</html>
|
@ -1,59 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let once = false;
|
||||
export let top = 0;
|
||||
export let bottom = 0;
|
||||
export let left = 0;
|
||||
export let right = 0;
|
||||
|
||||
let intersecting = false;
|
||||
let container;
|
||||
|
||||
onMount(() => {
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
intersecting = entries[0].isIntersecting;
|
||||
if (intersecting && once) {
|
||||
observer.unobserve(container);
|
||||
}
|
||||
}, {
|
||||
rootMargin
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
return () => observer.unobserve(container);
|
||||
}
|
||||
|
||||
function handler() {
|
||||
const bcr = container.getBoundingClientRect();
|
||||
|
||||
intersecting = (
|
||||
(bcr.bottom + bottom) > 0 &&
|
||||
(bcr.right + right) > 0 &&
|
||||
(bcr.top - top) < window.innerHeight &&
|
||||
(bcr.left - left) < window.innerWidth
|
||||
);
|
||||
|
||||
if (intersecting && once) {
|
||||
window.removeEventListener('scroll', handler);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handler);
|
||||
return () => window.removeEventListener('scroll', handler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div bind:this={container}>
|
||||
<slot {intersecting}></slot>
|
||||
</div>
|
@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let constructor;
|
||||
|
||||
onMount(async () => {
|
||||
constructor = await $$props.this();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:component this={constructor} {...$$props}/>
|
@ -1,63 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let p = 0;
|
||||
let visible = false;
|
||||
|
||||
onMount(() => {
|
||||
function next() {
|
||||
visible = true;
|
||||
p += 0.1;
|
||||
|
||||
const remaining = 1 - p;
|
||||
if (remaining > 0.15) setTimeout(next, 500 / remaining);
|
||||
}
|
||||
|
||||
setTimeout(next, 250);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.progress-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: var(--prime);
|
||||
transition: width 0.4s;
|
||||
}
|
||||
|
||||
.fade {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
pointer-events: none;
|
||||
z-index: 998;
|
||||
animation: fade 0.4s;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
from { opacity: 0 }
|
||||
to { opacity: 1 }
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if visible}
|
||||
<div class="progress-container">
|
||||
<div class="progress" style="width: {p * 100}%"></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if p >= 0.4}
|
||||
<div class="fade"></div>
|
||||
{/if}
|
@ -1,27 +0,0 @@
|
||||
<script>
|
||||
export let checked;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.input-output-toggle {
|
||||
display: grid;
|
||||
user-select: none;
|
||||
flex: 0;
|
||||
grid-template-columns: 1fr 40px 1fr;
|
||||
grid-gap: 0.5em;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
border-top: 1px solid var(--second);
|
||||
}
|
||||
|
||||
input { display: block }
|
||||
span { color: #ccc }
|
||||
.active { color: #555 }
|
||||
</style>
|
||||
|
||||
<label class="input-output-toggle">
|
||||
<span class:active={!checked} style="text-align: right">input</span>
|
||||
<input type="checkbox" bind:checked>
|
||||
<span class:active={checked}>output</span>
|
||||
</label>
|
@ -1,136 +0,0 @@
|
||||
<script>
|
||||
import Repl from '@sveltejs/svelte-repl';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import { process_example } from '../../utils/examples';
|
||||
import InputOutputToggle from './InputOutputToggle.svelte';
|
||||
|
||||
export let version = '3';
|
||||
export let gist = null;
|
||||
export let example = null;
|
||||
export let embedded = false;
|
||||
|
||||
let repl;
|
||||
let name = 'loading...';
|
||||
let width = browser
|
||||
? window.innerWidth - 32
|
||||
: 1000;
|
||||
|
||||
let checked = false;
|
||||
|
||||
onMount(() => {
|
||||
if (version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${version}/package.json`)
|
||||
.then(r => r.json())
|
||||
.then(pkg => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
|
||||
if (gist) {
|
||||
fetch(`repl/${gist}.json`).then(r => r.json()).then(data => {
|
||||
const { description, files } = data;
|
||||
|
||||
name = description;
|
||||
|
||||
const components = Object.keys(files)
|
||||
.map(file => {
|
||||
const dot = file.lastIndexOf('.');
|
||||
if (!~dot) return;
|
||||
|
||||
const source = files[file].content;
|
||||
|
||||
return {
|
||||
name: file.slice(0, dot),
|
||||
type: file.slice(dot + 1),
|
||||
source
|
||||
};
|
||||
})
|
||||
.filter(x => x.type === 'svelte' || x.type === 'js')
|
||||
.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;
|
||||
});
|
||||
|
||||
repl.set({ components });
|
||||
});
|
||||
} else if (example) {
|
||||
fetch(`examples/${example}.json`).then(async response => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
repl.set({
|
||||
components: process_example(data.files)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$: if (embedded) document.title = `${name} • Svelte REPL`;
|
||||
|
||||
$: svelteUrl = browser && version === 'local' ?
|
||||
`${location.origin}/repl/local` :
|
||||
`https://unpkg.com/svelte@${version}`;
|
||||
|
||||
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
|
||||
|
||||
$: mobile = width < 560;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--back);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
--pane-controls-h: 4.2rem;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile .viewport {
|
||||
width: 200%;
|
||||
height: calc(100% - 42px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.mobile .offset {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="repl-outer" bind:clientWidth={width} class:mobile>
|
||||
<div class="viewport" class:offset={checked}>
|
||||
{#if browser}
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
workersUrl="workers"
|
||||
fixed={mobile}
|
||||
{svelteUrl}
|
||||
{rollupUrl}
|
||||
embedded
|
||||
relaxed
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if mobile}
|
||||
<InputOutputToggle bind:checked/>
|
||||
{/if}
|
||||
</div>
|
@ -1,46 +0,0 @@
|
||||
<script>
|
||||
export let labels;
|
||||
export let offset = 0;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.toggle {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 4.6rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--second);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 .15em;
|
||||
width: 4em;
|
||||
height: 1em;
|
||||
padding: .3em .4em;
|
||||
border-radius: var(--border-r);
|
||||
line-height: 1em;
|
||||
box-sizing: content-box;
|
||||
color: #888;
|
||||
border: 1px solid var(--back-light);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--prime);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="toggle">
|
||||
{#each labels as label, index}
|
||||
<button
|
||||
class:selected={offset === index}
|
||||
on:click={() => offset = index}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
@ -1,5 +0,0 @@
|
||||
// REPL props
|
||||
|
||||
export const svelteUrl = `https://unpkg.com/svelte@3`;
|
||||
export const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
|
||||
export const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = '${import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}';`;
|
@ -1,31 +0,0 @@
|
||||
// need to do this first before importing database, etc.
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import * as cookie from 'cookie';
|
||||
import { get_user, sanitize_user } from './utils/auth';
|
||||
import { query } from './utils/db';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ request, resolve }) {
|
||||
if (process.env['PGHOST']) {
|
||||
// this is a convenient time to clear out expired sessions
|
||||
query('delete from sessions where expiry < now()');
|
||||
|
||||
request.locals.cookies = cookie.parse(request.headers.cookie || '');
|
||||
request.locals.user = await get_user(request.locals.cookies.sid);
|
||||
}
|
||||
|
||||
const response = await resolve(request);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/** @type {import('@sveltejs/kit').GetSession} */
|
||||
export function getSession(request) {
|
||||
return request.locals.user
|
||||
? {
|
||||
user: sanitize_user(request.locals.user)
|
||||
}
|
||||
: {};
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
<script context="module">
|
||||
/** @type {import('@sveltejs/kit').ErrorLoad} */
|
||||
export function load({ error, status }) {
|
||||
return {
|
||||
props: { error, status }
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let status;
|
||||
export let error;
|
||||
|
||||
// we don't want to use <svelte:window bind:online> here,
|
||||
// because we only care about the online state when
|
||||
// the page first loads
|
||||
const online = typeof navigator !== 'undefined'
|
||||
? navigator.onLine
|
||||
: true;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
|
||||
}
|
||||
|
||||
h1, p { margin: 0 auto }
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
font-weight: 300;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
p { margin: 1em auto }
|
||||
|
||||
.error {
|
||||
background-color: var(--second);
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
font: 600 16px/1.7 var(--font);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* @media (min-width: 480px) {
|
||||
h1 { font-size: 4em }
|
||||
} */
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
{#if online}
|
||||
<h1>Yikes!</h1>
|
||||
|
||||
{#if error.message}
|
||||
<p class="error">{status}: {error.message}</p>
|
||||
{:else}
|
||||
<p class="error">Encountered a {status} error</p>
|
||||
{/if}
|
||||
|
||||
{#if import.meta.env.DEV && error.stack}
|
||||
<pre>{error.stack}</pre>
|
||||
{:else}
|
||||
{#if status >= 500}
|
||||
<p>Please try reloading the page.</p>
|
||||
{/if}
|
||||
|
||||
<p>If the error persists, please drop by the <a rel="external" href="chat">Discord chatroom</a> and let us know, or raise an issue on <a href="https://github.com/sveltejs/svelte">GitHub</a>. Thanks!</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<h1>It looks like you're offline</h1>
|
||||
|
||||
<p>Reload the page once you've found the internet.</p>
|
||||
{/if}
|
||||
</div>
|
@ -1,70 +0,0 @@
|
||||
<script>
|
||||
import '@sveltejs/site-kit/base.css';
|
||||
import { setContext } from 'svelte';
|
||||
import { page, navigating, session } from '$app/stores';
|
||||
import { Icon, Icons, Nav, NavItem } from '@sveltejs/site-kit';
|
||||
import PreloadingIndicator from '../components/PreloadingIndicator.svelte';
|
||||
|
||||
export let segment;
|
||||
|
||||
setContext('app', {
|
||||
login: () => {
|
||||
const login_window = window.open(`${window.location.origin}/auth/login`, 'login', 'width=600,height=400');
|
||||
|
||||
window.addEventListener('message', function handler(event) {
|
||||
login_window.close();
|
||||
window.removeEventListener('message', handler);
|
||||
$session.user = event.data.user;
|
||||
});
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
const r = await fetch(`/auth/logout`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (r.ok) $session.user = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Icons/>
|
||||
|
||||
{#if $navigating && $navigating.to}
|
||||
<PreloadingIndicator/>
|
||||
{/if}
|
||||
|
||||
{#if $page.path !== '/repl/embed'}
|
||||
<Nav {segment} {page} logo="svelte-logo-horizontal.svg">
|
||||
<NavItem segment="tutorial">Tutorial</NavItem>
|
||||
<NavItem segment="docs">Docs</NavItem>
|
||||
<NavItem segment="examples">Examples</NavItem>
|
||||
<NavItem segment="repl">REPL</NavItem>
|
||||
<NavItem segment="blog">Blog</NavItem>
|
||||
<NavItem segment="faq">FAQ</NavItem>
|
||||
|
||||
<NavItem external="https://kit.svelte.dev">SvelteKit</NavItem>
|
||||
|
||||
<NavItem external="chat" title="Discord Chat">
|
||||
<Icon name="message-square"/>
|
||||
</NavItem>
|
||||
|
||||
<NavItem external="https://github.com/sveltejs/svelte" title="GitHub Repo">
|
||||
<Icon name="github"/>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
{/if}
|
||||
|
||||
<main>
|
||||
<slot></slot>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
/* padding: var(--nav-h) var(--side-nav) 0 var(--side-nav); */
|
||||
padding: var(--nav-h) 0 0 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
@ -1,34 +0,0 @@
|
||||
<script>
|
||||
import contributors from '../_contributors.js';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#contributors {
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.contributor {
|
||||
width: 4.2em;
|
||||
height: 4.2em;
|
||||
border-radius: 50%;
|
||||
text-indent: -9999px;
|
||||
display: inline-block;
|
||||
background: no-repeat url(/contributors.jpg);
|
||||
background-size: auto 102%;
|
||||
margin: 0 1.5em 1.5em 0;
|
||||
border: 2px solid var(--second);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="contributors">
|
||||
{#each contributors as contributor, i}
|
||||
<a
|
||||
class="contributor"
|
||||
style="background-position: {(100 * i) / (contributors.length - 1)}% 0"
|
||||
href="https://github.com/{contributor}">
|
||||
{contributor}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p>And so <a href="https://github.com/sveltejs/svelte/graphs/contributors">many more →</a></p>
|
@ -1,35 +0,0 @@
|
||||
<script>
|
||||
import donors from '../_donors.js';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#donors {
|
||||
margin: 0.4em 0;
|
||||
}
|
||||
|
||||
.donor {
|
||||
width: 4.2em;
|
||||
height: 4.2em;
|
||||
border-radius: 50%;
|
||||
text-indent: -9999px;
|
||||
display: inline-block;
|
||||
background: no-repeat url(/donors.jpg);
|
||||
background-size: auto 102%;
|
||||
margin: 0 1.5em 1.5em 0;
|
||||
border: 2px solid var(--second);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="donors">
|
||||
{#each donors as donor, i}
|
||||
<a
|
||||
class="donor"
|
||||
style="background-position: {(100 * i) / (donors.length - 1)}% 0"
|
||||
alt="{donor}"
|
||||
href="https://opencollective.com/svelte">
|
||||
{donor}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p>And so <a href="https://opencollective.com/svelte">many more →</a></p>
|
@ -1,52 +0,0 @@
|
||||
<script>
|
||||
import { Section } from '@sveltejs/site-kit';
|
||||
import IntersectionObserver from '../../components/IntersectionObserver.svelte';
|
||||
import ReplWidget from '../../components/Repl/ReplWidget.svelte';
|
||||
|
||||
export let id;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.example {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example :global(a) {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.example > :global(p) {
|
||||
margin: 4.4rem 2.4rem 2.4rem 0;
|
||||
}
|
||||
|
||||
.repl-container {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border-radius: var(--border-r);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 920px) {
|
||||
.example {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-gap: 0.5em;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<Section>
|
||||
<div class="example">
|
||||
<slot></slot>
|
||||
|
||||
<div class="repl-container">
|
||||
<IntersectionObserver once let:intersecting top={400}>
|
||||
{#if intersecting}
|
||||
<!-- <Lazy this={loadReplWidget} example={id}/> -->
|
||||
<ReplWidget example={id}/>
|
||||
{/if}
|
||||
</IntersectionObserver>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
@ -1,80 +0,0 @@
|
||||
export const companies = [
|
||||
{
|
||||
href: "https://1password.com",
|
||||
filename: "1password.svg",
|
||||
alt: "1Password logo",
|
||||
},
|
||||
{
|
||||
href: "https://www.alaskaair.com/",
|
||||
style: "background-color: #01426a;",
|
||||
filename: "alaskaairlines.svg",
|
||||
alt: "Alaska Airlines logo",
|
||||
},
|
||||
{
|
||||
href: "https://avast.com",
|
||||
filename: "avast.svg",
|
||||
alt: "Avast logo",
|
||||
},
|
||||
{
|
||||
href: "https://chess.com",
|
||||
style: "background-color: #312e2b;",
|
||||
filename: "chess.svg",
|
||||
alt: "Chess.com logo",
|
||||
},
|
||||
{
|
||||
href: "https://fusioncharts.com",
|
||||
filename: "fusioncharts.svg",
|
||||
alt: "FusionCharts logo",
|
||||
},
|
||||
{
|
||||
href: "https://godaddy.com",
|
||||
filename: "godaddy.svg",
|
||||
alt: "GoDaddy logo",
|
||||
},
|
||||
{
|
||||
href: "https://www.ibm.com/",
|
||||
filename: "ibm.svg",
|
||||
alt: "IBM logo",
|
||||
},
|
||||
{
|
||||
href: "https://media.lesechos.fr/infographie",
|
||||
filename: "les-echos.svg",
|
||||
alt: "Les Echos",
|
||||
},
|
||||
{
|
||||
href: "https://www.philips.co.uk",
|
||||
filename: "philips.svg",
|
||||
alt: "Philips logo",
|
||||
},
|
||||
{
|
||||
href: "https://global.rakuten.com/corp/",
|
||||
filename: "rakuten.svg",
|
||||
alt: "Rakuten logo",
|
||||
},
|
||||
{
|
||||
href: "https://razorpay.com",
|
||||
filename: "razorpay.svg",
|
||||
alt: "Razorpay logo",
|
||||
},
|
||||
{
|
||||
href: "https://www.se.com",
|
||||
style: " background-color: #3dcd58; ",
|
||||
filename: "Schneider_Electric.svg",
|
||||
alt: "Schneider Electric",
|
||||
},
|
||||
{
|
||||
href: "https://squareup.com",
|
||||
filename: "square.svg",
|
||||
alt: "Square",
|
||||
},
|
||||
{
|
||||
href: "https://nytimes.com",
|
||||
filename: "nyt.svg",
|
||||
alt: "The New York Times logo",
|
||||
},
|
||||
{
|
||||
href: "https://transloadit.com",
|
||||
filename: "transloadit.svg",
|
||||
alt: "Transloadit",
|
||||
},
|
||||
];
|
@ -1,66 +0,0 @@
|
||||
<script>
|
||||
import { companies } from './WhosUsingSvelte.js';
|
||||
const randomizer = ({ prominent }) => Math.random();
|
||||
const doSort = (a, b) => randomizer(b) - randomizer(a);
|
||||
const sortedCompanies = companies.sort(doSort);
|
||||
</script>
|
||||
|
||||
<div class="logos">
|
||||
{#each sortedCompanies as { href, filename, alt, style, picture, span }, index}
|
||||
<a target="_blank" rel="noopener" {href} style={style || ""}>
|
||||
{#if picture}
|
||||
<picture>
|
||||
{#each picture as { type, srcset }}
|
||||
<source {type} {srcset} />
|
||||
{/each}
|
||||
<img src="/whos-using-svelte/{filename}" {alt} loading="lazy" />
|
||||
</picture>
|
||||
{:else}
|
||||
<img src="/whos-using-svelte/{filename}" {alt} loading="lazy" />
|
||||
{#if span}
|
||||
<span>{span}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.logos {
|
||||
margin: 1em 0 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
a {
|
||||
height: 40px;
|
||||
margin: 0 0.5em 0.5em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px solid var(--second);
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
picture,
|
||||
img {
|
||||
height: 100%;
|
||||
padding: 5px 10px;
|
||||
transition: transform 0.2s;
|
||||
min-width: 0; /* Avoid image overflow in Safari */
|
||||
}
|
||||
picture:hover,
|
||||
img:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
@media (min-width: 540px) {
|
||||
a {
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
picture,
|
||||
img {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,25 +0,0 @@
|
||||
import { query as db_query } from '../../utils/db';
|
||||
|
||||
export async function get({ query, locals }) {
|
||||
if (locals.user) {
|
||||
const page_size = 100;
|
||||
const offset = query.get('offset') ? parseInt(query.get('offset')) : 0;
|
||||
const rows = await db_query(`
|
||||
select g.uid, g.name, coalesce(g.updated_at, g.created_at) as updated_at
|
||||
from gists g
|
||||
where g.user_id = $1
|
||||
order by id desc
|
||||
limit ${page_size + 1}
|
||||
offset $2
|
||||
`, [locals.user.id, offset]);
|
||||
|
||||
rows.forEach(row => {
|
||||
row.uid = row.uid.replace(/-/g, '');
|
||||
});
|
||||
|
||||
const more = rows.length > page_size;
|
||||
return { body: { apps: rows.slice(0, page_size), offset: more ? offset + page_size : null }};
|
||||
} else {
|
||||
return { status: 401 };
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export const oauth = 'https://github.com/login/oauth';
|
||||
export const baseurl = process.env['BASEURL'];
|
||||
export const secure = baseurl && baseurl.startsWith('https:');
|
||||
|
||||
export const client_id = process.env['GITHUB_CLIENT_ID'];
|
||||
export const client_secret = process.env['GITHUB_CLIENT_SECRET'];
|
@ -1,54 +0,0 @@
|
||||
import devalue from 'devalue';
|
||||
import * as cookie from 'cookie';
|
||||
import * as httpie from 'httpie';
|
||||
import { parse, stringify } from 'querystring';
|
||||
import { sanitize_user, create_user, create_session } from '../../utils/auth';
|
||||
import { oauth, secure, client_id, client_secret } from './_config.js';
|
||||
|
||||
export async function get({ query }) {
|
||||
try {
|
||||
// Trade "code" for "access_token"
|
||||
const r1 = await httpie.post(`${oauth}/access_token?` + stringify({
|
||||
code: query.get('code'),
|
||||
client_id,
|
||||
client_secret,
|
||||
}));
|
||||
|
||||
// Now fetch User details
|
||||
const { access_token } = parse(r1.data);
|
||||
const r2 = await httpie.get('https://api.github.com/user', {
|
||||
headers: {
|
||||
'User-Agent': 'svelte.dev',
|
||||
Authorization: `token ${access_token}`
|
||||
}
|
||||
});
|
||||
|
||||
const user = await create_user(r2.data, access_token);
|
||||
const session = await create_session(user);
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('sid', session.uid, {
|
||||
maxAge: 31536000,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure
|
||||
}),
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
},
|
||||
body: `
|
||||
<script>
|
||||
window.opener.postMessage({
|
||||
user: ${devalue(sanitize_user(user))}
|
||||
}, window.location.origin);
|
||||
</script>
|
||||
`
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('GET /auth/callback', err);
|
||||
return {
|
||||
status: 500,
|
||||
body: err.data
|
||||
};
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { stringify } from 'querystring';
|
||||
import { oauth, baseurl, client_id } from './_config.js';
|
||||
|
||||
export const get = client_id
|
||||
? () => {
|
||||
const Location = `${oauth}/authorize?` + stringify({
|
||||
scope: 'read:user',
|
||||
client_id,
|
||||
redirect_uri: `${baseurl}/auth/callback`,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location
|
||||
}
|
||||
};
|
||||
}
|
||||
: () => {
|
||||
return {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8'
|
||||
},
|
||||
body: `
|
||||
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
|
||||
<h1>Missing .env file</h1>
|
||||
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
|
||||
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
|
||||
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
|
||||
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
|
||||
</body>
|
||||
`
|
||||
};
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import * as cookie from 'cookie';
|
||||
import { secure } from './_config.js';
|
||||
import { delete_session } from '../../utils/auth.js';
|
||||
|
||||
export async function get(request) {
|
||||
await delete_session(request.locals.cookies.sid);
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Set-Cookie': cookie.serialize('sid', '', {
|
||||
maxAge: -1,
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import get_posts from './_posts.js';
|
||||
|
||||
let lookup;
|
||||
|
||||
export function get({ params }) {
|
||||
if (!lookup || process.env.NODE_ENV !== 'production') {
|
||||
lookup = new Map();
|
||||
get_posts().forEach(post => {
|
||||
lookup.set(post.slug, post);
|
||||
});
|
||||
}
|
||||
|
||||
const post = lookup.get(params.slug);
|
||||
|
||||
if (post) {
|
||||
return {
|
||||
body: post,
|
||||
headers: {
|
||||
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
<script context="module">
|
||||
export async function load({ fetch, page: { params } }) {
|
||||
const res = await fetch(`/blog/${params.slug}.json`);
|
||||
if (res.ok) {
|
||||
return { props: { post: await res.json() }};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export let post;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{post.metadata.title}</title>
|
||||
|
||||
<meta name="twitter:title" content={post.metadata.title}>
|
||||
<meta name="twitter:description" content={post.metadata.description}>
|
||||
<meta name="Description" content={post.metadata.description}>
|
||||
</svelte:head>
|
||||
|
||||
<article class='post listify'>
|
||||
<h1>{post.metadata.title}</h1>
|
||||
<p class='standfirst'>{post.metadata.description}</p>
|
||||
|
||||
<p class='byline'><a href='{post.metadata.authorURL}'>{post.metadata.author}</a> <time datetime='{post.metadata.pubdate}'>{post.metadata.dateString}</time></p>
|
||||
|
||||
{@html post.html}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.post {
|
||||
padding: var(--top-offset) var(--side-nav) 6rem var(--side-nav);
|
||||
max-width: var(--main-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
font-size: var(--h4);
|
||||
color: var(--second);
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.byline {
|
||||
margin: 0 0 6rem 0;
|
||||
padding: 1.6rem 0 0 0;
|
||||
border-top: var(--border-w) solid #6767785b;
|
||||
font-size: var(--h6);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.byline a {
|
||||
/* border-bottom: none; */
|
||||
/* font-weight: 600; */
|
||||
}
|
||||
|
||||
.byline a:hover {
|
||||
/* border-bottom: 2px solid var(--prime); */
|
||||
}
|
||||
|
||||
.post h1 {
|
||||
color: var(--second);
|
||||
max-width: 20em;
|
||||
margin: 0 0 .8rem 0;
|
||||
}
|
||||
|
||||
.post :global(h2) {
|
||||
margin: 2em 0 0.5em 0;
|
||||
/* color: var(--second); */
|
||||
color: var(--text);
|
||||
font-size: var(--h3);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.post :global(figure) {
|
||||
margin: 1.6rem 0 3.2rem 0;
|
||||
}
|
||||
|
||||
.post :global(figure) :global(img) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.post :global(figcaption) {
|
||||
color: var(--second);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post :global(video) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(blockquote) {
|
||||
max-width: none;
|
||||
border-left: 4px solid #eee;
|
||||
background: #f9f9f9;
|
||||
border-radius: 0 var(--border-r) var(--border-r) 0;
|
||||
}
|
||||
|
||||
.post :global(code) {
|
||||
padding: .3rem .8rem .3rem;
|
||||
margin: 0 0.2rem;
|
||||
top: -.1rem;
|
||||
background: var(--back-api);
|
||||
}
|
||||
|
||||
.post :global(pre) :global(code) {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
top: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.post :global(aside) {
|
||||
float: right;
|
||||
margin: 0 0 1em 1em;
|
||||
width: 16rem;
|
||||
color: var(--second);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.post :global(.max) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post :global(iframe) {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
margin: 2em 0;
|
||||
border-radius: var(--border-r);
|
||||
border: 0.8rem solid var(--second);
|
||||
}
|
||||
|
||||
.post :global(.anchor) {
|
||||
top: calc((var(--h3) - 24px) / 2);
|
||||
}
|
||||
|
||||
.post :global(a) {
|
||||
padding: 0;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.post :global(a):not(:hover) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.post :global(.anchor) {
|
||||
transform: scale(0.6);
|
||||
opacity: 1;
|
||||
left: -1.0em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 910px) {
|
||||
.post :global(.max) {
|
||||
width: calc(100vw - 2 * var(--side-nav));
|
||||
margin: 0 calc(var(--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,59 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { extract_frontmatter, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
|
||||
import { marked } from 'marked';
|
||||
import { makeSlugProcessor } from '../../utils/slug';
|
||||
import { highlight } from '../../utils/highlight';
|
||||
import { SLUG_PRESERVE_UNICODE } from '../../../config';
|
||||
|
||||
const makeSlug = makeSlugProcessor(SLUG_PRESERVE_UNICODE);
|
||||
|
||||
export default function get_posts() {
|
||||
return fs
|
||||
.readdirSync('content/blog')
|
||||
.map(file => {
|
||||
if (path.extname(file) !== '.md') return;
|
||||
|
||||
const match = /^(\d+-\d+-\d+)-(.+)\.md$/.exec(file);
|
||||
if (!match) throw new Error(`Invalid filename '${file}'`);
|
||||
|
||||
const [, pubdate, slug] = match;
|
||||
|
||||
const markdown = fs.readFileSync(`content/blog/${file}`, 'utf-8');
|
||||
|
||||
const { content, metadata } = extract_frontmatter(markdown);
|
||||
|
||||
const date = new Date(`${pubdate} EDT`); // cheeky hack
|
||||
metadata.pubdate = pubdate;
|
||||
metadata.dateString = date.toDateString();
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.link = link_renderer;
|
||||
|
||||
renderer.code = highlight;
|
||||
|
||||
renderer.heading = (text, level, rawtext) => {
|
||||
const fragment = makeSlug(rawtext);
|
||||
|
||||
return `
|
||||
<h${level}>
|
||||
<span id="${fragment}" class="offset-anchor"></span>
|
||||
<a href="blog/${slug}#${fragment}" class="anchor" aria-hidden="true"></a>
|
||||
${text}
|
||||
</h${level}>`;
|
||||
};
|
||||
|
||||
const html = marked(
|
||||
content.replace(/^\t+/gm, match => match.split('\t').join(' ')),
|
||||
{ renderer }
|
||||
);
|
||||
|
||||
return {
|
||||
html,
|
||||
metadata,
|
||||
slug
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.metadata.pubdate < b.metadata.pubdate ? 1 : -1);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import get_posts from './_posts.js';
|
||||
|
||||
let json;
|
||||
|
||||
export function get() {
|
||||
if (!json || process.env.NODE_ENV !== 'production') {
|
||||
const posts = get_posts()
|
||||
.filter(post => !post.metadata.draft)
|
||||
.map(post => {
|
||||
return {
|
||||
slug: post.slug,
|
||||
metadata: post.metadata
|
||||
};
|
||||
});
|
||||
|
||||
json = JSON.stringify(posts);
|
||||
}
|
||||
|
||||
return {
|
||||
body: json,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import get_posts from '../blog/_posts.js';
|
||||
|
||||
const months = ',Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(',');
|
||||
|
||||
function formatPubdate(str) {
|
||||
const [y, m, d] = str.split('-');
|
||||
return `${d} ${months[+m]} ${y} 12:00 +0000`;
|
||||
}
|
||||
|
||||
function escapeHTML(html) {
|
||||
const chars = {
|
||||
'"' : 'quot',
|
||||
"'": '#39',
|
||||
'&': 'amp',
|
||||
'<' : 'lt',
|
||||
'>' : 'gt'
|
||||
};
|
||||
|
||||
return html.replace(/["'&<>]/g, c => `&${chars[c]};`);
|
||||
}
|
||||
|
||||
const rss = `
|
||||
<?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>
|
||||
${get_posts().filter(post => !post.metadata.draft).map(post => `
|
||||
<item>
|
||||
<title>${escapeHTML(post.metadata.title)}</title>
|
||||
<link>https://svelte.dev/blog/${post.slug}</link>
|
||||
<description>${escapeHTML(post.metadata.description)}</description>
|
||||
<pubDate>${formatPubdate(post.metadata.pubdate)}</pubDate>
|
||||
</item>
|
||||
`).join('')}
|
||||
</channel>
|
||||
|
||||
</rss>
|
||||
`.replace(/>[^\S]+/gm, '>').replace(/[^\S]+</gm, '<').trim();
|
||||
|
||||
export function get() {
|
||||
return {
|
||||
body: rss,
|
||||
headers: {
|
||||
'Cache-Control': `max-age=${30 * 60 * 1e3}`,
|
||||
'Content-Type': 'application/rss+xml'
|
||||
}
|
||||
};
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export function get() {
|
||||
return {
|
||||
status: 302,
|
||||
headers: { Location: 'https://discord.gg/yy75DKs' },
|
||||
};
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { SLUG_PRESERVE_UNICODE, SLUG_SEPARATOR } from '../../../config';
|
||||
import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
|
||||
import { make_session_slug_processor } from '@sveltejs/site-kit/utils/slug';
|
||||
import { highlight } from '../../utils/highlight';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const blockTypes = [
|
||||
'blockquote',
|
||||
'html',
|
||||
'heading',
|
||||
'hr',
|
||||
'list',
|
||||
'listitem',
|
||||
'paragraph',
|
||||
'table',
|
||||
'tablerow',
|
||||
'tablecell'
|
||||
];
|
||||
|
||||
export default function() {
|
||||
const make_slug = make_session_slug_processor({
|
||||
preserve_unicode: SLUG_PRESERVE_UNICODE,
|
||||
separator: SLUG_SEPARATOR
|
||||
});
|
||||
|
||||
return fs
|
||||
.readdirSync(`content/docs`)
|
||||
.filter(file => file[0] !== '.' && path.extname(file) === '.md')
|
||||
.map(file => {
|
||||
const markdown = fs.readFileSync(`content/docs/${file}`, 'utf-8');
|
||||
|
||||
const { content, metadata } = extract_frontmatter(markdown);
|
||||
|
||||
const section_slug = make_slug(metadata.title);
|
||||
|
||||
const subsections = [];
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
let block_open = false;
|
||||
|
||||
renderer.link = link_renderer;
|
||||
|
||||
renderer.hr = () => {
|
||||
block_open = true;
|
||||
|
||||
return '<div class="side-by-side"><div class="copy">';
|
||||
};
|
||||
|
||||
renderer.code = (source, lang) => {
|
||||
source = source.replace(/^ +/gm, match =>
|
||||
match.split(' ').join('\t')
|
||||
);
|
||||
|
||||
const lines = source.split('\n');
|
||||
|
||||
const meta = extract_metadata(lines[0], lang);
|
||||
|
||||
let prefix = '';
|
||||
let className = 'code-block';
|
||||
|
||||
if (meta) {
|
||||
source = lines.slice(1).join('\n');
|
||||
const filename = meta.filename || (lang === 'html' && 'App.svelte');
|
||||
if (filename) {
|
||||
prefix = `<span class='filename'>${prefix} ${filename}</span>`;
|
||||
className += ' named';
|
||||
}
|
||||
}
|
||||
|
||||
if (meta && meta.hidden) return '';
|
||||
|
||||
const html = `<div class='${className}'>${prefix}${highlight(source, lang)}</div>`;
|
||||
|
||||
if (block_open) {
|
||||
block_open = false;
|
||||
return `</div><div class="code">${html}</div></div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
renderer.heading = (text, level, rawtext) => {
|
||||
let slug;
|
||||
|
||||
const match = /<a href="([^"]+)"[^>]*>(.+)<\/a>/.exec(text);
|
||||
if (match) {
|
||||
slug = match[1];
|
||||
text = match[2];
|
||||
} else {
|
||||
slug = make_slug(rawtext);
|
||||
}
|
||||
|
||||
if (level === 3 || level === 4) {
|
||||
const title = text
|
||||
.replace(/<\/?code>/g, '')
|
||||
.replace(/\.(\w+)(\((.+)?\))?/, (m, $1, $2, $3) => {
|
||||
if ($3) return `.${$1}(...)`;
|
||||
if ($2) return `.${$1}()`;
|
||||
return `.${$1}`;
|
||||
});
|
||||
|
||||
subsections.push({ slug, title, level });
|
||||
}
|
||||
|
||||
return `
|
||||
<h${level}>
|
||||
<span id="${slug}" class="offset-anchor" ${level > 4 ? 'data-scrollignore' : ''}></span>
|
||||
<a href="docs#${slug}" class="anchor" aria-hidden="true"></a>
|
||||
${text}
|
||||
</h${level}>`;
|
||||
};
|
||||
|
||||
blockTypes.forEach(type => {
|
||||
const fn = renderer[type];
|
||||
renderer[type] = function() {
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
const html = marked(content, { renderer });
|
||||
|
||||
const hashes = {};
|
||||
|
||||
return {
|
||||
html: html.replace(/@@(\d+)/g, (m, id) => hashes[id] || m),
|
||||
metadata,
|
||||
subsections,
|
||||
slug: section_slug,
|
||||
file,
|
||||
};
|
||||
});
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import get_sections from './_sections.js';
|
||||
|
||||
let json;
|
||||
|
||||
export function get() {
|
||||
if (!json || process.env.NODE_ENV !== 'production') {
|
||||
json = get_sections();
|
||||
}
|
||||
|
||||
return {
|
||||
body: json
|
||||
};
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
<script context="module">
|
||||
const title_replacements = {
|
||||
'1_export_creates_a_component_prop': 'props',
|
||||
'2_Assignments_are_reactive': 'reactive assignments',
|
||||
'3_$_marks_a_statement_as_reactive': 'reactive statements ($:)',
|
||||
'4_Prefix_stores_with_$_to_access_their_values': 'accessing stores ($)'
|
||||
};
|
||||
|
||||
export async function load({ fetch }) {
|
||||
const sections = await fetch(`docs.json`).then(r => r.json());
|
||||
for (const section of sections) {
|
||||
for (const subsection of section.subsections) {
|
||||
const { slug } = subsection;
|
||||
// show abbreviated title in the table of contents
|
||||
if (slug in title_replacements) {
|
||||
subsection.title = title_replacements[slug];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
sections
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { Docs } from '@sveltejs/site-kit';
|
||||
|
||||
export let sections;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>API Docs • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte API docs">
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps">
|
||||
<meta name="Description" content="Cybernetically enhanced web apps">
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="visually-hidden">API Docs</h1>
|
||||
<Docs {sections}/>
|
@ -1,20 +0,0 @@
|
||||
import { get_example } from './_examples.js';
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
export function get({ params }) {
|
||||
const { slug } = params;
|
||||
|
||||
let example = cache.get(slug);
|
||||
|
||||
if (!example || process.env.NODE_ENV !== 'production') {
|
||||
example = get_example(slug);
|
||||
if (example) cache.set(slug, example);
|
||||
}
|
||||
|
||||
if (example) {
|
||||
return {
|
||||
body: example
|
||||
};
|
||||
}
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
<script>
|
||||
export let sections = [];
|
||||
export let active_section = null;
|
||||
export let isLoading = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.examples-toc {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--second);
|
||||
background-color: var(--second);
|
||||
color: white;
|
||||
padding: 3rem 3rem 0 3rem;
|
||||
}
|
||||
|
||||
.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(--h6) var(--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 {
|
||||
background: rgba(0, 0, 0, 0.15) calc(100% - 3rem) 47% no-repeat
|
||||
url(/icons/arrow-right.svg);
|
||||
background-size: 1em 1em;
|
||||
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(--sidebar-text);
|
||||
border-bottom: none;
|
||||
font-size: 1.6rem;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.repl-link {
|
||||
flex: 0 1 auto;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-right: 2.5rem;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
background-color: white;
|
||||
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>
|
||||
|
||||
<ul class="examples-toc">
|
||||
{#each sections as section}
|
||||
<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 href="repl/{example.slug}" class="repl-link">REPL</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
@ -1,50 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
let lookup;
|
||||
const titles = new Map();
|
||||
|
||||
export function get_examples() {
|
||||
lookup = new Map();
|
||||
|
||||
return fs.readdirSync(`content/examples`).map(group_dir => {
|
||||
const metadata = JSON.parse(fs.readFileSync(`content/examples/${group_dir}/meta.json`, 'utf-8'));
|
||||
|
||||
return {
|
||||
title: metadata.title,
|
||||
examples: fs.readdirSync(`content/examples/${group_dir}`).filter(file => file !== 'meta.json').map(example_dir => {
|
||||
const slug = example_dir.replace(/^\d+-/, '');
|
||||
|
||||
if (lookup.has(slug)) throw new Error(`Duplicate example slug "${slug}"`);
|
||||
lookup.set(slug, `${group_dir}/${example_dir}`);
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(`content/examples/${group_dir}/${example_dir}/meta.json`, 'utf-8'));
|
||||
titles.set(slug, metadata.title);
|
||||
|
||||
return {
|
||||
slug,
|
||||
title: metadata.title
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function get_example(slug) {
|
||||
if (!lookup || !lookup.has(slug)) get_examples();
|
||||
|
||||
const dir = lookup.get(slug);
|
||||
const title = titles.get(slug);
|
||||
|
||||
if (!dir || !title) return null;
|
||||
|
||||
const files = fs.readdirSync(`content/examples/${dir}`)
|
||||
.filter(name => name[0] !== '.' && name !== 'meta.json')
|
||||
.map(name => {
|
||||
return {
|
||||
name,
|
||||
source: fs.readFileSync(`content/examples/${dir}/${name}`, 'utf-8')
|
||||
};
|
||||
});
|
||||
|
||||
return { title, files };
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { get_examples } from './_examples.js';
|
||||
|
||||
let cached;
|
||||
|
||||
export function get() {
|
||||
if (!cached || process.env.NODE_ENV !== 'production') {
|
||||
cached = get_examples().filter(section => section.title);
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
body: cached
|
||||
};
|
||||
} catch(err) {
|
||||
return {
|
||||
status: e.status || 500,
|
||||
body: {
|
||||
message: e.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
<!-- FIXME sometimes it adds a trailing slash when landing -->
|
||||
<script context="module">
|
||||
export async function load({ fetch }) {
|
||||
const sections = await fetch(`examples.json`).then(r => r.json());
|
||||
const title_by_slug = sections.reduce((acc, {examples}) => {
|
||||
examples.forEach(({slug, title}) => {
|
||||
acc[slug] = title;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
props: {
|
||||
sections,
|
||||
title_by_slug
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import Repl from '@sveltejs/svelte-repl';
|
||||
|
||||
import ScreenToggle from '../../components/ScreenToggle.svelte';
|
||||
import {
|
||||
mapbox_setup, // see site/content/examples/15-context/00-context-api
|
||||
rollupUrl,
|
||||
svelteUrl
|
||||
} from '../../config';
|
||||
import { process_example } from '../../utils/examples';
|
||||
import { getFragment } from '@sveltejs/site-kit/utils/navigation';
|
||||
import TableOfContents from './_TableOfContents.svelte';
|
||||
|
||||
export let sections;
|
||||
export let title_by_slug;
|
||||
|
||||
let active_slug;
|
||||
let width;
|
||||
let offset = 1;
|
||||
let repl;
|
||||
let isLoading = false;
|
||||
const cache = {};
|
||||
|
||||
function showExampleCodeOnChange() {
|
||||
offset = 1;
|
||||
}
|
||||
|
||||
$: title = title_by_slug[active_slug] || '';
|
||||
$: first_slug = sections[0].examples[0].slug;
|
||||
$: mobile = width < 768; // note: same as per media query below
|
||||
$: replOrientation = (mobile || width > 1080) ? 'columns' : 'rows';
|
||||
$: if (repl && active_slug) {
|
||||
if (active_slug in cache) {
|
||||
repl.set({ components: cache[active_slug] });
|
||||
showExampleCodeOnChange();
|
||||
} else {
|
||||
isLoading = true;
|
||||
fetch(`examples/${active_slug}.json`)
|
||||
.then(async response => {
|
||||
if (response.ok) {
|
||||
const {files} = await response.json();
|
||||
return process_example(files);
|
||||
}
|
||||
})
|
||||
.then(components => {
|
||||
cache[active_slug] = components;
|
||||
repl.set({components});
|
||||
showExampleCodeOnChange();
|
||||
isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const onhashchange = () => {
|
||||
active_slug = getFragment();
|
||||
};
|
||||
window.addEventListener('hashchange', onhashchange, false);
|
||||
|
||||
const fragment = getFragment();
|
||||
if (fragment) {
|
||||
active_slug = fragment;
|
||||
} else {
|
||||
active_slug = first_slug;
|
||||
goto(`examples#${active_slug}`);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', onhashchange, false);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title} {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} active_section={active_slug} {isLoading} />
|
||||
<div class="repl-container" class:loading={isLoading}>
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
workersUrl="workers"
|
||||
{svelteUrl}
|
||||
{rollupUrl}
|
||||
orientation={replOrientation}
|
||||
fixed={mobile}
|
||||
relaxed
|
||||
injectedJS={mapbox_setup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if mobile}
|
||||
<ScreenToggle bind:offset labels={['index', 'input', 'output']}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.examples-container {
|
||||
position: relative;
|
||||
height: calc(100vh - var(--nav-h));
|
||||
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 .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;
|
||||
grid-template-columns: var(--sidebar-mid-w) auto;
|
||||
grid-auto-rows: 100%;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.offset-1, .offset-2 { transform: none; }
|
||||
}
|
||||
</style>
|
@ -1,58 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { extract_frontmatter, link_renderer } from '@sveltejs/site-kit/utils/markdown.js';
|
||||
import { marked } from 'marked';
|
||||
import { makeSlugProcessor } from '../../utils/slug';
|
||||
import { highlight } from '../../utils/highlight';
|
||||
import { SLUG_PRESERVE_UNICODE } from '../../../config';
|
||||
|
||||
const makeSlug = makeSlugProcessor(SLUG_PRESERVE_UNICODE);
|
||||
|
||||
export default function get_faqs() {
|
||||
return fs
|
||||
.readdirSync('content/faq')
|
||||
.map(file => {
|
||||
if (path.extname(file) !== '.md') return;
|
||||
|
||||
const match = /^([0-9]+)-(.+)\.md$/.exec(file);
|
||||
if (!match) throw new Error(`Invalid filename '${file}'`);
|
||||
|
||||
const [, order, slug] = match;
|
||||
|
||||
const markdown = fs.readFileSync(`content/faq/${file}`, 'utf-8');
|
||||
|
||||
const { content, metadata } = extract_frontmatter(markdown);
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.link = link_renderer;
|
||||
|
||||
renderer.code = highlight;
|
||||
|
||||
renderer.heading = (text, level, rawtext) => {
|
||||
const fragment = makeSlug(rawtext);
|
||||
|
||||
return `
|
||||
<h${level}>
|
||||
<span id="${fragment}" class="offset-anchor"></span>
|
||||
<a href="faq#${fragment}" class="anchor" aria-hidden="true"></a>
|
||||
${text}
|
||||
</h${level}>`;
|
||||
};
|
||||
|
||||
const answer = marked(
|
||||
content.replace(/^\t+/gm, match => match.split('\t').join(' ')),
|
||||
{ renderer }
|
||||
);
|
||||
|
||||
const fragment = makeSlug(slug);
|
||||
|
||||
return {
|
||||
fragment,
|
||||
order,
|
||||
answer,
|
||||
metadata
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import get_faqs from './_faqs.js';
|
||||
|
||||
let json;
|
||||
|
||||
export function get() {
|
||||
if (!json || process.env.NODE_ENV !== 'production') {
|
||||
const faqs = get_faqs()
|
||||
.map(faq => {
|
||||
return {
|
||||
fragment: faq.fragment,
|
||||
answer: faq.answer,
|
||||
metadata: faq.metadata
|
||||
};
|
||||
});
|
||||
|
||||
json = JSON.stringify(faqs);
|
||||
}
|
||||
|
||||
return {
|
||||
body: json,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': `max-age=${5 * 60 * 1e3}` // 5 minutes
|
||||
}
|
||||
};
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
<script>
|
||||
import { Blurb, Hero, Section } from '@sveltejs/site-kit';
|
||||
import Contributors from './_components/Contributors.svelte';
|
||||
import Donors from './_components/Donors.svelte';
|
||||
import Example from './_components/Example.svelte';
|
||||
import WhosUsingSvelte from './_components/WhosUsingSvelte.svelte';
|
||||
// import Lazy from '../components/Lazy.svelte';
|
||||
|
||||
// TODO this causes a Sapper CSS bug...
|
||||
// function loadReplWidget() {
|
||||
// console.log('lazy loading');
|
||||
// return import('../components/Repl/ReplWidget.svelte').then(mod => mod.default);
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* darken text for accessibility */
|
||||
/* TODO does this belong elsewhere? */
|
||||
:global(.back-light) {
|
||||
--text: hsl(36, 3%, 44%);
|
||||
}
|
||||
|
||||
.examples {
|
||||
background: var(--second);
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>Svelte • Cybernetically enhanced web apps</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte">
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps">
|
||||
<meta name="Description" content="Cybernetically enhanced web apps">
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="visually-hidden">Svelte</h1>
|
||||
<Hero
|
||||
title="Svelte"
|
||||
tagline="Cybernetically enhanced web apps"
|
||||
outline="svelte-logo-outline.svg"
|
||||
logotype="svelte-logotype.svg"
|
||||
/>
|
||||
|
||||
<Blurb>
|
||||
<a href="blog/write-less-code" slot="one">
|
||||
<h2>Write less code</h2>
|
||||
<p>Build boilerplate-free components using languages you already know — HTML, CSS and JavaScript</p>
|
||||
|
||||
<span class="learn-more">learn more</span>
|
||||
</a>
|
||||
|
||||
<a href="blog/virtual-dom-is-pure-overhead" slot="two">
|
||||
<h2>No virtual DOM</h2>
|
||||
<p>Svelte compiles your code to tiny, framework-less vanilla JS — your app starts fast and stays fast</p>
|
||||
|
||||
<span class="learn-more">learn more</span>
|
||||
</a>
|
||||
|
||||
<a href="blog/svelte-3-rethinking-reactivity" slot="three">
|
||||
<h2>Truly reactive</h2>
|
||||
<p>No more complex state management libraries — Svelte brings reactivity to JavaScript itself</p>
|
||||
|
||||
<span class="learn-more">learn more</span>
|
||||
</a>
|
||||
|
||||
<div class="description" slot="what">
|
||||
<p>Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the <em>browser</em>, Svelte shifts that work into a <em>compile step</em> that happens when you build your app.</p>
|
||||
|
||||
<p>Instead of using techniques like virtual DOM diffing, Svelte writes code that surgically updates the DOM when the state of your app changes.</p>
|
||||
|
||||
<p>We're proud that Svelte was recently voted the <a href="https://insights.stackoverflow.com/survey/2021#section-most-loved-dreaded-and-wanted-web-frameworks">most loved web framework</a> with the <a href="https://2020.stateofjs.com/en-US/technologies/front-end-frameworks/">most satisfied developers</a> in a pair of industry surveys. We think you'll love it too. <a href="blog/svelte-3-rethinking-reactivity">Read the introductory blog post</a> to learn more.</p>
|
||||
</div>
|
||||
|
||||
<div style="grid-area: start; display: flex; flex-direction: column; min-width: 0" slot="how">
|
||||
<pre class="language-bash" style="margin: 0 0 1em 0; min-width: 0; min-height: 0">
|
||||
npx degit <a href="https://github.com/sveltejs/template" style="user-select: initial;">sveltejs/template</a> my-svelte-project
|
||||
<span class="token comment"># or download and extract <a href="https://github.com/sveltejs/template/archive/master.zip">this .zip file</a></span>
|
||||
cd my-svelte-project
|
||||
<span class="token comment"># to use <a href="blog/svelte-and-typescript">TypeScript</a> run:</span>
|
||||
<span class="token comment"># node scripts/setupTypeScript.js</span>
|
||||
|
||||
npm install
|
||||
npm run dev
|
||||
</pre>
|
||||
|
||||
<p style="flex: 1">See the <a href="blog/the-easiest-way-to-get-started">quickstart guide</a> for more information.</p>
|
||||
|
||||
<p class="cta"><a sveltekit:prefetch href="tutorial">Learn Svelte</a></p>
|
||||
</div>
|
||||
</Blurb>
|
||||
|
||||
<div class="examples">
|
||||
<Example id="hello-world">
|
||||
<p>Svelte components are built on top of HTML. Just add data.</p>
|
||||
</Example>
|
||||
|
||||
<Example id="nested-components">
|
||||
<p>CSS is component-scoped by default — no more style collisions or specificity wars. Or you can <a href="/blog/svelte-css-in-js">use your favourite CSS-in-JS library</a>.</p>
|
||||
</Example>
|
||||
|
||||
<Example id="reactive-assignments">
|
||||
<p>Trigger efficient, granular updates by assigning to local variables. The compiler does the rest.</p>
|
||||
</Example>
|
||||
|
||||
<Example id="svg-transitions">
|
||||
<p>Build beautiful UIs with a powerful, performant transition engine built right into the framework.</p>
|
||||
</Example>
|
||||
</div>
|
||||
|
||||
<Section>
|
||||
<h3>Who's using Svelte?</h3>
|
||||
|
||||
<WhosUsingSvelte/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<h3>Supporters</h3>
|
||||
|
||||
<p>Svelte is free and open source software, made possible by the work of hundreds of volunteers and donors. <a href="https://github.com/sveltejs/svelte">Join us</a> or <a href="https://opencollective.com/svelte">give</a>!</p>
|
||||
|
||||
<h4>Contributors</h4>
|
||||
|
||||
<Contributors/>
|
||||
|
||||
<p></p>
|
||||
|
||||
<h4>Donors</h4>
|
||||
|
||||
<Donors/>
|
||||
</Section>
|
@ -1,114 +0,0 @@
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
import { session } from '$app/stores';
|
||||
|
||||
const { logout } = getContext('app');
|
||||
|
||||
let showMenu = false;
|
||||
let name;
|
||||
|
||||
$: name = $session.user.name || $session.user.username;
|
||||
</script>
|
||||
|
||||
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
|
||||
<span>{name}</span>
|
||||
<img alt="{name} avatar" src="{$session.user.avatar}">
|
||||
|
||||
{#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 1.2rem 0 1.6rem;
|
||||
height: 0.8em;
|
||||
line-height: 1;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.user::after {
|
||||
/* embiggen hit zone, so log out menu doesn't disappear */
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3.2rem;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
/* position: relative; padding: 0 2em 0 0; */
|
||||
line-height: 1;
|
||||
display: none;
|
||||
font-family: var(--font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.user:hover span {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: -0.05em;
|
||||
right: 0;
|
||||
width: 2.1rem;
|
||||
height: 2.1rem;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
width: calc(100% + 1.6rem);
|
||||
min-width: 10em;
|
||||
top: 3rem;
|
||||
right: -1.6rem;
|
||||
background-color: var(--second);
|
||||
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(--font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
padding: 0.4rem 0;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.menu button:hover, .menu a:hover {
|
||||
opacity: 1;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.user {
|
||||
padding: 0em 3.2rem 0 1.6rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,315 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import { session } from '$app/stores';
|
||||
import UserMenu from './UserMenu.svelte';
|
||||
import { Icon } from '@sveltejs/site-kit';
|
||||
import * as doNotZip from 'do-not-zip';
|
||||
import downloadBlob from '../../../_utils/downloadBlob.js';
|
||||
import { enter } from '../../../../../utils/events.js';
|
||||
import { isMac } from '../../../../../utils/compat.js';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const { login } = getContext('app');
|
||||
|
||||
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 = $session.user && gist && gist.owner === $session.user.uid;
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 's' && (isMac ? event.metaKey : event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
async function fork(intentWasSave) {
|
||||
saving = true;
|
||||
|
||||
const { components } = repl.toJSON();
|
||||
|
||||
try {
|
||||
const r = await fetch(`repl/create.json`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: components.map(component => ({
|
||||
name: `${component.name}.${component.type}`,
|
||||
source: component.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 (saving) return;
|
||||
|
||||
if (!canSave) {
|
||||
fork(true);
|
||||
return;
|
||||
}
|
||||
|
||||
saving = true;
|
||||
|
||||
try {
|
||||
// Send all files back to API
|
||||
// ~> Any missing files are considered deleted!
|
||||
const { components } = repl.toJSON();
|
||||
|
||||
const r = await fetch(`repl/${gist.uid}.json`, {
|
||||
method: 'PATCH',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
files: components.map(component => ({
|
||||
name: `${component.name}.${component.type}`,
|
||||
source: component.source
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
if (r.status < 200 || r.status >= 300) {
|
||||
const { error } = await r.json();
|
||||
throw new Error(`Received an HTTP ${r.status} response: ${error}`);
|
||||
}
|
||||
|
||||
await r.json();
|
||||
|
||||
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 { 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 => e.target.blur()}"
|
||||
>
|
||||
|
||||
<div style="text-align: right; margin-right:.4rem">
|
||||
<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 || !$session.user}" on:click={() => fork(false)} title="fork">
|
||||
{#if justForked}
|
||||
<Icon name="check" />
|
||||
{:else}
|
||||
<Icon name="git-branch" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="icon" disabled="{saving || !$session.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 $session.user}
|
||||
<UserMenu/>
|
||||
{: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: .6rem var(--side-nav);
|
||||
background-color: var(--second);
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
top: -0.1rem;
|
||||
display: inline-block;
|
||||
padding: 0.2em;
|
||||
opacity: .7;
|
||||
transition: opacity .3s;
|
||||
font-family: var(--font);
|
||||
font-size: 1.6rem;
|
||||
color: white;
|
||||
/* width: 1.6em;
|
||||
height: 1.6em; */
|
||||
line-height: 1;
|
||||
margin: 0 0 0 0.2em;
|
||||
}
|
||||
|
||||
.icon:hover { opacity: 1 }
|
||||
.icon:disabled { opacity: .3 }
|
||||
|
||||
.icon[title^='fullscreen'] { display: none }
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
font-family: var(--font);
|
||||
font-size: 1.6rem;
|
||||
opacity: 0.7;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
margin: 0 0.2em 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,173 +0,0 @@
|
||||
import * as httpie from 'httpie';
|
||||
import { query, find } from '../../../utils/db';
|
||||
import { get_example } from '../../examples/_examples.js';
|
||||
|
||||
const GITHUB_CLIENT_ID = process.env['GITHUB_CLIENT_ID'];
|
||||
const GITHUB_CLIENT_SECRET = process.env['GITHUB_CLIENT_SECRET'];
|
||||
|
||||
export async function get({ path, params }) {
|
||||
// is this an example?
|
||||
const example = get_example(params.id);
|
||||
|
||||
if (example) {
|
||||
return {
|
||||
body: {
|
||||
relaxed: true,
|
||||
uid: params.id,
|
||||
name: example.title,
|
||||
files: example.files,
|
||||
owner: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// In dev, proxy requests to load particular REPLs to the real server.
|
||||
// This avoids needing to connect to the real database server.
|
||||
try {
|
||||
const res_proxy = await httpie.get(`https://svelte.dev${path}`);
|
||||
return {
|
||||
body: res_proxy.data,
|
||||
status: res_proxy.statusCode,
|
||||
headers: res_proxy.headers
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: err.statusCode,
|
||||
body: { error: err.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const [row] = await query(`
|
||||
select g.*, u.uid as owner from gists g
|
||||
left join users u on g.user_id = u.id
|
||||
where g.uid = $1 limit 1
|
||||
`, [params.id]); // via filename pattern
|
||||
|
||||
if (!row) {
|
||||
const base = `https://api.github.com/gists/${params.id}`;
|
||||
const url = `${base}?client_id=${GITHUB_CLIENT_ID}&client_secret=${GITHUB_CLIENT_SECRET}`;
|
||||
|
||||
try {
|
||||
const { data } = await httpie.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'https://svelte.dev'
|
||||
}
|
||||
});
|
||||
|
||||
// create owner if necessary...
|
||||
let user = await find(`select * from users where uid = $1`, [data.owner.id]);
|
||||
|
||||
if (!user) {
|
||||
const { id, name, login, avatar_url } = data.owner;
|
||||
|
||||
user = await find(`
|
||||
insert into users(uid, name, username, avatar)
|
||||
values ($1, $2, $3, $4)
|
||||
returning *
|
||||
`, [id, name, login, avatar_url]);
|
||||
}
|
||||
|
||||
delete data.files['README.md'];
|
||||
delete data.files['meta.json'];
|
||||
|
||||
const files = Object.keys(data.files).map(key => {
|
||||
const name = key.replace(/\.html$/, '.svelte');
|
||||
|
||||
return {
|
||||
name,
|
||||
source: data.files[key].content
|
||||
};
|
||||
});
|
||||
|
||||
// add gist to database...
|
||||
await query(`
|
||||
insert into gists(uid, user_id, name, files)
|
||||
values ($1, $2, $3, $4)
|
||||
`, [params.id, user.id, data.description, JSON.stringify(files)]);
|
||||
|
||||
return {
|
||||
body: {
|
||||
uid: params.id,
|
||||
name: data.description,
|
||||
files,
|
||||
owner: data.owner.id
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: err.statusCode,
|
||||
body: { error: err.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
body: {
|
||||
uid: row.uid.replace(/-/g, ''),
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: row.owner
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function patch({ params, locals, body }) {
|
||||
const { user } = locals;
|
||||
if (!user) return;
|
||||
|
||||
let id;
|
||||
const uid = params.id;
|
||||
|
||||
try {
|
||||
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
|
||||
if (!row) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
error: 'Gist not found'
|
||||
}
|
||||
};
|
||||
}
|
||||
if (row.user_id !== user.id) {
|
||||
return { status: 403, body: { error: 'Item does not belong to you' }};
|
||||
}
|
||||
id = row.id;
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ select', err);
|
||||
return { status: 500 };
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = body;
|
||||
obj.updated_at = 'now()';
|
||||
let k;
|
||||
const cols = [];
|
||||
const vals = [];
|
||||
for (k in obj) {
|
||||
cols.push(k);
|
||||
vals.push(k === 'files' ? JSON.stringify(obj[k]) : obj[k]);
|
||||
}
|
||||
|
||||
const tmp = vals.map((x, i) => `$${i + 1}`).join(',');
|
||||
const set = `set (${cols.join(',')}) = (${tmp})`;
|
||||
|
||||
const [row] = await query(`update gists ${set} where id = ${id} returning *`, vals);
|
||||
|
||||
return {
|
||||
body: {
|
||||
uid: row.uid.replace(/-/g, ''),
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: user.uid,
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ update', err);
|
||||
return {
|
||||
status: 500,
|
||||
body: { error: err.message }
|
||||
};
|
||||
}
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
<script context="module">
|
||||
export function load({ page: { params, query }}) {
|
||||
return {
|
||||
props: {
|
||||
version: query.get('version') || '3',
|
||||
id: params.id
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Repl from '@sveltejs/svelte-repl';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { goto } from '$app/navigation';
|
||||
import { session } from '$app/stores';
|
||||
import { mapbox_setup } from '../../../config';
|
||||
import InputOutputToggle from '../../../components/Repl/InputOutputToggle.svelte';
|
||||
import AppControls from './_components/AppControls/index.svelte';
|
||||
|
||||
export let version;
|
||||
export let id;
|
||||
|
||||
let repl;
|
||||
let gist;
|
||||
let name = 'Loading...';
|
||||
let zen_mode = false;
|
||||
let is_relaxed_gist = false;
|
||||
let width = browser ? window.innerWidth : 1000;
|
||||
let checked = 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/${id}?${params.join('&')}`
|
||||
: `/repl/${id}`;
|
||||
|
||||
history.replaceState({}, 'x', url);
|
||||
}
|
||||
|
||||
$: if (typeof history !== 'undefined') update_query_string(version);
|
||||
|
||||
function fetch_gist(id) {
|
||||
if (gist && gist.uid === id) {
|
||||
// if the id changed because we just forked, don't refetch
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO handle `relaxed` logic
|
||||
fetch(`/repl/${id}.json`).then(r => {
|
||||
if (r.ok) {
|
||||
r.json().then(data => {
|
||||
gist = data;
|
||||
name = data.name;
|
||||
|
||||
is_relaxed_gist = data.relaxed;
|
||||
|
||||
const components = data.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'; // TODO do this on the server
|
||||
return { name, type, source: file.source };
|
||||
});
|
||||
|
||||
components.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;
|
||||
});
|
||||
|
||||
repl.set({ components });
|
||||
});
|
||||
} else {
|
||||
console.warn('TODO: 404 Gist');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$: if (browser) fetch_gist(id);
|
||||
|
||||
onMount(() => {
|
||||
if (version !== 'local') {
|
||||
fetch(`https://unpkg.com/svelte@${version || '3'}/package.json`)
|
||||
.then(r => r.json())
|
||||
.then(pkg => {
|
||||
version = pkg.version;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handle_fork(event) {
|
||||
console.log('> handle_fork', event);
|
||||
gist = event.detail.gist;
|
||||
goto(`/repl/${gist.uid}?version=${version}`);
|
||||
}
|
||||
|
||||
function handle_change(event) {
|
||||
modified_count = event.detail.components.filter(c => c.modified).length;
|
||||
}
|
||||
|
||||
$: svelteUrl = browser && version === 'local' ?
|
||||
`${location.origin}/repl/local` :
|
||||
`https://unpkg.com/svelte@${version}`;
|
||||
|
||||
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
|
||||
|
||||
$: mobile = width < 540;
|
||||
|
||||
$: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: relative;
|
||||
height: calc(100vh - var(--nav-h));
|
||||
--app-controls-h: 5.6rem;
|
||||
--pane-controls-h: 4.2rem;
|
||||
overflow: hidden;
|
||||
background-color: var(--back);
|
||||
padding: var(--app-controls-h) 0 0 0;
|
||||
/* margin: 0 calc(var(--side-nav) * -1); */
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile .viewport {
|
||||
width: 200%;
|
||||
height: calc(100% - 42px);
|
||||
transition: transform 0.3s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile .offset {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
/* temp fix for #2499 and #2550 while waiting for a fix for https://github.com/sveltejs/svelte-repl/issues/8 */
|
||||
|
||||
.viewport :global(.tab-content),
|
||||
.viewport :global(.tab-content.visible) {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
}
|
||||
.viewport :global(.tab-content) {
|
||||
visibility: hidden;
|
||||
}
|
||||
.viewport :global(.tab-content.visible) {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.zen-mode {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 111;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0 }
|
||||
100% { opacity: 1 }
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{name} • REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte REPL">
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps">
|
||||
<meta name="Description" content="Interactive Svelte playground">
|
||||
</svelte:head>
|
||||
|
||||
<svelte:window bind:innerWidth={width}/>
|
||||
|
||||
<div class="repl-outer {zen_mode ? 'zen-mode' : ''}" class:mobile>
|
||||
<AppControls
|
||||
{gist}
|
||||
{repl}
|
||||
bind:name
|
||||
bind:zen_mode
|
||||
bind:modified_count
|
||||
on:forked={handle_fork}
|
||||
/>
|
||||
|
||||
{#if browser}
|
||||
<div class="viewport" class:offset={checked}>
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
workersUrl="workers"
|
||||
{svelteUrl}
|
||||
{rollupUrl}
|
||||
{relaxed}
|
||||
fixed={mobile}
|
||||
injectedJS={mapbox_setup}
|
||||
on:change={handle_change}
|
||||
on:add={handle_change}
|
||||
on:remove={handle_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if mobile}
|
||||
<InputOutputToggle bind:checked/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
export default function body(req) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
let str = '';
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
req.on('data', chunk => {
|
||||
str += chunk;
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
try {
|
||||
fulfil(JSON.parse(str));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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,31 +0,0 @@
|
||||
import { query } from '../../utils/db';
|
||||
|
||||
export async function post({ locals, body }) {
|
||||
const { user } = locals;
|
||||
if (!user) return; // response already sent
|
||||
|
||||
try {
|
||||
const { name, files } = body;
|
||||
|
||||
const [row] = await query(`
|
||||
insert into gists(user_id, name, files)
|
||||
values ($1, $2, $3) returning *`, [user.id, name, JSON.stringify(files)]);
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
uid: row.uid.replace(/-/g, ''),
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: user.uid,
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
error: err.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
<script context="module">
|
||||
export function load({ page: { query }}) {
|
||||
return {
|
||||
props: {
|
||||
version: query.get('version') || '3',
|
||||
gist: query.get('gist'),
|
||||
example: query.get('example')
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { browser } from '$app/env';
|
||||
import ReplWidget from '../../components/Repl/ReplWidget.svelte';
|
||||
|
||||
export let version;
|
||||
export let gist;
|
||||
export let example;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.repl-outer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--back);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
--pane-controls-h: 4.2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>REPL • Svelte</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte REPL">
|
||||
<meta name="twitter:description" content="Cybernetically enhanced web apps">
|
||||
<meta name="Description" content="Interactive Svelte playground">
|
||||
</svelte:head>
|
||||
|
||||
<div class="repl-outer">
|
||||
{#if browser}
|
||||
<ReplWidget {version} {gist} {example} embedded={true}/>
|
||||
{/if}
|
||||
</div>
|
@ -1,23 +0,0 @@
|
||||
<script context="module">
|
||||
export function load({ page: { query } }) {
|
||||
const gist = query.get('gist');
|
||||
const example = query.get('example');
|
||||
const version = query.get('version');
|
||||
|
||||
// redirect to v2 REPL if appropriate
|
||||
if (/^[^>]?[12]/.test(version)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `https://v2.svelte.dev/repl?${query}`
|
||||
};
|
||||
}
|
||||
|
||||
const id = gist || example || 'hello-world';
|
||||
const q = version ? `?version=${version}` : ``;
|
||||
|
||||
return {
|
||||
status: 301,
|
||||
redirect: `/repl/${id}${q}`
|
||||
};
|
||||
}
|
||||
</script>
|
@ -1,11 +0,0 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
export function get({ params: { path } }) {
|
||||
if (process.env.NODE_ENV !== 'development' || ('/' + path).includes('/.')) {
|
||||
return { status: 403 };
|
||||
}
|
||||
return {
|
||||
headers: { 'Content-Type': 'text/javascript' },
|
||||
body: readFileSync('../' + path)
|
||||
};
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { Icon } from '@sveltejs/site-kit';
|
||||
|
||||
export let sections;
|
||||
export let slug;
|
||||
export let selected;
|
||||
|
||||
function navigate(e) {
|
||||
goto(`tutorial/${e.target.value}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 2.5em 1fr 2.5em;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
padding: 1em 0.5em;
|
||||
font-weight: 300;
|
||||
font-size: var(--h6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.7em 0;
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
color: white;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
a.disabled, a.disabled:hover, a.disabled:active {
|
||||
color: white;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
top: 0.3em;
|
||||
}
|
||||
|
||||
strong { opacity: 0.7 }
|
||||
|
||||
select {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.0001;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav>
|
||||
<a sveltekit:prefetch aria-label="Previous tutorial step" class="no-underline" href="tutorial/{(selected.prev || selected).slug}" class:disabled={!selected.prev}>
|
||||
<Icon name="arrow-left" />
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
<strong>
|
||||
<span style="position: relative; top: -0.1em; margin: 0 0.5em 0 0"><Icon name="menu"/></span>
|
||||
{selected.section.title} /
|
||||
</strong>
|
||||
{selected.chapter.title}
|
||||
</span>
|
||||
|
||||
<select value={slug} on:change={navigate}>
|
||||
{#each sections as section, i}
|
||||
<optgroup label="{i + 1}. {section.title}">
|
||||
{#each section.chapters as chapter, i}
|
||||
<option value={chapter.slug}>{String.fromCharCode(i + 97)}. {chapter.title}</option>
|
||||
{/each}
|
||||
</optgroup>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a sveltekit:prefetch aria-label="Next tutorial step" class="no-underline" href="tutorial/{(selected.next || selected).slug}" class:disabled={!selected.next}>
|
||||
<Icon name="arrow-right" />
|
||||
</a>
|
||||
</nav>
|
@ -1,101 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { marked } from 'marked';
|
||||
import { extract_frontmatter, extract_metadata, link_renderer } from '@sveltejs/site-kit/utils/markdown';
|
||||
import { highlight } from '../../../utils/highlight';
|
||||
|
||||
const cache = new Map();
|
||||
|
||||
function find_tutorial(slug) {
|
||||
const sections = fs.readdirSync(`content/tutorial`);
|
||||
|
||||
for (const section of sections) {
|
||||
const chapters = fs.readdirSync(`content/tutorial/${section}`).filter(dir => /^\d+/.test(dir));
|
||||
for (const chapter of chapters) {
|
||||
if (slug === chapter.replace(/^\d+-/, '')) {
|
||||
return { section, chapter };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_tutorial(slug) {
|
||||
const found = find_tutorial(slug);
|
||||
if (!found) return found;
|
||||
|
||||
const dir = `content/tutorial/${found.section}/${found.chapter}`;
|
||||
|
||||
const markdown = fs.readFileSync(`${dir}/text.md`, 'utf-8');
|
||||
const app_a = fs.readdirSync(`${dir}/app-a`);
|
||||
const app_b = fs.existsSync(`${dir}/app-b`) && fs.readdirSync(`${dir}/app-b`);
|
||||
|
||||
const { content } = extract_frontmatter(markdown);
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.link = link_renderer;
|
||||
|
||||
renderer.code = (source, lang) => {
|
||||
source = source.replace(/^ +/gm, match =>
|
||||
match.split(' ').join('\t')
|
||||
);
|
||||
|
||||
const lines = source.split('\n');
|
||||
|
||||
const meta = extract_metadata(lines[0], lang);
|
||||
|
||||
let prefix = '';
|
||||
let className = 'code-block';
|
||||
|
||||
if (meta) {
|
||||
source = lines.slice(1).join('\n');
|
||||
const filename = meta.filename || (lang === 'html' && 'App.svelte');
|
||||
if (filename) {
|
||||
prefix = `<span class='filename'>${prefix} ${filename}</span>`;
|
||||
className += ' named';
|
||||
}
|
||||
}
|
||||
|
||||
return `<div class='${className}'>${prefix}${highlight(source, lang)}</div>`;
|
||||
};
|
||||
|
||||
let html = marked(content, { renderer });
|
||||
if (found.chapter.startsWith('01')) {
|
||||
const meta = JSON.parse(fs.readFileSync(`content/tutorial/${found.section}/meta.json`));
|
||||
html = `<h2>${meta.title}</h2>\n${html}`;
|
||||
}
|
||||
|
||||
function get_file(stage, file) {
|
||||
const ext = path.extname(file);
|
||||
const name = file.slice(0, -ext.length);
|
||||
const type = ext.slice(1);
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
source: fs.readFileSync(`${dir}/${stage}/${file}`, 'utf-8')
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
html,
|
||||
app_a: app_a.map(file => get_file('app-a', file)),
|
||||
app_b: app_b && app_b.map(file => get_file('app-b', file))
|
||||
};
|
||||
}
|
||||
|
||||
export function get({ params }) {
|
||||
const { slug } = params;
|
||||
|
||||
let tut = cache.get(slug);
|
||||
if (!tut || process.env.NODE_ENV !== 'production') {
|
||||
tut = get_tutorial(slug);
|
||||
cache.set(slug, tut);
|
||||
}
|
||||
|
||||
if (tut) {
|
||||
return {
|
||||
body: tut
|
||||
};
|
||||
}
|
||||
}
|
@ -1,323 +0,0 @@
|
||||
<script context="module">
|
||||
export async function load({ fetch, page: { params }}) {
|
||||
const res = await fetch(`/tutorial/${params.slug}.json`);
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: 301,
|
||||
redirect: '/tutorial/basics'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
slug: params.slug,
|
||||
chapter: await res.json()
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Repl from '@sveltejs/svelte-repl';
|
||||
import { getContext } from 'svelte';
|
||||
import { browser } from '$app/env';
|
||||
|
||||
import ScreenToggle from '../../../components/ScreenToggle.svelte';
|
||||
import TableOfContents from './_TableOfContents.svelte';
|
||||
|
||||
import {
|
||||
mapbox_setup, // needed for context API tutorial
|
||||
rollupUrl,
|
||||
svelteUrl
|
||||
} from '../../../config';
|
||||
|
||||
export let slug;
|
||||
export let chapter;
|
||||
|
||||
const { sections } = getContext('tutorial');
|
||||
|
||||
let repl;
|
||||
let prev;
|
||||
let scrollable;
|
||||
const lookup = new Map();
|
||||
|
||||
let width = browser ? window.innerWidth : 1000;
|
||||
let offset = 0;
|
||||
|
||||
sections.forEach(section => {
|
||||
section.chapters.forEach(chapter => {
|
||||
const obj = {
|
||||
slug: chapter.slug,
|
||||
section,
|
||||
chapter,
|
||||
prev
|
||||
};
|
||||
|
||||
lookup.set(chapter.slug, obj);
|
||||
|
||||
if (browser) { // pending https://github.com/sveltejs/svelte/issues/2135
|
||||
if (prev) prev.next = obj;
|
||||
prev = obj;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// TODO is there a non-hacky way to trigger scroll when chapter changes?
|
||||
$: if (scrollable) chapter, scrollable.scrollTo(0, 0);
|
||||
|
||||
// TODO: this will need to be changed to the master branch, and probably should be dynamic instead of included
|
||||
// here statically
|
||||
const tutorial_repo_link = 'https://github.com/sveltejs/svelte/tree/master/site/content/tutorial';
|
||||
|
||||
$: selected = lookup.get(slug);
|
||||
$: improve_link = `${tutorial_repo_link}/${selected.chapter.section_dir}/${selected.chapter.chapter_dir}`;
|
||||
|
||||
const clone = file => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
source: file.source
|
||||
});
|
||||
|
||||
$: if (repl) {
|
||||
completed = false;
|
||||
repl.set({
|
||||
components: chapter.app_a.map(clone)
|
||||
});
|
||||
}
|
||||
|
||||
$: mobile = width < 768;
|
||||
|
||||
function reset() {
|
||||
repl.update({
|
||||
components: chapter.app_a.map(clone)
|
||||
});
|
||||
}
|
||||
|
||||
function complete() {
|
||||
repl.update({
|
||||
components: chapter.app_b.map(clone)
|
||||
});
|
||||
}
|
||||
|
||||
let completed = false;
|
||||
|
||||
function handle_change(event) {
|
||||
completed = event.detail.components.every((file, i) => {
|
||||
const expected = chapter.app_b[i];
|
||||
return expected && (
|
||||
file.name === expected.name &&
|
||||
file.type === expected.type &&
|
||||
file.source.trim().replace(/\s+$/gm, '') === expected.source.trim().replace(/\s+$/gm, '')
|
||||
);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tutorial-outer {
|
||||
position: relative;
|
||||
height: calc(100vh - var(--nav-h));
|
||||
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 .3s;
|
||||
grid-auto-rows: 100%;
|
||||
}
|
||||
|
||||
.offset-1 { transform: translate(-33.333%, 0); }
|
||||
.offset-2 { transform: translate(-66.666%, 0); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tutorial-outer { padding: 0 }
|
||||
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(33.333%, var(--sidebar-large-w)) auto;
|
||||
grid-auto-rows: 100%;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.offset-1, .offset-2 { transform: none; }
|
||||
}
|
||||
|
||||
.tutorial-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--second);
|
||||
background-color: var(--second);
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.chapter-markup {
|
||||
padding: 3.2rem 4rem;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.chapter-markup :global(h2) {
|
||||
margin: 4rem 0 1.6rem 0;
|
||||
font-size: var(--h3);
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chapter-markup :global(h2:first-child) {
|
||||
margin-top: .4rem;
|
||||
}
|
||||
|
||||
.chapter-markup :global(a) {
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.chapter-markup :global(a:hover) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.chapter-markup :global(ul) {
|
||||
padding: 0 0 0 2em;
|
||||
}
|
||||
|
||||
.chapter-markup :global(blockquote) {
|
||||
background-color: rgba(0,0,0,.17);
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.chapter-markup::-webkit-scrollbar {
|
||||
background-color: var(--second);
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.chapter-markup::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,.7);
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.chapter-markup :global(p) > :global(code),
|
||||
.chapter-markup :global(ul) :global(code) {
|
||||
color: var(--sidebar-text);
|
||||
background: rgba(0,0,0,.12);
|
||||
padding: .2em .4em .3em;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
top: -0.1em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
border-top: 1px solid rgba(255,255,255,.15);
|
||||
padding: 1em 0 0 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.show {
|
||||
background: var(--prime);
|
||||
padding: .3em .7em;
|
||||
border-radius: var(--border-r);
|
||||
top: .1em;
|
||||
position: relative;
|
||||
font-size: var(--h5);
|
||||
font-weight: 300;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.show:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
a.next {
|
||||
padding-right: 1.2em;
|
||||
background: no-repeat 100% 50% url(/icons/arrow-right.svg);
|
||||
background-size: 1em 1em;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.improve-chapter {
|
||||
padding: 1em 0 .5em 0;
|
||||
}
|
||||
|
||||
.improve-chapter a {
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
opacity: .3;
|
||||
padding: 0 .1em 0 1.2em;
|
||||
background: no-repeat 0 50% url(/icons/edit.svg);
|
||||
background-size: 1em 1em;
|
||||
}
|
||||
|
||||
.improve-chapter a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{selected.section.title} / {selected.chapter.title} • Svelte Tutorial</title>
|
||||
|
||||
<meta name="twitter:title" content="Svelte tutorial">
|
||||
<meta name="twitter:description" content="{selected.section.title} / {selected.chapter.title}">
|
||||
<meta name="Description" content="{selected.section.title} / {selected.chapter.title}">
|
||||
</svelte:head>
|
||||
|
||||
<svelte:window bind:innerWidth={width}/>
|
||||
|
||||
<div class="tutorial-outer">
|
||||
<div class="viewport offset-{offset}">
|
||||
<div class="tutorial-text">
|
||||
<div class="table-of-contents">
|
||||
<TableOfContents {sections} {slug} {selected}/>
|
||||
</div>
|
||||
|
||||
<div class="chapter-markup" bind:this={scrollable}>
|
||||
{@html chapter.html}
|
||||
|
||||
<div class="controls">
|
||||
{#if chapter.app_b}
|
||||
<!-- TODO disable this button when the contents of the REPL
|
||||
matches the expected end result -->
|
||||
<button class="show" on:click="{() => completed ? reset() : complete()}">
|
||||
{completed ? 'Reset' : 'Show me'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if selected.next}
|
||||
<a class="next" href="tutorial/{selected.next.slug}">Next</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="improve-chapter">
|
||||
<a class="no-underline" href={improve_link}>Edit this chapter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tutorial-repl">
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
workersUrl="workers"
|
||||
{svelteUrl}
|
||||
{rollupUrl}
|
||||
orientation={mobile ? 'columns' : 'rows'}
|
||||
fixed={mobile}
|
||||
on:change={handle_change}
|
||||
injectedJS={mapbox_setup}
|
||||
relaxed
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if mobile}
|
||||
<ScreenToggle bind:offset labels={['tutorial', 'input', 'output']}/>
|
||||
{/if}
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
<script context="module">
|
||||
export async function load({ fetch }) {
|
||||
const sections = await fetch(`/tutorial.json`).then(r => r.json());
|
||||
return {
|
||||
props: {
|
||||
sections
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
export let sections;
|
||||
setContext('tutorial', { sections });
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
@ -1,67 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import { extract_frontmatter } from '@sveltejs/site-kit/utils/markdown';
|
||||
|
||||
let json;
|
||||
|
||||
function get_sections() {
|
||||
const slugs = new Set();
|
||||
|
||||
const sections = fs.readdirSync(`content/tutorial`)
|
||||
.filter(dir => /^\d+/.test(dir))
|
||||
.map(dir => {
|
||||
let meta;
|
||||
|
||||
try {
|
||||
meta = JSON.parse(fs.readFileSync(`content/tutorial/${dir}/meta.json`, 'utf-8'));
|
||||
} catch (err) {
|
||||
throw new Error(`Error reading metadata for ${dir}`);
|
||||
}
|
||||
|
||||
return {
|
||||
title: meta.title,
|
||||
chapters: fs.readdirSync(`content/tutorial/${dir}`)
|
||||
.filter(dir => /^\d+/.test(dir))
|
||||
.map(tutorial => {
|
||||
try {
|
||||
const md = fs.readFileSync(`content/tutorial/${dir}/${tutorial}/text.md`, 'utf-8');
|
||||
const { metadata } = extract_frontmatter(md);
|
||||
|
||||
const slug = tutorial.replace(/^\d+-/, '');
|
||||
|
||||
if (slugs.has(slug)) throw new Error(`Duplicate slug: ${slug}`);
|
||||
slugs.add(slug);
|
||||
|
||||
return {
|
||||
slug,
|
||||
title: metadata.title,
|
||||
section_dir: dir,
|
||||
chapter_dir: tutorial,
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Error building tutorial ${dir}/${tutorial}: ${err.message}`);
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
export function get() {
|
||||
try {
|
||||
if (!json || process.env.NODE_ENV !== 'production') {
|
||||
json = get_sections();
|
||||
}
|
||||
|
||||
return {
|
||||
body: json
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 500,
|
||||
body: {
|
||||
message: err.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<script context="module">
|
||||
export function load() {
|
||||
return {
|
||||
status: 301,
|
||||
redirect: '/tutorial/basics'
|
||||
};
|
||||
}
|
||||
</script>
|
@ -1,23 +0,0 @@
|
||||
export async function get(req) {
|
||||
let { min = '0', max = '100' } = req.query;
|
||||
min = +min;
|
||||
max = +max;
|
||||
|
||||
// simulate a long delay
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
|
||||
// fail sometimes
|
||||
if (Math.random() < 0.333) {
|
||||
return {
|
||||
status: 400,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
body: `Failed to generate random number. Please try again`
|
||||
};
|
||||
}
|
||||
|
||||
const num = min + Math.round(Math.random() * (max - min));
|
||||
return {
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
body: String(num)
|
||||
};
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import flru from 'flru';
|
||||
import { find, query } from './db';
|
||||
|
||||
export const sanitize_user = obj => obj && ({
|
||||
uid: obj.uid,
|
||||
username: obj.username,
|
||||
name: obj.name,
|
||||
avatar: obj.avatar
|
||||
});
|
||||
|
||||
const session_cache = flru(1000);
|
||||
|
||||
export const create_user = async (gh_user, access_token) => {
|
||||
return await find(`
|
||||
insert into users(uid, name, username, avatar, github_token)
|
||||
values ($1, $2, $3, $4, $5) on conflict (uid) do update
|
||||
set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now())
|
||||
returning id, uid, username, name, avatar
|
||||
`, [gh_user.id, gh_user.name, gh_user.login, gh_user.avatar_url, access_token]);
|
||||
};
|
||||
|
||||
export const create_session = async user => {
|
||||
const session = await find(`
|
||||
insert into sessions(user_id)
|
||||
values ($1)
|
||||
returning uid
|
||||
`, [user.id]);
|
||||
|
||||
session_cache.set(session.uid, user);
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
export const delete_session = async sid => {
|
||||
await query(`delete from sessions where uid = $1`, [sid]);
|
||||
session_cache.set(sid, null);
|
||||
};
|
||||
|
||||
export const get_user = async sid => {
|
||||
if (!sid) return null;
|
||||
|
||||
if (!session_cache.has(sid)) {
|
||||
session_cache.set(sid, await find(`
|
||||
select users.id, users.uid, users.username, users.name, users.avatar
|
||||
from sessions
|
||||
left join users on sessions.user_id = users.id
|
||||
where sessions.uid = $1 and expiry > now()
|
||||
`, [sid]));
|
||||
}
|
||||
|
||||
return session_cache.get(sid);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export const isMac = typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
|
@ -1,12 +0,0 @@
|
||||
import pg from 'pg';
|
||||
|
||||
// Uses `PG*` ENV vars
|
||||
export const DB = process.env['PGHOST'] ? new pg.Pool() : null;
|
||||
|
||||
export function query(text, values=[]) {
|
||||
return DB.query(text, values).then(r => r.rows);
|
||||
}
|
||||
|
||||
export function find(text, values=[]) {
|
||||
return query(text, values).then(arr => arr[0]);
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
export function keyEvent(code) {
|
||||
return function (node, callback) {
|
||||
node.addEventListener('keydown', handleKeydown);
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.keyCode === code) {
|
||||
callback.call(this, event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', handleKeydown);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const enter = keyEvent(13);
|
@ -1,16 +0,0 @@
|
||||
export function process_example(files) {
|
||||
return files
|
||||
.map(file => {
|
||||
const [name, type] = file.name.split('.');
|
||||
return { name, type, source: file.source };
|
||||
})
|
||||
.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.name < b.name ? -1 : 1;
|
||||
|
||||
if (a.type === 'svelte') return -1;
|
||||
if (b.type === 'svelte') return 1;
|
||||
});
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { langs } from '@sveltejs/site-kit/utils/markdown.js';
|
||||
import PrismJS from 'prismjs';
|
||||
import 'prismjs/components/prism-bash.js';
|
||||
import 'prismjs/components/prism-diff.js';
|
||||
import 'prism-svelte';
|
||||
|
||||
export function highlight(source, lang) {
|
||||
const plang = langs[lang] || '';
|
||||
const highlighted = plang ? PrismJS.highlight(
|
||||
source,
|
||||
PrismJS.languages[plang],
|
||||
lang,
|
||||
) : source.replace(/[&<>]/g, c => ({ '&': '&', '<': '<', '>': '>' })[c]);
|
||||
|
||||
return `<pre class='language-${plang}'><code>${highlighted}</code></pre>`;
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
import {SLUG_SEPARATOR} from '../../config.js';
|
||||
|
||||
/* url-safe processor */
|
||||
|
||||
export const urlsafeSlugProcessor = string =>
|
||||
slugify(string, {
|
||||
customReplacements: [ // runs before any other transformations
|
||||
['$', 'DOLLAR'], // `$destroy` & co
|
||||
['-', 'DASH'], // conflicts with `separator`
|
||||
],
|
||||
separator: SLUG_SEPARATOR,
|
||||
decamelize: false,
|
||||
lowercase: false
|
||||
})
|
||||
.replace(/DOLLAR/g, '$')
|
||||
.replace(/DASH/g, '-');
|
||||
|
||||
/* unicode-preserver processor */
|
||||
|
||||
const alphaNumRegex = /[a-zA-Z0-9]/;
|
||||
const unicodeRegex = /\p{Letter}/u;
|
||||
const isNonAlphaNumUnicode =
|
||||
string => !alphaNumRegex.test(string) && unicodeRegex.test(string);
|
||||
|
||||
export const unicodeSafeProcessor = string =>
|
||||
string.split('')
|
||||
.reduce((accum, char, index, array) => {
|
||||
const type = isNonAlphaNumUnicode(char) ? 'pass' : 'process';
|
||||
|
||||
if (index === 0) {
|
||||
accum.current = {type, string: char};
|
||||
} else if (type === accum.current.type) {
|
||||
accum.current.string += char;
|
||||
} else {
|
||||
accum.chunks.push(accum.current);
|
||||
accum.current = {type, string: char};
|
||||
}
|
||||
|
||||
if (index === array.length - 1) {
|
||||
accum.chunks.push(accum.current);
|
||||
}
|
||||
|
||||
return accum;
|
||||
}, {chunks: [], current: {type: '', string: ''}})
|
||||
.chunks
|
||||
.reduce((accum, chunk) => {
|
||||
const processed = chunk.type === 'process'
|
||||
? urlsafeSlugProcessor(chunk.string)
|
||||
: chunk.string;
|
||||
|
||||
processed.length > 0 && accum.push(processed);
|
||||
|
||||
return accum;
|
||||
}, [])
|
||||
.join(SLUG_SEPARATOR);
|
||||
|
||||
/* processor */
|
||||
|
||||
export const makeSlugProcessor = (preserveUnicode = false) => preserveUnicode
|
||||
? unicodeSafeProcessor
|
||||
: urlsafeSlugProcessor;
|
||||
|
||||
/* session processor */
|
||||
|
||||
export const makeSessionSlugProcessor = (preserveUnicode = false) => {
|
||||
const processor = makeSlugProcessor(preserveUnicode);
|
||||
const seen = new Set();
|
||||
|
||||
return string => {
|
||||
const slug = processor(string);
|
||||
|
||||
if (seen.has(slug)) throw new Error(`Duplicate slug ${slug}`);
|
||||
seen.add(slug);
|
||||
|
||||
return slug;
|
||||
};
|
||||
};
|
Before Width: | Height: | Size: 841 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 810 B |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 2.9 KiB |