@@ -0,0 +1 @@ | |||
{} |
@@ -0,0 +1,5 @@ | |||
describe('login', () => { | |||
it('should log the current user in', () => { | |||
cy.visit('http://localhost:3000') | |||
}) | |||
}) |
@@ -0,0 +1,22 @@ | |||
/// <reference types="cypress" /> | |||
// *********************************************************** | |||
// This example plugins/index.js can be used to load plugins | |||
// | |||
// You can change the location of this file or turn off loading | |||
// the plugins file with the 'pluginsFile' configuration option. | |||
// | |||
// You can read more here: | |||
// https://on.cypress.io/plugins-guide | |||
// *********************************************************** | |||
// This function is called when a project is opened or re-opened (e.g. due to | |||
// the project's config changing) | |||
/** | |||
* @type {Cypress.PluginConfig} | |||
*/ | |||
// eslint-disable-next-line no-unused-vars | |||
export default (on, config) => { | |||
// `on` is used to hook into various events Cypress emits | |||
// `config` is the resolved Cypress config | |||
} |
@@ -0,0 +1,25 @@ | |||
// *********************************************** | |||
// This example commands.js shows you how to | |||
// create various custom commands and overwrite | |||
// existing commands. | |||
// | |||
// For more comprehensive examples of custom | |||
// commands please read more here: | |||
// https://on.cypress.io/custom-commands | |||
// *********************************************** | |||
// | |||
// | |||
// -- This is a parent command -- | |||
// Cypress.Commands.add('login', (email, password) => { ... }) | |||
// | |||
// | |||
// -- This is a child command -- | |||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) | |||
// | |||
// | |||
// -- This is a dual command -- | |||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) | |||
// | |||
// | |||
// -- This will overwrite an existing command -- | |||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) |
@@ -0,0 +1,20 @@ | |||
// *********************************************************** | |||
// This example support/index.js is processed and | |||
// loaded automatically before your test files. | |||
// | |||
// This is a great place to put global configuration and | |||
// behavior that modifies Cypress. | |||
// | |||
// You can change the location of this file or turn off | |||
// automatically serving support files with the | |||
// 'supportFile' configuration option. | |||
// | |||
// You can read more here: | |||
// https://on.cypress.io/configuration | |||
// *********************************************************** | |||
// Import commands.js using ES2015 syntax: | |||
import './commands' | |||
// Alternatively you can use CommonJS syntax: | |||
// require('./commands') |
@@ -0,0 +1,8 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es5", | |||
"lib": ["es5", "dom"], | |||
"types": ["cypress"] | |||
}, | |||
"include": ["**/*.ts"] | |||
} |
@@ -8,5 +8,7 @@ module.exports = { | |||
}, | |||
collectCoverageFrom: [ | |||
'src/components/**/*.{jsx,tsx}', | |||
'src/modules/**/*.{js,ts}', | |||
'src/utils/**/*.{js,ts}', | |||
] | |||
}; |
@@ -1,2 +1,3 @@ | |||
/// <reference types="next" /> | |||
/// <reference types="next/types/global" /> | |||
/// <reference types="jest" /> |
@@ -6,10 +6,12 @@ | |||
"dev": "next dev", | |||
"build": "next build", | |||
"start": "next start", | |||
"test": "jest" | |||
"test": "jest", | |||
"e2e": "cypress" | |||
}, | |||
"dependencies": { | |||
"@tesseract-design/viewfinder": "^0.1.1", | |||
"@theoryofnekomata/formxtr": "^0.1.2", | |||
"next": "10.2.0", | |||
"react": "17.0.2", | |||
"react-dom": "17.0.2", | |||
@@ -21,6 +23,7 @@ | |||
"@types/node": "^15.0.2", | |||
"@types/react": "^17.0.5", | |||
"@types/styled-components": "^5.1.9", | |||
"cypress": "^7.3.0", | |||
"jest": "^26.6.3", | |||
"ts-jest": "^26.5.6", | |||
"typescript": "^4.2.4" | |||
@@ -0,0 +1,22 @@ | |||
import {cleanup, render} from '@testing-library/react' | |||
import ActionButton from '.' | |||
describe('button component for triggering actions', () => { | |||
afterEach(() => { | |||
cleanup() | |||
}) | |||
it('should render a button element with a no-op action', () => { | |||
const result = render(<ActionButton />) | |||
const input = result.queryByRole('button') | |||
expect(input).not.toBeNull() | |||
}) | |||
describe.each(['button', 'reset', 'submit'] as const)('on %p action', (type) => { | |||
it('should render a button element with a submit action', () => { | |||
const result = render(<ActionButton type={type} />) | |||
const input = result.queryByRole('button') as HTMLButtonElement | |||
expect(input.type).toBe(type) | |||
}) | |||
}) | |||
}) |
@@ -0,0 +1,89 @@ | |||
import styled from 'styled-components'; | |||
import {FC, ReactChild} from 'react'; | |||
const Base = styled('div')({ | |||
height: '3rem', | |||
borderRadius: '0.25rem', | |||
overflow: 'hidden', | |||
position: 'relative', | |||
'::before': { | |||
content: "''", | |||
borderWidth: 1, | |||
borderStyle: 'solid', | |||
borderColor: 'inherit', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
}, | |||
}) | |||
const ClickArea = styled('button')({ | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
margin: 0, | |||
padding: '0 1rem', | |||
boxSizing: 'border-box', | |||
font: 'inherit', | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
color: 'inherit', | |||
outline: 0, | |||
textTransform: 'uppercase', | |||
fontWeight: 'bolder', | |||
position: 'relative', | |||
}) | |||
const VARIANTS = { | |||
default: { | |||
backgroundColor: 'var(--color-bg, white)', | |||
borderColor: 'var(--color-fg, black)', | |||
color: 'var(--color-fg, black)', | |||
}, | |||
primary: { | |||
backgroundColor: 'var(--color-fg, black)', | |||
borderColor: 'var(--color-fg, black)', | |||
color: 'var(--color-bg, white)', | |||
}, | |||
} | |||
type Props = { | |||
children?: ReactChild, | |||
className?: string, | |||
type?: 'button' | 'reset' | 'submit', | |||
block?: boolean, | |||
variant?: keyof typeof VARIANTS, | |||
} | |||
const ActionButton: FC<Props> = ({ | |||
children, | |||
className, | |||
type = 'button', | |||
block, | |||
variant = 'default', | |||
...etcProps | |||
}) => { | |||
return ( | |||
<Base | |||
className={className} | |||
style={{ | |||
...VARIANTS[variant], | |||
display: block ? 'block' : 'inline-block', | |||
width: block ? '100%' : undefined, | |||
}} | |||
> | |||
<ClickArea | |||
{...etcProps} | |||
type={type} | |||
> | |||
{children} | |||
</ClickArea> | |||
</Base> | |||
) | |||
} | |||
export default ActionButton |
@@ -0,0 +1,21 @@ | |||
import {render, cleanup} from '@testing-library/react' | |||
import TextInput from '.' | |||
describe('single-line text input component', () => { | |||
afterEach(() => { | |||
cleanup() | |||
}) | |||
it('should contain a text input element', () => { | |||
const result = render(<TextInput label="" name="" />) | |||
const input = result.queryByRole('textbox') | |||
expect(input).not.toBeNull() | |||
}) | |||
it('should acquire a descriptive label', () => { | |||
const label = 'foo' | |||
const result = render(<TextInput label={label} name="" />) | |||
const input = result.queryByLabelText(label) | |||
expect(input).not.toBeNull() | |||
}) | |||
}) |
@@ -0,0 +1,85 @@ | |||
import {FC} from 'react' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
height: '3rem', | |||
borderRadius: '0.25rem', | |||
overflow: 'hidden', | |||
position: 'relative', | |||
backgroundColor: 'var(--color-bg, white)', | |||
'::before': { | |||
content: "''", | |||
borderWidth: 1, | |||
borderStyle: 'solid', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
}, | |||
}) | |||
const ClickArea = styled('label')({ | |||
position: 'relative', | |||
height: '100%', | |||
}) | |||
const Label = styled('span')({ | |||
position: 'absolute', | |||
left: -999999, | |||
}) | |||
const Input = styled('textarea')({ | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
margin: 0, | |||
padding: '0 1rem', | |||
boxSizing: 'border-box', | |||
font: 'inherit', | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
color: 'inherit', | |||
outline: 0, | |||
}) | |||
type Props = { | |||
label: string, | |||
name: string, | |||
className?: string, | |||
block?: boolean, | |||
placeholder?: string, | |||
} | |||
const TextArea: FC<Props> = ({ | |||
label, | |||
className, | |||
block, | |||
...etcProps | |||
}) => { | |||
return ( | |||
<Base | |||
className={className} | |||
style={{ | |||
display: block ? 'block' : 'inline-block', | |||
width: block ? '100%' : undefined, | |||
}} | |||
> | |||
<ClickArea> | |||
<Label> | |||
{label} | |||
</Label> | |||
<Input | |||
{...etcProps} | |||
style={{ | |||
resize: block ? 'vertical' : undefined, | |||
}} | |||
/> | |||
</ClickArea> | |||
</Base> | |||
) | |||
} | |||
export default TextArea |
@@ -0,0 +1,21 @@ | |||
import {render, cleanup} from '@testing-library/react' | |||
import TextInput from '.' | |||
describe('single-line text input component', () => { | |||
afterEach(() => { | |||
cleanup() | |||
}) | |||
it('should contain a text input element', () => { | |||
const result = render(<TextInput label="" name="" />) | |||
const input = result.queryByRole('textbox') | |||
expect(input).not.toBeNull() | |||
}) | |||
it('should acquire a descriptive label', () => { | |||
const label = 'foo' | |||
const result = render(<TextInput label={label} name="" />) | |||
const input = result.queryByLabelText(label) | |||
expect(input).not.toBeNull() | |||
}) | |||
}) |
@@ -0,0 +1,83 @@ | |||
import {FC} from 'react' | |||
import styled from 'styled-components' | |||
const Base = styled('div')({ | |||
height: '3rem', | |||
borderRadius: '0.25rem', | |||
overflow: 'hidden', | |||
position: 'relative', | |||
backgroundColor: 'var(--color-bg, white)', | |||
'::before': { | |||
content: "''", | |||
borderWidth: 1, | |||
borderStyle: 'solid', | |||
position: 'absolute', | |||
top: 0, | |||
left: 0, | |||
width: '100%', | |||
height: '100%', | |||
borderRadius: 'inherit', | |||
boxSizing: 'border-box', | |||
}, | |||
}) | |||
const ClickArea = styled('label')({ | |||
position: 'relative', | |||
height: '100%', | |||
}) | |||
const Label = styled('span')({ | |||
position: 'absolute', | |||
left: -999999, | |||
}) | |||
const Input = styled('input')({ | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
margin: 0, | |||
padding: '0 1rem', | |||
boxSizing: 'border-box', | |||
font: 'inherit', | |||
border: 0, | |||
backgroundColor: 'transparent', | |||
color: 'inherit', | |||
outline: 0, | |||
}) | |||
type Props = { | |||
label: string, | |||
name: string, | |||
className?: string, | |||
block?: boolean, | |||
placeholder?: string, | |||
type?: 'email' | 'url' | 'text' | 'tel', | |||
} | |||
const TextInput: FC<Props> = ({ | |||
label, | |||
className, | |||
block, | |||
...etcProps | |||
}) => { | |||
return ( | |||
<Base | |||
className={className} | |||
style={{ | |||
display: block ? 'block' : 'inline-block', | |||
width: block ? '100%' : undefined, | |||
}} | |||
> | |||
<ClickArea> | |||
<Label> | |||
{label} | |||
</Label> | |||
<Input | |||
{...etcProps} | |||
/> | |||
</ClickArea> | |||
</Base> | |||
) | |||
} | |||
export default TextInput |
@@ -0,0 +1,39 @@ | |||
import * as React from 'react' | |||
import NextLink from 'next/link' | |||
import {UrlObject} from 'url' | |||
type Props = { | |||
href: UrlObject, | |||
as?: UrlObject, | |||
prefetch?: boolean, | |||
replace?: boolean, | |||
shallow?: boolean, | |||
component?: React.ElementType, | |||
} | |||
const Link: React.FC<Props> = ({ | |||
href, | |||
as, | |||
prefetch, | |||
replace, | |||
shallow, | |||
component: Component = 'a', | |||
...etcProps | |||
}) => { | |||
return ( | |||
<NextLink | |||
href={href} | |||
as={as} | |||
passHref | |||
replace={replace} | |||
shallow={shallow} | |||
prefetch={prefetch} | |||
> | |||
<Component | |||
{...etcProps} | |||
/> | |||
</NextLink> | |||
) | |||
} | |||
export default Link |
@@ -0,0 +1,37 @@ | |||
import {FC, FormEventHandler} from 'react' | |||
import styled from 'styled-components' | |||
import TextInput from '../../../molecules/forms/TextInput' | |||
import TextArea from '../../../molecules/forms/TextArea' | |||
import ActionButton from '../../../molecules/forms/ActionButton' | |||
const Form = styled('form')({ | |||
display: 'grid', | |||
gap: '1rem', | |||
}) | |||
type Props = { | |||
onSubmit?: FormEventHandler | |||
} | |||
const CreateRingtoneForm: FC<Props> = ({ | |||
onSubmit, | |||
}) => { | |||
return ( | |||
<Form | |||
onSubmit={onSubmit} | |||
method="post" | |||
action="/api/a/create/ringtone" | |||
> | |||
<TextInput label="Name" name="name" block /> | |||
<TextArea label="Data" name="data" block /> | |||
<ActionButton | |||
type="submit" | |||
block | |||
> | |||
Post | |||
</ActionButton> | |||
</Form> | |||
) | |||
} | |||
export default CreateRingtoneForm |
@@ -1,8 +1,76 @@ | |||
const CreateRingtoneTemplate = () => { | |||
import {FC, FormEventHandler} from 'react' | |||
import { LeftSidebarWithMenu } from '@tesseract-design/viewfinder' | |||
import CreateRingtoneForm from '../../organisms/forms/CreateRingtone' | |||
import Link from '../../molecules/navigation/Link' | |||
type Props = { | |||
onSubmit?: FormEventHandler | |||
} | |||
const CreateRingtoneTemplate: FC<Props> = ({ | |||
onSubmit, | |||
}) => { | |||
return ( | |||
<div> | |||
CreateRingtone | |||
</div> | |||
<LeftSidebarWithMenu.Layout | |||
linkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.SidebarMenuContainer> | |||
<LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.SidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.SidebarMenuContainer> | |||
</Link> | |||
)} | |||
moreLinkMenuItem={{ | |||
label: 'More', | |||
icon: 'M', | |||
url: {}, | |||
}} | |||
moreLinkComponent={({ url, icon, label, }) => ( | |||
<Link | |||
href={url} | |||
> | |||
<LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
<LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{icon} | |||
</LeftSidebarWithMenu.MoreSidebarMenuItemIcon> | |||
{label} | |||
</LeftSidebarWithMenu.MoreSidebarMenuContainer> | |||
</Link> | |||
)} | |||
sidebarMenuItems={[ | |||
{ | |||
id: 'browse', | |||
label: 'Browse', | |||
icon: 'B', | |||
url: { | |||
pathname: '/ringtones', | |||
}, | |||
}, | |||
{ | |||
id: 'compose', | |||
label: 'Compose', | |||
icon: 'C', | |||
url: { | |||
pathname: '/my/create/ringtone', | |||
}, | |||
}, | |||
]} | |||
sidebarMain={ | |||
<LeftSidebarWithMenu.SidebarMainContainer> | |||
Hi | |||
</LeftSidebarWithMenu.SidebarMainContainer> | |||
} | |||
> | |||
<LeftSidebarWithMenu.ContentContainer> | |||
<CreateRingtoneForm | |||
onSubmit={onSubmit} | |||
/> | |||
</LeftSidebarWithMenu.ContentContainer> | |||
</LeftSidebarWithMenu.Layout> | |||
) | |||
} | |||
@@ -0,0 +1,11 @@ | |||
export default class Ringtone { | |||
name: string | |||
data: string | |||
createdAt: Date | |||
updatedAt: Date | |||
deletedAt?: Date | |||
} |
@@ -0,0 +1,10 @@ | |||
import getFormValues from '@theoryofnekomata/formxtr' | |||
import {FormEvent} from 'react' | |||
export default class RingtoneClient { | |||
async save(e: FormEvent & { submitter: HTMLInputElement | HTMLButtonElement }) { | |||
e.preventDefault() | |||
const values = getFormValues(e.target as HTMLFormElement, e.submitter) | |||
console.log(values) | |||
} | |||
} |
@@ -1,5 +1,32 @@ | |||
import { createGlobalStyle } from 'styled-components' | |||
const GlobalStyle = createGlobalStyle({ | |||
':root': { | |||
'--color-bg': 'white', | |||
'--color-fg': 'black', | |||
color: 'var(--color-fg, black)', | |||
backgroundColor: 'var(--color-bg, white)', | |||
}, | |||
'body': { | |||
margin: 0, | |||
}, | |||
'@media (prefers-color-scheme: dark)': { | |||
':root': { | |||
'--color-bg': 'black', | |||
'--color-fg': 'white', | |||
color: 'var(--color-fg, white)', | |||
backgroundColor: 'var(--color-bg, black)', | |||
}, | |||
}, | |||
}) | |||
const MyApp = ({Component, pageProps}) => { | |||
return <Component {...pageProps} /> | |||
return ( | |||
<> | |||
<GlobalStyle/> | |||
<Component {...pageProps} /> | |||
</> | |||
) | |||
} | |||
export default MyApp; |
@@ -1,11 +1,16 @@ | |||
import {NextPage} from 'next' | |||
import {useRef} from 'react' | |||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | |||
import RingtoneClient from '../../../../modules/ringtone/client' | |||
type Props = {} | |||
const MyCreateRingtonePage: NextPage<Props> = () => { | |||
const ringtoneClient = useRef(new RingtoneClient()) | |||
return ( | |||
<CreateRingtoneTemplate /> | |||
<CreateRingtoneTemplate | |||
onSubmit={ringtoneClient.current.save} | |||
/> | |||
) | |||
} | |||
@@ -1,6 +1,7 @@ | |||
{ | |||
"extends": "./tsconfig.json", | |||
"compilerOptions": { | |||
"jsx": "react-jsx" | |||
} | |||
"jsx": "react-jsx", | |||
"types": ["jest"] | |||
}, | |||
} |