update, formatting and vercel

This commit is contained in:
cupcakearmy 2021-01-02 00:21:29 +01:00
parent 4ee9c1bb8b
commit 801e1b6710
No known key found for this signature in database
GPG Key ID: D28129AE5654D9D9
23 changed files with 1456 additions and 2986 deletions

View File

@ -1,41 +0,0 @@
kind: pipeline
name: default
steps:
# - name: build
# image: node:alpine
# pull: always
# commands:
# - node -v
# - yarn -v
# - yarn
# - yarn run build
- name: deploy
image: cupcakearmy/drone-deploy
pull: always
environment:
PLUGIN_KEY:
from_secret: ssh_key
settings:
host: fantus.studio
user: root
port: 1312
target: /srv/web/fantus
sources:
- ./Dockerfile
- ./docker-compose.prod.yml
- ./yarn.lock
- ./package.json
- ./tsconfig.json
- ./next.config.js
- ./components
- ./pages
- ./public
- ./screens
- ./styles
commands:
- docker-compose -f docker-compose.prod.yml up -d --build
when:
event: push
branch: master

View File

@ -1,3 +0,0 @@
RECAPTCHA_CLIENT=client_key
RECAPTCHA_SERVER=server_key
NEXTCLOUD_TOKEN=next_cloud_share_id

3
.gitignore vendored
View File

@ -11,4 +11,5 @@ node_modules
dist dist
# Config # Config
.env .env
.vercel

View File

@ -1,12 +0,0 @@
FROM node:alpine
WORKDIR /app
COPY package.json .
COPY yarn.lock .
RUN yarn
COPY . ./
RUN yarn run build
CMD [ "yarn", "run", "start" ]

View File

@ -4,5 +4,4 @@ Website for fantus
## Stack ## Stack
- nextjs Next.js hosted on Vercel
- directus headless cms

View File

@ -1,9 +1,8 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { PerspectiveCamera, Scene, BufferGeometry, BufferAttribute, ShaderMaterial, Color, Points, WebGLRenderer, Camera } from 'three' import { PerspectiveCamera, Scene, BufferGeometry, BufferAttribute, ShaderMaterial, Color, Points, WebGLRenderer } from 'three'
import '../styles/backgorund.styl' import '../styles/backgorund.styl'
// const SEPARATION = 150, AMOUNTX = 200, AMOUNTY = 200, HEIGHT = 75
const SEPARATION = 150, AMOUNTX = 50, AMOUNTY = 50, HEIGHT = 75 const SEPARATION = 150, AMOUNTX = 50, AMOUNTY = 50, HEIGHT = 75
let container: HTMLElement | null let container: HTMLElement | null

View File

@ -4,9 +4,7 @@ import '../styles/content.styl'
const Content: React.FC = ({ children }) => { const Content: React.FC = ({ children }) => {
return <div className='container'> return <div className='container'>
{/* <div className='content'> */}
{children} {children}
{/* </div> */}
</div> </div>
} }

View File

@ -1,48 +0,0 @@
version: '3.7'
networks:
traefik:
external: true
services:
front:
build: .
restart: unless-stopped
ports:
- 80
networks:
- traefik
labels:
- 'traefik.enable=true'
- 'traefik.backend=fantus'
- 'traefik.docker.network=traefik'
- 'traefik.frontend.rule=Host:fantus.studio'
- 'traefik.port=80'
mysql:
image: mysql:5.7
restart: unless-stopped
env_file: .directus
ports:
- 3306
volumes:
- ./data/db:/var/lib/mysql
directus:
image: directus/directus:v8-apache
restart: unless-stopped
env_file: .directus
volumes:
- ./data/config:/var/directus/config
- ./data/uploads:/var/directus/public/uploads
- ./custom.ini:/usr/local/etc/php/conf.d/custom.ini
networks:
- traefik
- default
labels:
- "traefik.enable=true"
- "traefik.port=80"
- "traefik.docker.network=traefik"
- "traefik.backend=fantus-api"
- "traefik.frontend.rule=Host:api.fantus.studio"

View File

@ -1,10 +0,0 @@
version: '3.7'
services:
front:
build: .
ports:
- 80:80
volumes:
- ./.next:/app/.next:ro

