mirror of https://github.com/sveltejs/svelte
Merge pull request #2680 from sveltejs/site/database-with-fallback
add database entries for new gists, update REPL URLspull/2572/head
commit
d654b34959
@ -1,65 +0,0 @@
|
||||
import send from '@polka/send';
|
||||
import { body } from './_utils.js';
|
||||
import { query } from '../../utils/db';
|
||||
import { isUser } from '../../backend/auth';
|
||||
|
||||
export async function get(req, res) {
|
||||
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
|
||||
`, [req.params.id]); // via filename pattern
|
||||
|
||||
if (!row) {
|
||||
return send(res, 404, { error: 'Gist not found' });
|
||||
}
|
||||
|
||||
send(res, 200, {
|
||||
uid: row.uid,
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: row.owner
|
||||
});
|
||||
}
|
||||
|
||||
export async function patch(req, res) {
|
||||
const user = await isUser(req, res);
|
||||
if (!user) return; // response already sent
|
||||
|
||||
let id, uid=req.params.id;
|
||||
|
||||
try {
|
||||
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
|
||||
if (!row) return send(res, 404, { error: 'Gist not found' });
|
||||
if (row.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' });
|
||||
id = row.id;
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ select', err);
|
||||
return send(res, 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = await body(req);
|
||||
obj.updated_at = 'now()';
|
||||
let k, cols=[], vals=[];
|
||||
for (k in obj) {
|
||||
cols.push(k);
|
||||
vals.push(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);
|
||||
|
||||
send(res, 200, {
|
||||
uid: row.uid,
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: user.uid,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ update', err);
|
||||
send(res, 500, { error: err.message });
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { user, logout } from '../../../../user.js';
|
||||
import { user, logout } from '../../../../../user.js';
|
||||
|
||||
let showMenu = false;
|
||||
let name;
|
@ -0,0 +1,135 @@
|
||||
import send from '@polka/send';
|
||||
import redirect from '@polka/redirect';
|
||||
import body from '../_utils/body.js';
|
||||
import * as httpie from 'httpie';
|
||||
import { query, find } from '../../../utils/db';
|
||||
import { isUser } from '../../../backend/auth';
|
||||
import { get_example } from '../../examples/_examples.js';
|
||||
|
||||
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
|
||||
|
||||
async function import_gist(req, res) {
|
||||
const base = `https://api.github.com/gists/${req.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 query(`
|
||||
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...
|
||||
const [gist] = await query(`
|
||||
insert into gists(uid, user_id, name, files)
|
||||
values ($1, $2, $3, $4) returning *`, [req.params.id, user.id, data.description, JSON.stringify(files)]);
|
||||
|
||||
send(res, 200, {
|
||||
uid: req.params.id,
|
||||
name: data.description,
|
||||
files,
|
||||
owner: data.owner.id
|
||||
});
|
||||
} catch (err) {
|
||||
send(res, err.statusCode, { error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req, res) {
|
||||
// is this an example?
|
||||
const example = get_example(req.params.id);
|
||||
|
||||
if (example) {
|
||||
return send(res, 200, {
|
||||
relaxed: true,
|
||||
uid: req.params.id,
|
||||
name: example.title,
|
||||
files: example.files,
|
||||
owner: null
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
`, [req.params.id]); // via filename pattern
|
||||
|
||||
if (!row) {
|
||||
return import_gist(req, res);
|
||||
}
|
||||
|
||||
send(res, 200, {
|
||||
uid: row.uid.replace(/-/g, ''),
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: row.owner
|
||||
});
|
||||
}
|
||||
|
||||
export async function patch(req, res) {
|
||||
const user = await isUser(req, res);
|
||||
if (!user) return; // response already sent
|
||||
|
||||
let id, uid=req.params.id;
|
||||
|
||||
try {
|
||||
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);
|
||||
if (!row) return send(res, 404, { error: 'Gist not found' });
|
||||
if (row.user_id !== user.id) return send(res, 403, { error: 'Item does not belong to you' });
|
||||
id = row.id;
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ select', err);
|
||||
return send(res, 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const obj = await body(req);
|
||||
obj.updated_at = 'now()';
|
||||
let k, cols=[], 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);
|
||||
|
||||
send(res, 200, {
|
||||
uid: row.uid.replace(/-/g, ''),
|
||||
name: row.name,
|
||||
files: row.files,
|
||||
owner: user.uid,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('PATCH /gists @ update', err);
|
||||
send(res, 500, { error: err.message });
|
||||
}
|
||||
}
|
@ -0,0 +1,223 @@
|
||||
<script context="module">
|
||||
export function preload({ params, query }) {
|
||||
return {
|
||||
version: query.version || '3',
|
||||
id: params.id
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Repl from '@sveltejs/svelte-repl';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '@sapper/app';
|
||||
import { process_example } from '../../../utils/examples';
|
||||
import { user } from '../../../user.js';
|
||||
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 = process.browser ? window.innerWidth : 1000;
|
||||
let checked = false;
|
||||
|
||||
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 rgx = /(js|svelte)$/i;
|
||||
const components = data.files.map(file => {
|
||||
let [name, type] = file.name.split('.');
|
||||
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 (process.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}`);
|
||||
}
|
||||
|
||||
$: svelteUrl = process.browser && version === 'local' ?
|
||||
`${location.origin}/repl/local` :
|
||||
`https://unpkg.com/svelte@${version}`;
|
||||
|
||||
const rollupUrl = `https://unpkg.com/rollup@1/dist/rollup.browser.js`;
|
||||
|
||||
// needed for context API example
|
||||
const mapbox_setup = `window.MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN;`;
|
||||
|
||||
$: mobile = width < 540;
|
||||
|
||||
$: relaxed = is_relaxed_gist || ($user && gist && $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;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile .viewport {
|
||||
width: 200%;
|
||||
height: calc(100% - 42px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.pane { width: 100%; height: 100% }
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: var(--second);
|
||||
font-weight: 400;
|
||||
margin: 2em 0 0 0;
|
||||
opacity: 0;
|
||||
animation: fade-in .4s;
|
||||
animation-delay: .2s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0 }
|
||||
100% { opacity: 1 }
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: 2.4em 0 0 0;
|
||||
}
|
||||
</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
|
||||
on:forked={handle_fork}
|
||||
/>
|
||||
|
||||
{#if process.browser}
|
||||
<div class="viewport" class:offset={checked}>
|
||||
<Repl
|
||||
bind:this={repl}
|
||||
{svelteUrl}
|
||||
{rollupUrl}
|
||||
{relaxed}
|
||||
fixed={mobile}
|
||||
injectedJS={mapbox_setup}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if mobile}
|
||||
<InputOutputToggle bind:checked/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
@ -1,4 +1,4 @@
|
||||
export function body(req) {
|
||||
export default function body(req) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
let str = '';
|
||||
|
Loading…
Reference in new issue