This commit is contained in:
keyan 2021-04-13 19:57:32 -05:00
parent 9acde2df1c
commit bc5d4d4808
23 changed files with 695 additions and 308 deletions

View File

@ -1,26 +1,13 @@
import { UserInputError, AuthenticationError } from 'apollo-server-micro' import { UserInputError, AuthenticationError } from 'apollo-server-micro'
export default { const createItem = async (parent, { title, text, url, parentId }, { me, models }) => {
Query: {
items: async (parent, args, { models }) => {
return await models.$queryRaw(`
SELECT id, text, "userId", ltree2text("path") AS "path"
FROM "Item"
ORDER BY "path"`)
}
},
Mutation: {
createItem: async (parent, { text, parentId }, { me, models }) => {
if (!me) { if (!me) {
throw new AuthenticationError('You must be logged in') throw new AuthenticationError('You must be logged in')
} }
if (!text) {
throw new UserInputError('Item must have text', { argumentName: 'text' })
}
const data = { const data = {
title,
url,
text, text,
user: { user: {
connect: { connect: {
@ -39,6 +26,47 @@ export default {
return await models.item.create({ data }) return await models.item.create({ data })
} }
export default {
Query: {
items: async (parent, args, { models }) => {
return await models.$queryRaw(`
SELECT id, "created_at" as "createdAt", title, url, text, "userId", ltree2text("path") AS "path"
FROM "Item"
ORDER BY "path"`)
}
},
Mutation: {
createLink: async (parent, { title, url }, { me, models }) => {
if (!title) {
throw new UserInputError('Link must have title', { argumentName: 'title' })
}
if (!url) {
throw new UserInputError('Link must have url', { argumentName: 'url' })
}
return await createItem(parent, { title, url }, { me, models })
},
createDiscussion: async (parent, { title, text }, { me, models }) => {
if (!title) {
throw new UserInputError('Link must have title', { argumentName: 'title' })
}
return await createItem(parent, { title, text }, { me, models })
},
createComment: async (parent, { text, parentId }, { me, models }) => {
if (!text) {
throw new UserInputError('Comment must have text', { argumentName: 'text' })
}
if (!parentId) {
throw new UserInputError('Comment must have text', { argumentName: 'text' })
}
return await createItem(parent, { text, parentId }, { me, models })
}
}, },
Item: { Item: {
@ -56,6 +84,8 @@ export default {
WHERE id = ${item.id}` WHERE id = ${item.id}`
return path.split('.').length - 1 return path.split('.').length - 1
} },
comments: () => 0,
sats: () => 0
} }
} }

View File

@ -6,13 +6,20 @@ export default gql`
} }
extend type Mutation { extend type Mutation {
createItem(text: String!, parentId: ID): Item! createLink(title: String!, url: String): Item!
createDiscussion(title: String!, text: String): Item!
createComment(text: String!, parentId: ID!): Item!
} }
type Item { type Item {
id: ID! id: ID!
text: String! createdAt: String!
title: String
url: String
text: String
user: User! user: User!
depth: Int! depth: Int!
sats: Int!
comments: Int!
} }
` `

74
components/form.js Normal file
View File

@ -0,0 +1,74 @@
import Button from 'react-bootstrap/Button'
import InputGroup from 'react-bootstrap/InputGroup'
import BootstrapForm from 'react-bootstrap/Form'
import Alert from 'react-bootstrap/Alert'
import { Formik, Form as FormikForm, useFormikContext, useField } from 'formik'
import { useState } from 'react'
export function SubmitButton ({ children, variant, ...props }) {
const { isSubmitting } = useFormikContext()
return (
<Button
variant={variant || 'main'}
type='submit'
disabled={isSubmitting}
{...props}
>
{children}
</Button>
)
}
export function Input ({ label, prepend, append, hint, ...props }) {
const [field, meta] = useField(props)
return (
<BootstrapForm.Group>
{label && <BootstrapForm.Label>{label}</BootstrapForm.Label>}
<InputGroup hasValidation>
{prepend && (
<InputGroup.Prepend>
{prepend}
</InputGroup.Prepend>
)}
<BootstrapForm.Control
{...field} {...props}
isInvalid={meta.touched && meta.error}
/>
{append && (
<InputGroup.Append>
{append}
</InputGroup.Append>
)}
<BootstrapForm.Control.Feedback type='invalid'>
{meta.touched && meta.error}
</BootstrapForm.Control.Feedback>
{hint && (
<BootstrapForm.Text>
{hint}
</BootstrapForm.Text>
)}
</InputGroup>
</BootstrapForm.Group>
)
}
export function Form ({
initial, schema, onSubmit, children, ...props
}) {
const [error, setError] = useState()
return (
<Formik
initialValues={initial}
validationSchema={schema}
onSubmit={(...args) =>
onSubmit(...args).catch(e => setError(e.message))}
>
<FormikForm {...props} noValidate>
{error && <Alert variant='danger' onClose={() => setError(undefined)} dismissible>{error}</Alert>}
{children}
</FormikForm>
</Formik>
)
}

View File

@ -4,6 +4,7 @@ import Nav from 'react-bootstrap/Nav'
import Link from 'next/link' import Link from 'next/link'
import styles from './header.module.css' import styles from './header.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Container } from 'react-bootstrap'
export default function Header () { export default function Header () {
const [session, loading] = useSession() const [session, loading] = useSession()
@ -30,7 +31,8 @@ export default function Header () {
return ( return (
<> <>
<Navbar bg='brand' className={styles.navbar}> <Navbar bg='primary' className={styles.navbar}>
<Container>
<Link href='/' passHref> <Link href='/' passHref>
<Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand> <Navbar.Brand className={styles.brand}>STACKER NEWS</Navbar.Brand>
</Link> </Link>
@ -49,6 +51,7 @@ export default function Header () {
<Nav className='ml-auto align-items-center'> <Nav className='ml-auto align-items-center'>
<Corner /> <Corner />
</Nav> </Nav>
</Container>
</Navbar> </Navbar>
</> </>
) )

47
components/item.js Normal file
View File

@ -0,0 +1,47 @@
import UpVote from '../svgs/lightning-arrow.svg'
import styles from './item.module.css'
function timeSince (timeStamp) {
const now = new Date()
const secondsPast = (now.getTime() - timeStamp) / 1000
if (secondsPast < 60) {
return parseInt(secondsPast) + 's'
}
if (secondsPast < 3600) {
return parseInt(secondsPast / 60) + 'm'
}
if (secondsPast <= 86400) {
return parseInt(secondsPast / 3600) + 'h'
}
if (secondsPast > 86400) {
const day = timeStamp.getDate()
const month = timeStamp.toDateString().match(/ [a-zA-Z]*/)[0].replace(' ', '')
const year = timeStamp.getFullYear() === now.getFullYear() ? '' : ' ' + timeStamp.getFullYear()
return day + ' ' + month + year
}
}
export default function Item ({ item }) {
return (
<div className='d-flex justify-content-start align-items-center'>
<UpVote width={32} height={32} className={styles.upvote} />
<div>
<div>
<span>
<span className={styles.title}>{item.title}</span>
<a className={styles.link} href={item.url}>{item.url.replace(/(^\w+:|^)\/\//, '')}</a>
</span>
</div>
<div className={styles.other}>
<span>{item.sats} sats</span>
<span> \ </span>
<span>{item.comments} comments</span>
<span> </span>
<a href='/satoshi'>@{item.user.name}</a>
<span> </span>
<span>{timeSince(new Date(item.createdAt))}</span>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,23 @@
.upvote {
fill: grey;
margin-right: .25rem;
}
.upvote:hover {
fill: darkgray;
cursor: pointer;
}
.title {
font-weight: 500;
}
.link {
font-size: 80%;
margin-left: .5rem;
}
.other {
font-size: 70%;
color: grey;
}

View File

@ -12,13 +12,16 @@
"@prisma/client": "^2.19.0", "@prisma/client": "^2.19.0",
"apollo-server-micro": "^2.21.2", "apollo-server-micro": "^2.21.2",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"formik": "^2.2.6",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"next": "10.0.9", "next": "10.0.9",
"next-auth": "^3.13.3", "next-auth": "^3.13.3",
"react": "17.0.1", "react": "17.0.1",
"react-bootstrap": "^1.5.2", "react-bootstrap": "^1.5.2",
"react-dom": "17.0.1", "react-dom": "17.0.1",
"swr": "^0.5.4" "sass": "^1.32.8",
"swr": "^0.5.4",
"yup": "^0.32.9"
}, },
"standard": { "standard": {
"parser": "babel-eslint", "parser": "babel-eslint",

View File

@ -1,5 +1,4 @@
import 'bootstrap/dist/css/bootstrap.min.css' import '../styles/globals.scss'
import '../styles/globals.css'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client' import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
import { Provider } from 'next-auth/client' import { Provider } from 'next-auth/client'

View File

@ -1,103 +1,106 @@
import { useQuery, gql, useMutation } from '@apollo/client' import { useQuery, gql } from '@apollo/client'
import { useState } from 'react' // import styles from '../styles/index.module.css'
import { useSession } from 'next-auth/client'
import styles from '../styles/index.module.css'
import Layout from '../components/layout' import Layout from '../components/layout'
import Item from '../components/item'
import Container from 'react-bootstrap/Container'
function Users () { // function Users () {
const { loading, error, data } = useQuery(gql`{ users { id, name } }`) // const { loading, error, data } = useQuery(gql`{ users { id, name } }`)
if (error) return <div>Failed to load</div> // if (error) return <div>Failed to load</div>
if (loading) return <div>Loading...</div> // if (loading) return <div>Loading...</div>
const { users } = data // const { users } = data
return ( // return (
<div> // <div>
{users.map(user => ( // {users.map(user => (
<div key={user.id}>{user.name}</div> // <div key={user.id}>{user.name}</div>
))} // ))}
</div> // </div>
) // )
} // }
function NewItem ({ parentId }) { // function NewItem ({ parentId }) {
const [session] = useSession() // const [session] = useSession()
const [createItem] = useMutation( // const [createItem] = useMutation(
gql` // gql`
mutation CreateItem($text: String!, $parentId: ID) { // mutation CreateItem($text: String!, $parentId: ID) {
createItem(text: $text, parentId: $parentId) { // createItem(text: $text, parentId: $parentId) {
id // id
} // }
}`, { // }`, {
update (cache, { data: { createItem } }) { // update (cache, { data: { createItem } }) {
cache.modify({ // cache.modify({
fields: { // fields: {
items (existingItems = [], { readField }) { // items (existingItems = [], { readField }) {
const newItemRef = cache.writeFragment({ // const newItemRef = cache.writeFragment({
data: createItem, // data: createItem,
fragment: gql` // fragment: gql`
fragment NewItem on Item { // fragment NewItem on Item {
id // id
user { // user {
name // name
} // }
text // text
depth // depth
} // }
` // `
}) // })
for (let i = 0; i < existingItems.length; i++) { // for (let i = 0; i < existingItems.length; i++) {
if (readField('id', existingItems[i]) === parentId) { // if (readField('id', existingItems[i]) === parentId) {
return [...existingItems.slice(0, i), newItemRef, ...existingItems.slice(i)] // return [...existingItems.slice(0, i), newItemRef, ...existingItems.slice(i)]
} // }
} // }
return [newItemRef, ...existingItems] // return [newItemRef, ...existingItems]
} // }
} // }
}) // })
} // }
}) // })
const [open, setOpen] = useState(false) // const [open, setOpen] = useState(false)
if (!session) return null // if (!session) return null
if (!open) { // if (!open) {
return ( // return (
<div onClick={() => setOpen(true)}> // <div onClick={() => setOpen(true)}>
{parentId ? 'reply' : 'submit'} // {parentId ? 'reply' : 'submit'}
</div> // </div>
) // )
} // }
let text // let text
return ( // return (
<form // <form
style={{ marginLeft: '5px' }} // style={{ marginLeft: '5px' }}
onSubmit={e => { // onSubmit={e => {
e.preventDefault() // e.preventDefault()
createItem({ variables: { text: text.value, parentId } }) // createItem({ variables: { text: text.value, parentId } })
setOpen(false) // setOpen(false)
text.value = '' // text.value = ''
}} // }}
> // >
<textarea // <textarea
ref={node => { // ref={node => {
text = node // text = node
}} // }}
/> // />
<button type='submit'>Submit</button> // <button type='submit'>Submit</button>
</form> // </form>
) // )
} // }
function Items () { function Items () {
const { loading, error, data } = useQuery( const { loading, error, data } = useQuery(
gql` gql`
{ items { { items {
id id
createdAt
title
url
user { user {
name name
} }
text sats
depth comments
} }` } }`
) )
if (error) return <div>Failed to load</div> if (error) return <div>Failed to load</div>
@ -105,15 +108,18 @@ function Items () {
const { items } = data const { items } = data
return ( return (
<> <>
<NewItem /> <Container className='my-1 py-2 px-sm-0'>
<div> <table>
{items.map(item => ( <tbody>
<div key={item.id} style={{ marginLeft: `${5 * item.depth}px` }}> {items.map((item, i) => (
<div>{item.user.name} : {item.text}</div> <tr className='align-items-center' key={item.id}>
<NewItem parentId={item.id} /> <td align='right' className='font-weight-bold text-muted mr-1'>{i + 1}</td>
</div> <td><Item item={item} /></td>
</tr>
))} ))}
</div> </tbody>
</table>
</Container>
</> </>
) )
} }
@ -121,8 +127,6 @@ function Items () {
export default function Index () { export default function Index () {
return ( return (
<Layout> <Layout>
<div className={`${styles.box} flashit`} />
<Users />
<Items /> <Items />
</Layout> </Layout>
) )

View File

@ -0,0 +1,13 @@
import Container from "react-bootstrap/Container";
import Item from "../../../components/item";
import Layout from "../../../components/layout";
export default function AnItem () {
return (
<Layout>
<Container className='my-4'>
<Item />
</Container>
</Layout>
)
}

View File

@ -1,38 +1,102 @@
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import Layout from '../components/layout' import Layout from '../components/layout'
import Button from 'react-bootstrap/Button'
import { Form, Input, SubmitButton } from '../components/form'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import Link from 'next/link' import Link from 'next/link'
import styles from '../styles/post.module.css' import styles from '../styles/post.module.css'
import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
export const DiscussionSchema = Yup.object({
title: Yup.string().required('required').trim()
})
export function DiscussionForm () { export function DiscussionForm () {
const router = useRouter()
const [createDiscussion] = useMutation(
gql`
mutation createDiscussion($title: String!, $text: String) {
createDiscussion(title: $title, text: $text) {
id
}
}`
)
return ( return (
<Form> <Form
<Form.Group> initial={{
<Form.Label>title</Form.Label> title: '',
<Form.Control type='text' /> text: ''
</Form.Group> }}
<Form.Group> schema={DiscussionSchema}
<Form.Label>text <small className='text-muted ml-2'>optional</small></Form.Label> onSubmit={async (values) => {
<Form.Control as='textarea' rows={4} /> const { data: { createDiscussion: { id } }, error } = await createDiscussion({ variables: values })
</Form.Group> if (error) {
<Button className='mt-2' variant='main' size='lg' type='submit'>post</Button> throw new Error({ message: error.toString() })
}
router.push(`items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<Input
label={<>text <small className='text-muted ml-2'>optional</small></>}
name='text'
as='textarea'
rows={4}
/>
<SubmitButton variant='secondary' className='mt-2'>post</SubmitButton>
</Form> </Form>
) )
} }
export const LinkSchema = Yup.object({
title: Yup.string().required('required').trim(),
url: Yup.string().url('invalid url')
})
export function LinkForm () { export function LinkForm () {
const router = useRouter()
const [createLink] = useMutation(
gql`
mutation createLink($title: String!, $url: String!) {
createLink(title: $title, url: $url) {
id
}
}`
)
return ( return (
<Form> <Form
<Form.Group> initial={{
<Form.Label>title</Form.Label> title: '',
<Form.Control type='text' /> url: ''
</Form.Group> }}
<Form.Group> schema={LinkSchema}
<Form.Label>url</Form.Label> onSubmit={async (values) => {
<Form.Control type='url' /> const { data: { createLink: { id } }, error } = await createLink({ variables: values })
</Form.Group> if (error) {
<Button className='mt-2' variant='main' size='lg' type='submit'>post</Button> throw new Error({ message: error.toString() })
}
router.push(`items/${id}`)
}}
>
<Input
label='title'
name='title'
required
autoFocus
/>
<Input
label='url'
name='url'
required
/>
<SubmitButton variant='secondary' className='mt-2'>post</SubmitButton>
</Form> </Form>
) )
} }
@ -44,11 +108,11 @@ export function PostForm () {
return ( return (
<div className='align-items-center'> <div className='align-items-center'>
<Link href='/post?type=link'> <Link href='/post?type=link'>
<Button variant='main' size='lg'>link</Button> <Button variant='secondary'>link</Button>
</Link> </Link>
<span className='mx-3 font-weight-bold text-muted'>or</span> <span className='mx-3 font-weight-bold text-muted'>or</span>
<Link href='/post?type=discussion'> <Link href='/post?type=discussion'>
<Button variant='main' size='lg'> discussion</Button> <Button variant='secondary'> discussion</Button>
</Link> </Link>
</div> </div>
) )

View File

@ -28,6 +28,7 @@ CREATE TABLE "Item" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL, "updated_at" TIMESTAMP(3) NOT NULL,
"title" TEXT,
"text" TEXT, "text" TEXT,
"url" TEXT, "url" TEXT,
"userId" INTEGER NOT NULL, "userId" INTEGER NOT NULL,

View File

@ -35,7 +35,7 @@ model Item {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at") updatedAt DateTime @updatedAt @map(name: "updated_at")
title String title String?
text String? text String?
url String? url String?
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])

View File

@ -5,29 +5,14 @@ async function main () {
where: { name: 'k00b' }, where: { name: 'k00b' },
update: {}, update: {},
create: { create: {
name: 'k00b', name: 'k00b'
messages: {
create: {
text: 'Hello world'
}
}
} }
}) })
const satoshi = await prisma.user.upsert({ const satoshi = await prisma.user.upsert({
where: { name: 'satoshi' }, where: { name: 'satoshi' },
update: {}, update: {},
create: { create: {
name: 'satoshi', name: 'satoshi'
messages: {
create: [
{
text: 'Peer to peer digital cash'
},
{
text: 'Reengineer the world'
}
]
}
} }
}) })
const greg = await prisma.user.upsert({ const greg = await prisma.user.upsert({
@ -49,53 +34,40 @@ async function main () {
where: { id: 0 }, where: { id: 0 },
update: {}, update: {},
create: { create: {
text: 'A', title: 'System76 Developing “Cosmic” Desktop Environment',
userId: satoshi.id, url: 'https://blog.system76.com/post/648371526931038208/cosmic-to-arrive-in-june-release-of-popos-2104',
children: {
create: [
{
text: 'B',
userId: k00b.id,
children: {
create: [
{
text: 'G',
userId: satoshi.id,
children: {
create: [
{
text: 'H',
userId: greg.id
}
]
}
}
]
}
},
{
text: 'C',
userId: k00b.id,
children: {
create: [
{
text: 'D',
userId: satoshi.id userId: satoshi.id
}, }
{ })
text: 'E',
await prisma.item.upsert({
where: { id: 1 },
update: {},
create: {
title: 'Deno 1.9',
url: 'https://deno.com/blog/v1.9',
userId: k00b.id
}
})
await prisma.item.upsert({
where: { id: 2 },
update: {},
create: {
title: '1Password Secrets Automation',
url: 'https://blog.1password.com/introducing-secrets-automation/',
userId: greg.id userId: greg.id
}, }
{ })
text: 'F',
await prisma.item.upsert({
where: { id: 3 },
update: {},
create: {
title: 'Counter Strike Bug Allows Hackers to Take over a PC with a Steam Invite',
url: 'https://www.vice.com/en/article/dyvgej/counter-strike-bug-allows-hackers-to-take-over-a-pc-with-a-steam-invite',
userId: stan.id userId: stan.id
} }
]
}
}
]
}
}
}) })
} }
main() main()

View File

@ -1,74 +0,0 @@
@font-face {
font-family: lightning;
src: url(/Lightningvolt-xoqm.ttf);
}
html {
background-color: #fafafa !important;
}
body {
background-color: #fafafa !important;
}
.form-label {
font-size: 0.938rem;
font-weight: bold;
}
.form-control {
border-radius: .36rem;
height: calc(1.5em + 1rem + 2px);
}
.btn {
transition: none;
font-weight: 600;
font-size: 16px;
border-radius: .4rem;
}
.btn:focus {
box-shadow: none !important;
}
button:focus {
outline: none !important;
}
.btn-main {
background-color: #F6911D;
color: #ffffff;
}
.btn-main:hover {
background-color: #F6911D;
color: #ffffff;
}
.btn-lg {
padding: 0.5rem 1.25rem;
}
.bg-brand {
background: #FADA5E;
}
.flashit{
animation: flash ease-out 7s infinite;
animation-delay: -3s;
}
@keyframes flash {
from { opacity: 0; }
42% { opacity: 0; }
43% { opacity: 0.6; }
44% { opacity: 0.2; }
46% { opacity: 1; }
50% { opacity: 0; }
92% { opacity: 0; }
93% { opacity: 0.6; }
94% { opacity: 0.2; }
96% { opacity: 1; }
to { opacity: 0; }
}

99
styles/globals.scss Normal file
View File

@ -0,0 +1,99 @@
$theme-colors: (
"primary" : #FADA5E,
"secondary" : #F6911D,
"danger" : #c03221,
"info" : #007cbe,
"success" : #5c8001
);
$body-bg: #fafafa;
$border-radius: .4rem;
$enable-transitions: false;
$enable-gradients: false;
$enable-shadows: false;
$btn-transition: none;
$form-feedback-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z' fill='rgba(192,50,33,1)'/%3E%3C/svg%3E");
$line-height-base: 1.75;
$input-btn-padding-y: .42rem;
$input-btn-padding-x: .84rem;
$btn-padding-y: .5rem;
$btn-padding-x: 1.1rem;
$btn-font-weight: bold;
$btn-focus-width: 0;
$btn-border-width: 0;
$btn-focus-box-shadow: none;
$alert-border-width: 0;
$close-text-shadow: none;
$close-color: inherit;
$alert-border-radius: #{33% 2%} / #{11% 74%};
$link-color: #007cbe;
$font-size-base: .9rem;
$enable-responsive-font-sizes: true;
$link-hover-decoration: none;
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 900px,
xl: 900px
) !default;
@import "~bootstrap/scss/bootstrap";
@font-face {
font-family: lightning;
src: url(/Lightningvolt-xoqm.ttf);
}
.form-label {
font-size: 92%;
font-weight: bold;
}
.form-control:focus {
border-color: #fada5e;
}
.btn-secondary,
.btn-secondary:hover,
.btn-secondary:disabled {
color: #ffffff;
}
.flashit{
animation: flash ease-out 7s infinite;
animation-delay: -3s;
}
@keyframes flash {
from { opacity: 0; }
42% { opacity: 0; }
43% { opacity: 0.6; }
44% { opacity: 0.2; }
46% { opacity: 1; }
50% { opacity: 0; }
92% { opacity: 0; }
93% { opacity: 0.6; }
94% { opacity: 0.2; }
96% { opacity: 1; }
to { opacity: 0; }
}
.morphit{
animation: flash ease-out 7s infinite;
animation-delay: -3s;
}
@keyframes flash {
from { opacity: 0; }
42% { opacity: 0; }
43% { opacity: 0.6; }
44% { opacity: 0.2; }
46% { opacity: 1; }
50% { opacity: 0; }
92% { opacity: 0; }
93% { opacity: 0.6; }
94% { opacity: 0.2; }
96% { opacity: 1; }
to { opacity: 0; }
}

View File

@ -3,3 +3,9 @@
height: 100px; height: 100px;
background-color: grey; background-color: grey;
} }
.grid {
display: grid;
grid-template-columns: auto auto auto;
width: 100%;
}

1
svgs/arrow-up-s-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 8l6 6H6z"/></svg>

After

Width:  |  Height:  |  Size: 150 B

1
svgs/lightning-arrow.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 114 287"><path d="M113.62 193.44h-49.1l17.93 93.28H18.9L.5 164.46h64.02L48.76 62.02h-26.3L61.74.5l39.46 61.52H77.95z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 286.221 286.221" style="enable-background:new 0 0 286.221 286.221;" xml:space="preserve">
<g>
<polygon points="199.667,93.283 150.567,93.283 168.5,0 104.95,0 86.554,122.26 150.567,122.26 134.813,224.703 108.506,224.703
147.795,286.221 187.247,224.703 164,224.703 "/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

5
svgs/pyramid.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" >
<path d="M256,0.289L0,379.471l256,132.24l256-132.24L256,0.289z M240.992,470.175L54.582,373.882l186.411-96.294V470.175z
M240.992,243.805L67.197,333.582L240.992,76.16V243.805z M271.008,76.16l173.795,257.423l-173.795-89.778V76.16z
M271.008,277.588l186.411,96.294l-186.411,96.293V277.588z"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

1
svgs/triangle.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72.72 72.719"><path d="M72.72,65.686H0L36.36,7.034L72.72,65.686z M5.388,62.686h61.943L36.36,12.727L5.388,62.686z"/></svg>

After

Width:  |  Height:  |  Size: 176 B

View File

@ -149,7 +149,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.1", "@babel/runtime@^7.13.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.1", "@babel/runtime@^7.13.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7":
version "7.13.10" version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
@ -522,6 +522,11 @@
"@types/koa-compose" "*" "@types/koa-compose" "*"
"@types/node" "*" "@types/node" "*"
"@types/lodash@^4.14.165":
version "4.14.168"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
"@types/long@^4.0.0": "@types/long@^4.0.0":
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
@ -1277,7 +1282,7 @@ chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.1.0" ansi-styles "^4.1.0"
supports-color "^7.1.0" supports-color "^7.1.0"
chokidar@3.5.1: chokidar@3.5.1, "chokidar@>=2.0.0 <4.0.0":
version "3.5.1" version "3.5.1"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
@ -1572,6 +1577,11 @@ deep-is@^0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
define-properties@^1.1.3: define-properties@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@ -2196,6 +2206,19 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
formik@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.6.tgz#378a4bafe4b95caf6acf6db01f81f3fe5147559d"
integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.14"
lodash-es "^4.17.14"
react-fast-compare "^2.0.1"
tiny-warning "^1.0.2"
tslib "^1.10.0"
fs-capacitor@^2.0.4: fs-capacitor@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
@ -2412,7 +2435,7 @@ hoek@6.x.x:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
hoist-non-react-statics@^3.3.2: hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -2835,7 +2858,7 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash-es@^4.17.20: lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@ -3028,6 +3051,11 @@ mz@^2.4.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nanoclone@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
nanoid@^3.1.16: nanoid@^3.1.16:
version "3.1.22" version "3.1.22"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
@ -3584,6 +3612,11 @@ prop-types@15.7.2, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.8.1" react-is "^16.8.1"
property-expr@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
public-encrypt@^4.0.0: public-encrypt@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
@ -3694,6 +3727,11 @@ react-dom@17.0.1:
object-assign "^4.1.1" object-assign "^4.1.1"
scheduler "^0.20.1" scheduler "^0.20.1"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-is@16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1: react-is@16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -3888,6 +3926,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass@^1.32.8:
version "1.32.8"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc"
integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==
dependencies:
chokidar ">=2.0.0 <4.0.0"
sax@>=0.6.0: sax@>=0.6.0:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@ -4360,6 +4405,11 @@ timers-browserify@^2.0.4:
dependencies: dependencies:
setimmediate "^1.0.4" setimmediate "^1.0.4"
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
to-arraybuffer@^1.0.0: to-arraybuffer@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
@ -4382,6 +4432,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
tr46@^1.0.1: tr46@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@ -4747,6 +4802,19 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
yup@^0.32.9:
version "0.32.9"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.9.tgz#9367bec6b1b0e39211ecbca598702e106019d872"
integrity sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==
dependencies:
"@babel/runtime" "^7.10.5"
"@types/lodash" "^4.14.165"
lodash "^4.17.20"
lodash-es "^4.17.15"
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"
zen-observable-ts@^0.8.21: zen-observable-ts@^0.8.21:
version "0.8.21" version "0.8.21"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d"