View File

@ -1,14 +1,4 @@
/* jshint esversion:8, asi:true */
const withStylus = require('@zeit/next-stylus') const withStylus = require('@zeit/next-stylus')
const withCss = require('@zeit/next-css') const withCss = require('@zeit/next-css')
const config = require('dotenv').config().parsed || {} module.exports = withCss(withStylus({}))
const { RECAPTCHA_CLIENT, RECAPTCHA_SERVER } = config
module.exports = withCss(withStylus({
publicRuntimeConfig: {
RECAPTCHA_CLIENT,
},
serverRuntimeConfig: config,
}))

View File

@ -6,11 +6,9 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.19.1", "axios": "^0.19.1",
"dotenv": "^8.2.0",
"formhero": "^0.0.7", "formhero": "^0.0.7",
"formidable": "^1.2.1", "formidable": "^1.2.1",
"next": "^9.1.7", "next": "^10.0.4",
"nodemailer": "^6.4.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"tachyons": "^4.11.1", "tachyons": "^4.11.1",
@ -19,12 +17,11 @@
"devDependencies": { "devDependencies": {
"@types/formidable": "^1.0.31", "@types/formidable": "^1.0.31",
"@types/node": "^13.1.6", "@types/node": "^13.1.6",
"@types/nodemailer": "^6.4.0",
"@types/react": "^16.9.17", "@types/react": "^16.9.17",
"@types/react-dom": "^16.9.4", "@types/react-dom": "^16.9.4",
"@zeit/next-css": "^1.0.1", "@zeit/next-css": "^1.0.1",
"@zeit/next-stylus": "^1.0.1", "@zeit/next-stylus": "^1.0.1",
"stylus": "^0.54.7", "stylus": "^0.54.7",
"typescript": "^3.7.4" "typescript": "^4"
} }
} }

View File

@ -2,7 +2,6 @@ import React from 'react'
import App from 'next/app' import App from 'next/app'
import Head from 'next/head' import Head from 'next/head'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import getConfig from 'next/config'
import 'tachyons/css/tachyons.min.css' import 'tachyons/css/tachyons.min.css'
import '../styles/app.styl' import '../styles/app.styl'
@ -10,28 +9,29 @@ import Menu from '../screens/menu'
import Content from '../components/content' import Content from '../components/content'
const Background = dynamic(() => import('../components/background'), { ssr: false }) const Background = dynamic(() => import('../components/background'), { ssr: false })
const { RECAPTCHA_CLIENT } = getConfig().publicRuntimeConfig
export default class extends App { export default class extends App {
render() { render() {
const { Component, pageProps } = this.props const { Component, pageProps } = this.props
return ( return (
<React.Fragment> <React.Fragment>
<Head> <Head>
<title>fantus</title> <title>fantus</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="description" content="fantus - producer, dj, engineer" /> <meta name="description" content="fantus - producer, dj, engineer" />
<meta name="keywords" content="dj,producer,mastering,service,free,sets,mix,techno,music,set" /> <meta name="keywords" content="dj,producer,mastering,service,free,sets,mix,techno,music,set" />
<link href="https://fonts.googleapis.com/css?family=Space+Mono:400,700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css?family=Space+Mono:400,700&display=swap" rel="stylesheet" />
<script src={'https://www.google.com/recaptcha/api.js?render=' + RECAPTCHA_CLIENT}></script> <script
</Head> src={'https://www.google.com/recaptcha/api.js?render=' + process.env.NEXT_PUBLIC_RECAPTCHA_CLIENT}
<Menu /> ></script>
<Background /> </Head>
<Content> <Menu />
<Component {...pageProps} /> <Background />
</Content> <Content>
</React.Fragment> <Component {...pageProps} />
) </Content>
} </React.Fragment>
} )
}
}

View File

