First commit
Explore Svelte and Sapper
This commit is contained in:
5
src/client.js
Normal file
5
src/client.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import * as sapper from '@sapper/app'
|
||||
|
||||
sapper.start({
|
||||
target: document.getElementById('site')
|
||||
})
|
||||
14
src/components/Nav.svelte
Normal file
14
src/components/Nav.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
export let segment;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a class:selected='{segment === undefined}' href='.'>home</a></li>
|
||||
<li><a class:selected='{segment === "about"}' href='about'>about</a></li>
|
||||
|
||||
<!-- for the blog link, we're using rel=prefetch so that Sapper prefetches
|
||||
the blog data when we hover over the link or tap it on a touchscreen -->
|
||||
<li><a rel=prefetch class:selected='{segment === "blog"}' href='blog'>blog</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
36
src/components/Photo.svelte
Normal file
36
src/components/Photo.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { slide } from 'svelte/transition'
|
||||
import dayjs from 'dayjs'
|
||||
import advancedFormat from 'dayjs/plugin/advancedFormat'
|
||||
import * as fn from '../functions'
|
||||
|
||||
export let photo
|
||||
export let index
|
||||
export let location
|
||||
|
||||
// Manipulate data
|
||||
const thumbnail = photo.image.data.thumbnails.find(t => t.url.includes('key=large'))
|
||||
index = (index < 10 ? '0' : null) + index
|
||||
|
||||
// Format the date
|
||||
dayjs.extend(advancedFormat)
|
||||
const date = dayjs(photo.created_on).format('MMM Do, YYYY')
|
||||
</script>
|
||||
|
||||
<div class="card" in:slide out:slide>
|
||||
<div class="card-image">
|
||||
<figure class="image is-4by3">
|
||||
<img src="{thumbnail.url}" alt="{photo.name}, {location.name}, {location.country.name}" width="{thumbnail.width}" height="{thumbnail.height}">
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<h4 class="title is-4">{photo.name}</h4>
|
||||
<p>{location.name}, {location.country.name}</p>
|
||||
</div>
|
||||
|
||||
<footer class="card-footer">
|
||||
<span class="card-footer-item">{date}</span>
|
||||
<span class="card-footer-item">#{index}</span>
|
||||
</footer>
|
||||
</div>
|
||||
38
src/functions.js
Normal file
38
src/functions.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
** Get API function
|
||||
*/
|
||||
export const api = async (query) => {
|
||||
const res = await fetch('http://api.housesof.localhost/how/gql?access_token=NJk0urljsdSvApUDzWxGgoO6', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: `{${query}}` })
|
||||
})
|
||||
const data = await res.json()
|
||||
return data.data
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** Load API function
|
||||
*/
|
||||
export const loadAPI = async (requests = []) => {
|
||||
// Fetch all requests
|
||||
return await Promise.all(requests.map(req => api(req)
|
||||
.then(res => res.json())
|
||||
.then(data => data.data)
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** Slufigy a string
|
||||
*/
|
||||
export const slug = string => {
|
||||
return string.toString().toLowerCase().trim()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/&/g, '-and-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
}
|
||||
21
src/parts/Footer.svelte
Normal file
21
src/parts/Footer.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import { site } from '../store'
|
||||
</script>
|
||||
|
||||
<!-- <pre>{JSON.stringify($site, '', 2)}</pre> -->
|
||||
|
||||
<footer class="footer">
|
||||
<div class="left">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<div class="instagram">
|
||||
<a href="https://instagram.com/cetrucflotte" target="_blank">Instagram</a>
|
||||
</div>
|
||||
|
||||
<div class="ctf">
|
||||
<a href="https://cetrucflotte.com" target="_blank">A project by <img src="/assets/img/logo-cetrucflotte.png" alt="Cetrucflotte" width="118" height="22"></a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
40
src/routes/_error.svelte
Normal file
40
src/routes/_error.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
export let status;
|
||||
export let error;
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1, p {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.8em;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
font-size: 4em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>{status}</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>{status}</h1>
|
||||
|
||||
<p>{error.message}</p>
|
||||
|
||||
{#if dev && error.stack}
|
||||
<pre>{error.stack}</pre>
|
||||
{/if}
|
||||
86
src/routes/_layout.svelte
Normal file
86
src/routes/_layout.svelte
Normal file
@@ -0,0 +1,86 @@
|
||||
<script context="module">
|
||||
import { locations, countries, site } from '../store'
|
||||
|
||||
export async function preload (page, segment) {
|
||||
const res = await this.fetch('http://api.housesof.localhost/how/gql?access_token=NJk0urljsdSvApUDzWxGgoO6', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: `{
|
||||
site {
|
||||
data {
|
||||
description
|
||||
explore_globe
|
||||
explore_list
|
||||
instagram
|
||||
}
|
||||
}
|
||||
countries {
|
||||
data {
|
||||
name
|
||||
flag { metadata }
|
||||
}
|
||||
}
|
||||
locations (filter: { status_eq: "published" }) {
|
||||
data {
|
||||
name
|
||||
region
|
||||
country { name }
|
||||
}
|
||||
}
|
||||
}`})
|
||||
})
|
||||
const data = await res.json()
|
||||
countries.set(data.data.countries.data)
|
||||
locations.set(data.data.locations.data)
|
||||
site.set(data.data.site.data[0])
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import '../../node_modules/bulma/css/bulma.min.css'
|
||||
import * as fn from '../functions'
|
||||
|
||||
// import SwitcherLarge from '../components/Photos.svelte'
|
||||
</script>
|
||||
|
||||
<nav class="navbar is-danger" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
<strong>Houses Of</strong>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link" href="/">Locations</a>
|
||||
<div class="navbar-dropdown">
|
||||
{#each $locations as location}
|
||||
<a class="navbar-item" href="/location/{fn.slug(location.name)}">{location.name}</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="content columns">
|
||||
<div class="column is-7">
|
||||
<p><strong>Houses Of</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="column columns">
|
||||
<p class="column">
|
||||
<a href="https://instagram.com/{$site.instagram}" target="_blank">Instagram</a>
|
||||
</p>
|
||||
<p class="column">
|
||||
<a href="https://cetrucflotte.com" target="_blank">A project by Cetrucflotte</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
7
src/routes/about.svelte
Normal file
7
src/routes/about.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>About this site</h1>
|
||||
|
||||
<p>This is the 'about' page. There's not much here.</p>
|
||||
29
src/routes/index.svelte
Normal file
29
src/routes/index.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import { slide } from 'svelte/transition'
|
||||
import { locations, countries, site } from '../store'
|
||||
import * as fn from '../functions'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Houses Of - Beautiful houses of planet Earth</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container" in:slide out:slide>
|
||||
<section class="section">
|
||||
<h1 class="title is-2">Houses Of</h1>
|
||||
<article class="message is-dark">
|
||||
<div class="message-body">{$site.description}</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3 class="title is-3">Locations</h3>
|
||||
<ul>
|
||||
{#each $locations as location}
|
||||
<li>
|
||||
<a href="/location/{fn.slug(location.name)}">{location.name}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
54
src/routes/location/[location].svelte
Normal file
54
src/routes/location/[location].svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script context="module">
|
||||
export async function preload (page, session) {
|
||||
const location = await this.fetch('//api.housesof.localhost/how/gql?access_token=NJk0urljsdSvApUDzWxGgoO6', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query: `{
|
||||
locations (filter: { name_eq: "${page.params.location}" }) {
|
||||
data {
|
||||
name
|
||||
region
|
||||
country { name }
|
||||
description
|
||||
}
|
||||
}
|
||||
}`})
|
||||
})
|
||||
const photos = await this.fetch(`http://api.housesof.localhost/how/items/photos?fields=id,name,image.data,created_on&filter[location.name][rlike]=%${page.params.location}%`)
|
||||
const locationData = await location.json()
|
||||
const photosData = await photos.json()
|
||||
return {
|
||||
location: locationData.data.locations.data[0],
|
||||
photos: photosData.data
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { slide } from 'svelte/transition'
|
||||
import Photo from '../../components/Photo.svelte'
|
||||
|
||||
export let location
|
||||
export let photos
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Houses Of - Beautiful houses of {location.name}, {location.country.name}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="section container" in:slide out:slide>
|
||||
<div class="content">
|
||||
<h1 class="title is-2">{location.name}, {location.country.name}</h1>
|
||||
{#if location.description}
|
||||
<p>Houses Of {location.name} {location.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
{#each photos as photo, index (photo.id)}
|
||||
<div class="column">
|
||||
<Photo {photo} {location} index={index + 1} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
17
src/server.js
Normal file
17
src/server.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import sirv from 'sirv'
|
||||
import polka from 'polka'
|
||||
import compression from 'compression'
|
||||
import * as sapper from '@sapper/server'
|
||||
|
||||
const { PORT, NODE_ENV } = process.env
|
||||
const dev = NODE_ENV === 'development'
|
||||
|
||||
polka() // You can also use Express
|
||||
.use(
|
||||
compression({ threshold: 0 }),
|
||||
sirv('static', { dev }),
|
||||
sapper.middleware()
|
||||
)
|
||||
.listen(PORT, err => {
|
||||
if (err) console.log('error', err)
|
||||
})
|
||||
82
src/service-worker.js
Normal file
82
src/service-worker.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { timestamp, files, shell, routes } from '@sapper/service-worker'
|
||||
|
||||
const ASSETS = `cache${timestamp}`
|
||||
|
||||
// `shell` is an array of all the files generated by the bundler,
|
||||
// `files` is an array of everything in the `static` directory
|
||||
const to_cache = shell.concat(files)
|
||||
const cached = new Set(to_cache)
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.open(ASSETS)
|
||||
.then(cache => cache.addAll(to_cache))
|
||||
.then(() => {
|
||||
self.skipWaiting()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(async keys => {
|
||||
// delete old caches
|
||||
for (const key of keys) {
|
||||
if (key !== ASSETS) await caches.delete(key)
|
||||
}
|
||||
|
||||
self.clients.claim()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method !== 'GET' || event.request.headers.has('range')) return
|
||||
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
// don't try to handle e.g. data: URIs
|
||||
if (!url.protocol.startsWith('http')) return
|
||||
|
||||
// ignore dev server requests
|
||||
if (url.hostname === self.location.hostname && url.port !== self.location.port) return
|
||||
|
||||
// always serve static files and bundler-generated assets from cache
|
||||
if (url.host === self.location.host && cached.has(url.pathname)) {
|
||||
event.respondWith(caches.match(event.request))
|
||||
return
|
||||
}
|
||||
|
||||
// for pages, you might want to serve a shell `service-worker-index.html` file,
|
||||
// which Sapper has generated for you. It's not right for every
|
||||
// app, but if it's right for yours then uncomment this section
|
||||
/*
|
||||
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
|
||||
event.respondWith(caches.match('/service-worker-index.html'))
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
if (event.request.cache === 'only-if-cached') return
|
||||
|
||||
// for everything else, try the network first, falling back to
|
||||
// cache if the user is offline. (If the pages never change, you
|
||||
// might prefer a cache-first approach to a network-first one.)
|
||||
event.respondWith(
|
||||
caches
|
||||
.open(`offline${timestamp}`)
|
||||
.then(async cache => {
|
||||
try {
|
||||
const response = await fetch(event.request)
|
||||
cache.put(event.request, response.clone())
|
||||
return response
|
||||
} catch(err) {
|
||||
const response = await cache.match(event.request)
|
||||
if (response) return response
|
||||
|
||||
throw err
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
15
src/store.js
Normal file
15
src/store.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as fn from './functions'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Site related
|
||||
========================================================================== */
|
||||
export let currentLocation = {}
|
||||
export let loaded = writable(false)
|
||||
|
||||
// Data
|
||||
export let locations = writable()
|
||||
export let countries = writable()
|
||||
export let site = writable()
|
||||
17
src/template.html
Normal file
17
src/template.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
%sapper.base%
|
||||
%sapper.styles%
|
||||
%sapper.head%
|
||||
</head>
|
||||
<body>
|
||||
<div id="site">%sapper.html%</div>
|
||||
|
||||
%sapper.scripts%
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user