@ -1,17 +1,29 @@
import Document, { Html, Head, Main, NextScript } from 'next/document' import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class extends Document { export default class extends Document {
render() { render() {
return ( return (
<Html lang='en'> <Html lang="en">
<Head /> <Head />
<body> <body>
<script type="x-shader/x-vertex" id="vertexshader" dangerouslySetInnerHTML={{ __html: `attribute float scale;void main() {vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_PointSize = scale * ( 300.0 / - mvPosition.z );gl_Position = projectionMatrix * mvPosition;}` }}></script> <script
<script type="x-shader/x-fragment" id="fragmentshader" dangerouslySetInnerHTML={{ __html: `uniform vec3 color;void main() {if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;gl_FragColor = vec4( color, 1.0 );}` }}></script> type="x-shader/x-vertex"
<Main /> id="vertexshader"
<NextScript /> dangerouslySetInnerHTML={{
</body> __html: `attribute float scale;void main() {vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_PointSize = scale * ( 300.0 / - mvPosition.z );gl_Position = projectionMatrix * mvPosition;}`,
</Html> }}
) ></script>
} <script
} type="x-shader/x-fragment"
id="fragmentshader"
dangerouslySetInnerHTML={{
__html: `uniform vec3 color;void main() {if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;gl_FragColor = vec4( color, 1.0 );}`,
}}
></script>
<Main />
<NextScript />
</body>
</Html>
)
}
}

View File

@ -1,33 +1,45 @@
import React from 'react' import React from 'react'
const Home = () => { const Home = () => {
return <div className='center color'> return (
<h1 className='ma0'>about</h1> <div className="center color">
<div className='text'> <h1 className="ma0">about</h1>
<p> <div className="text">
yet another producer. because we don't already have enough of them. <p>
<br /> yet another producer. because we don't already have enough of them.
<br /> <br />
📍based on planet earth <br />
<br /> 📍based on planet earth
<br />
</p> </p>
<p> <p>
<a target='_blank' href='https://www.mixcloud.com/fantus/'>mixcloud</a> <a target="_blank" href="https://www.mixcloud.com/fantus/">
<br /> mixcloud
<a target='_blank' href='https://soundcloud.com/fantus_music'>soundcloud</a> </a>
<br /> <br />
<a target='_blank' href='https://www.instagram.com/fantus.studio/'>instagram</a> <a target="_blank" href="https://soundcloud.com/fantus_music">
<br /> soundcloud
<br /> </a>
<a target='_blank' href='mailto:hi@fantus.studio'>contact email</a> <br />
</p> <a target="_blank" href="https://www.instagram.com/fantus.studio/">
<br /> instagram
<p> </a>
website made by 🤖 with . <a target='_blank' href="https://github.com/cupcakearmy/fantus">source code available here</a> <br />
</p> <br />
</div> <a target="_blank" href="mailto:hi@fantus.studio">
contact email
</a>
</p>
<br />
<p>
website made by 🤖 with .{' '}
<a target="_blank" href="https://github.com/cupcakearmy/fantus">
source code available here
</a>
</p>
</div>
</div> </div>
)
} }
export default Home export default Home

View File

@ -1,83 +1,77 @@
import { createReadStream, statSync, unlinkSync, writeFileSync } from 'fs' import { createReadStream, statSync, unlinkSync, writeFileSync } from 'fs'
import { tmpdir } from 'os' import { tmpdir } from 'os'
import { NextApiRequest, NextApiResponse } from 'next' import type { NextApiRequest, NextApiResponse } from 'next'
import getConfig from 'next/config'
import Formidable from 'formidable' import Formidable from 'formidable'
import axios from 'axios' import axios from 'axios'
const { RECAPTCHA_SERVER, NEXTCLOUD_TOKEN } = getConfig().serverRuntimeConfig
const sendFileAndDelete = async (name: string, path: string) => { const sendFileAndDelete = async (name: string, path: string) => {
const stat = statSync(path) const stat = statSync(path)
const stream = createReadStream(path) const stream = createReadStream(path)
await axios({ await axios({
url: 'https://cloud.nicco.io/public.php/webdav/' + name, url: 'https://cloud.nicco.io/public.php/webdav/' + name,
method: 'put', method: 'put',
auth: { auth: {
username: NEXTCLOUD_TOKEN, username: process.env.NEXTCLOUD_TOKEN || '',
password: '', password: '',
}, },
headers: { headers: {
'Content-Length': stat.size 'Content-Length': stat.size,
}, },
data: stream data: stream,
}) maxContentLength: Infinity,
})
unlinkSync(path) unlinkSync(path)
} }
export default async (req: NextApiRequest, res: NextApiResponse) => { export default async (req: NextApiRequest, res: NextApiResponse) => {
try { try {
// @ts-ignore
const form = new Formidable()
// @ts-ignore form.maxFileSize = 300 * 1024 * 1024 // 300MiB
const form = new Formidable();
form.maxFileSize = 300 * 1024 * 1024 // 300MiB const body = await new Promise<any>((resolve, reject) => {
form.parse(req, (err: any, fields: any, files: any) => {
if (err) reject()
else
resolve({
fields: JSON.parse(fields.json),
files: Object.values(files),
})
})
})
const body = await new Promise<any>((resolve, reject) => { const { token, ...rest } = body.fields
form.parse(req, (err: any, fields: any, files: any) => {
if (err) reject()
else resolve({
fields: JSON.parse(fields.json),
files: Object.values(files)
})
});
})
const { data } = await axios({
url: 'https://www.google.com/recaptcha/api/siteverify',
method: 'post',
params: {
secret: process.env.RECAPTCHA_SERVER,
response: token,
},
})
if (!data.success) throw new Error()
const { token, ...rest } = body.fields const now = Date.now()
for (const file of body.files) await sendFileAndDelete(`${now}_${file.name}`, file.path)
const { data } = await axios({ const txtFile = `${tmpdir()}/text`
url: 'https://www.google.com/recaptcha/api/siteverify', writeFileSync(txtFile, `${rest.contact}\n${rest.description}`)
method: 'post', await sendFileAndDelete(`${now}_details.txt`, txtFile)
params: {
secret: RECAPTCHA_SERVER,
response: token,
},
})
if (!data.success) throw new Error() res.status(200).end()
} catch {
const now = Date.now() res.status(400).end()
for (const file of body.files) }
sendFileAndDelete(`${now}_${file.name}`, file.path)
const txtFile = `${tmpdir()}/text`
writeFileSync(txtFile, `${rest.contact}\n${rest.description}`)
sendFileAndDelete(`${now}_details.txt`, txtFile)
res.status(200).end()
} catch {
res.status(400).end()
}
} }
export const config = { export const config = {
api: { api: {
bodyParser: false, bodyParser: false,
}, },
} }

View File

@ -1,27 +1,37 @@
import React from 'react' import React from 'react'
const Home = () => { const Home = () => {
return <div className='center'> return (
<h2>neodymium enabled waves. new.</h2> <div className="center">
<p> <h2>neodymium enabled waves. new.</h2>
<b><u>neodymium:</u> </b> <p>
<cite> <b>
a bright, silvery, reactive rare-earth element, found in monazite and bastnaesite and used for coloring glass, for doping laser glass and crystals, and in materials with strong, permanent magnetic properties that make them useful for computer and audio equipment. Atomic number 60; atomic weight 144.24; melting point 1,016°C; boiling point 3,074°C; <u>neodymium:</u>{' '}
</cite> </b>
</p> <cite>
<p> a bright, silvery, reactive rare-earth element, found in monazite and bastnaesite and used for coloring glass,
<b><u>enabled:</u> </b> for doping laser glass and crystals, and in materials with strong, permanent magnetic properties that make
<cite> them useful for computer and audio equipment. Atomic number 60; atomic weight 144.24; melting point 1,016°C;
to make able; give power, means, competence, or ability to; authorize boiling point 3,074°C;
</cite> </cite>
</p> </p>
<p> <p>
<b><u>wave:</u> </b> <b>
<cite> <u>enabled:</u>{' '}
the continuous, repeating pattern in which some types of energy, such as sound, light, and heat, are spread or carried </b>
</cite> <cite>to make able; give power, means, competence, or ability to; authorize</cite>
</p> </p>
<p>
<b>
<u>wave:</u>{' '}
</b>
<cite>
the continuous, repeating pattern in which some types of energy, such as sound, light, and heat, are spread or
carried
</cite>
</p>
</div> </div>
)
} }
export default Home export default Home

View File

@ -3,34 +3,38 @@ import React from 'react'
import Form from '../screens/form' import Form from '../screens/form'
const Home = () => { const Home = () => {
return <div className='center color'> return (
<h1 className='ma0'>mastering</h1> <div className="center color">
<div> <h1 className="ma0">mastering</h1>
<h3>how does it work?</h3> <div>
<p> <h3>how does it work?</h3>
My aim is to practise mastering. <p>
As anything it requires a lot of trial and error. So the idea is as follows: My aim is to practise mastering. As anything it requires a lot of trial and error. So the idea is as follows:
</p> </p>
<ol> <ol>
<li>You send me a track</li> <li>You send me a track</li>
<li>It gets mastered based on your wishes & feedback</li> <li>It gets mastered based on your wishes & feedback</li>
<li>If you like it, you are free to use the master for free. Otherwise just trash it.</li> <li>If you like it, you are free to use the master for free. Otherwise just trash it.</li>
</ol> </ol>
<h3>what's the catch then?</h3> <h3>what's the catch then?</h3>
<p> <p>
Nothing really. It's <b><i>completely free</i></b> for you. Nothing really. It's{' '}
It goes without saying that of course I will not upload the tracks anywhere. <b>
No surprises. <i>completely free</i>
</p> </b>{' '}
<h3>what if you like the result?</h3> for you. It goes without saying that of course I will not upload the tracks anywhere. No surprises.
<p> </p>
If you think the master is good you can use it for free withouth any royalties to me. <h3>what if you like the result?</h3>
It would be cool if you could reference me in the youtube description or spotify credits for mastering it, but it's totally optional and up to you. <p>
</p> If you think the master is good you can use it for free withouth any royalties to me. It would be cool if you
</div> could reference me in the youtube description or spotify credits for mastering it, but it's totally optional
<br /> and up to you.
<Form /> </p>
</div>
<br />
<Form />
</div> </div>
)
} }
export default Home export default Home

View File

@ -1,31 +1,56 @@
import React, { useState, useEffect } from 'react' import type { NextPage, GetStaticProps } from 'next'
import { NextPage } from 'next' import React from 'react'
import axios from 'axios' import axios from 'axios'
const Home: NextPage = () => { type Props = {
slugs: string[]
const [links, setLinks] = useState([] as string[])
useEffect(() => {
axios.get('https://api.fantus.studio/directus/items/mixes?status=published')
.then(({ data }) => {
setLinks(data.data.map((entry: any) => entry.link))
})
}, [])
return <div className='sets'>
<h1 className='ma0'>sets</h1>
<p>
collection of some sets made here and there.
</p>
<ul>
{links.map(link => (
<li key={link}>
<iframe width="100%" height="120" src={link} />
</li>
))}
</ul>
</div>
} }
export default Home const Home: NextPage<Props> = ({ slugs }) => {
const encoded = slugs.map((slug) => encodeURIComponent(`/fantus/${slug}/`))
return (
<div className="sets">
<h1 className="ma0">sets</h1>
<p>collection of some sets made here and there.</p>
<ul>
{encoded.map((link) => (
<li key={link}>
<iframe
width="100%"
height="120"
src={`https://www.mixcloud.com/widget/iframe/?hide_cover=1&feed=${link}`}
/>
</li>
))}
</ul>
</div>
)
}
export default Home
export const getStaticProps: GetStaticProps<Props> = async (context) => {
const { data } = await axios({
url: 'https://www.mixcloud.com/graphql',
method: 'post',
data: {
query: `
query UserUploadsQuery($lookup: UserLookup!, $orderBy: CloudcastOrderByEnum) {
user: userLookup(lookup: $lookup) {
uploads(first: 100, orderBy: $orderBy) {
edges {
node {
slug
}
}
}
}
}
`,
variables: { lookup: { username: 'fantus' }, orderBy: 'LATEST' },
},
})
const slugs: string[] = data.data.user.uploads.edges.map((item: any) => item.node.slug)
return { props: { slugs } }
}

View File

@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
const Home = () => { const Home = () => {
return <div className='center'> return (
<h2>works</h2> <div className="center">
<p> <h2>works</h2>
in progress... lot of skratches lying around <p>in progress... lot of skratches lying around</p>
</p>
</div> </div>
)
} }
export default Home export default Home

View File

@ -1,153 +1,141 @@
import React, { useState, useRef, useCallback } from 'react' import React, { useState, useCallback } from 'react'
import { useForm } from 'formhero' import { useForm } from 'formhero'
import axios from 'axios'
import getConfig from 'next/config'
const { RECAPTCHA_CLIENT } = getConfig().publicRuntimeConfig
const initial = { const initial = {
contact: '', contact: '',
description: '', description: '',
}
const ab2str = (buf: any): string => {
return String.fromCharCode.apply(null, buf);
} }
const Form: React.FC = () => { const Form: React.FC = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [message, setMessage] = useState({ const [message, setMessage] = useState({
title: '', title: '',
error: false, error: false,
}) })
const { field, form, setForm } = useForm(initial) const { field, form, setForm } = useForm(initial)
const [files, setFiles] = useState([] as File[]) const [files, setFiles] = useState([] as File[])
const _onFileChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => { const _onFileChange = useCallback(
setMessage({ title: '', error: false }) async (e: React.ChangeEvent<HTMLInputElement>) => {
const uploaded = Array.from(e.target.files || []) setMessage({ title: '', error: false })
const uploaded = Array.from(e.target.files || [])
const nonAudio = uploaded.find(file => !/^audio\//.test(file.type)) const nonAudio = uploaded.find((file) => !/^audio\//.test(file.type))
if (nonAudio) { if (nonAudio) {
setMessage({ setMessage({
title: `Error: ${nonAudio.name} You can only select audio files.`, title: `Error: ${nonAudio.name} You can only select audio files.`,
error: true, error: true,
})
return
}
setFiles([...files, ...uploaded])
}, [files])
const _clear = useCallback(() => {
setFiles([])
}, [])
const _submit = useCallback((e: React.FormEvent) => {
e.preventDefault()
if (loading) return
// @ts-ignore
window.grecaptcha.ready(() => {
// @ts-ignore
window.grecaptcha.execute(RECAPTCHA_CLIENT, { action: 'homepage' }).then(function (token) {
setLoading(true)
setMessage({ title: '', error: false })
const body = new FormData()
body.append('json', JSON.stringify({
...form,
token,
}))
for (const file of files) {
body.append(file.name, file)
}
fetch('/api/form', {
method: 'POST',
body
})
.then(() => {
setForm(initial)
setFiles([])
setMessage({ title: 'Uploaded 🚀', error: false })
})
.catch(() => setMessage({ title: 'Something went wrong 😥', error: true }))
.finally(() => setLoading(false))
})
}) })
}, [form, files, loading]) return
}
return ( setFiles([...files, ...uploaded])
<form onSubmit={_submit}> },
<div className='body'> [files]
<h3 className='ma0 mb3'>submit track</h3> )
<label className='text'>
<small>contact email</small>
<input
type='email'
disabled={loading}
placeholder={'me@example.org'}
{...field('contact')}
/>
</label>
<br />
<label className='text'>
<small>description</small>
<textarea
rows={2}
placeholder='reference trakcs, comments, ...'
disabled={loading}
{...field('description')}
/>
</label>
<br />
<label className='file'>
<input
type='file'
multiple
disabled={loading}
onChange={_onFileChange}
/>
upload tracks [max. 300MiB]
</label>
{files.length > 0 && (
<div>
<input onClick={_clear} type='button' value='clear all' />
<ul>
{files.map((file, i) => <li key={i}>
{file.name}
</li>)}
</ul>
</div>
)}
<br />
<input
type='submit'
value={loading ? 'uploading...' : 'submit'}
disabled={loading}
/>
{!!message.title && ( const _clear = useCallback(() => {
<div> setFiles([])
<br /> }, [])
<div className={`pa2 ba br1 ${message.error ? 'b--red' : 'b--light-blue'}`}>{message.title}</div>
</div>
)}
</div>
<div className='grc'> const _submit = useCallback(
This site is protected by reCAPTCHA and the Google&nbsp; (e: React.FormEvent) => {
<a href="https://policies.google.com/privacy">Privacy Policy</a> and&nbsp; e.preventDefault()
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
</div> if (loading) return
</form>
) // @ts-ignore
window.grecaptcha.ready(() => {
// @ts-ignore
window.grecaptcha
.execute(process.env.NEXT_PUBLIC_RECAPTCHA_CLIENT, { action: 'homepage' })
.then(function (token: string) {
setLoading(true)
setMessage({ title: '', error: false })
const body = new FormData()
body.append(
'json',
JSON.stringify({
...form,
token,
})
)
for (const file of files) {
body.append(file.name, file)
}
fetch('/api/form', {
method: 'POST',
body,
})
.then(() => {
setForm(initial)
setFiles([])
setMessage({ title: 'Uploaded 🚀', error: false })
})
.catch(() => setMessage({ title: 'Something went wrong 😥', error: true }))
.finally(() => setLoading(false))
})
})
},
[form, files, loading]
)
return (
<form onSubmit={_submit}>
<div className="body">
<h3 className="ma0 mb3">submit track</h3>
<label className="text">
<small>contact email</small>
<input type="email" disabled={loading} placeholder={'me@example.org'} {...field('contact')} />
</label>
<br />
<label className="text">
<small>description</small>
<textarea
rows={2}
placeholder="reference trakcs, comments, ..."
disabled={loading}
{...field('description')}
/>
</label>
<br />
<label className="file">
<input type="file" multiple disabled={loading} onChange={_onFileChange} />
upload tracks [max. 300MiB]
</label>
{files.length > 0 && (
<div>
<input onClick={_clear} type="button" value="clear all" />
<ul>
{files.map((file, i) => (
<li key={i}>{file.name}</li>
))}
</ul>
</div>
)}
<br />
<input type="submit" value={loading ? 'uploading...' : 'submit'} disabled={loading} />
{!!message.title && (
<div>
<br />
<div className={`pa2 ba br1 ${message.error ? 'b--red' : 'b--light-blue'}`}>{message.title}</div>
</div>
)}
</div>
<div className="grc">
This site is protected by reCAPTCHA and the Google&nbsp;
<a href="https://policies.google.com/privacy">Privacy Policy</a> and&nbsp;
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
</div>
</form>
)
} }
export default Form export default Form

View File

@ -4,45 +4,48 @@ import Link from '../components/link'
import '../styles/menu.styl' import '../styles/menu.styl'
const HomeLink = () => <div className='home'> const HomeLink = () => (
<Link href='/'> <div className="home">
<div>fantus</div> <Link href="/">
<div>fantus</div>
</Link> </Link>
</div> </div>
)
const Menu: React.FC = () => { const Menu: React.FC = () => {
const [open, setOpen] = useState(false)
const [open, setOpen] = useState(false) const _close = useCallback(() => {
setOpen(false)
}, [])
const _close = useCallback(() => { const _toggle = useCallback(() => {
setOpen(false) setOpen(!open)
}, []) }, [open])
const _toggle = useCallback(() => { return (
setOpen(!open) <nav className="main flex justify-between items-center">
}, [open]) <HomeLink />
return <nav className='main flex justify-between items-center'> <img id="icon" src="/assets/menu.svg" onClick={_toggle} />
<div className={`links flex ${open ? 'open' : ''}`.trim()} onClick={_close}>
<HomeLink /> <HomeLink />
<Link href="/works">
<img id='icon' src='/assets/menu.svg' onClick={_toggle} /> <div>works</div>
</Link>
<div className={`links flex ${open ? 'open' : ''}`.trim()} onClick={_close}> <Link href="/sets">
<HomeLink /> <div>sets</div>
<Link href='/works'> </Link>
<div>works</div> <Link href="/mastering">
</Link> <div>mastering</div>
<Link href='/sets'> </Link>
<div>sets</div> <Link href="/about">
</Link> <div>about</div>
<Link href='/mastering'> </Link>
<div>mastering</div> </div>
</Link>
<Link href='/about'>
<div>about</div>
</Link>
</div>
</nav> </nav>
)
} }
export default Menu export default Menu

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2018", "target": "es2019",
"module": "esnext", "module": "esnext",
"jsx": "preserve", "jsx": "preserve",
"strict": true, "strict": true,

3496
yarn.lock

File diff suppressed because it is too large Load Diff