@@ -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: [ | collectCoverageFrom: [ | ||||
'src/components/**/*.{jsx,tsx}', | 'src/components/**/*.{jsx,tsx}', | ||||
'src/modules/**/*.{js,ts}', | |||||
'src/utils/**/*.{js,ts}', | |||||
] | ] | ||||
}; | }; |
@@ -1,2 +1,3 @@ | |||||
/// <reference types="next" /> | /// <reference types="next" /> | ||||
/// <reference types="next/types/global" /> | /// <reference types="next/types/global" /> | ||||
/// <reference types="jest" /> |
@@ -6,10 +6,12 @@ | |||||
"dev": "next dev", | "dev": "next dev", | ||||
"build": "next build", | "build": "next build", | ||||
"start": "next start", | "start": "next start", | ||||
"test": "jest" | |||||
"test": "jest", | |||||
"e2e": "cypress" | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@tesseract-design/viewfinder": "^0.1.1", | "@tesseract-design/viewfinder": "^0.1.1", | ||||
"@theoryofnekomata/formxtr": "^0.1.2", | |||||
"next": "10.2.0", | "next": "10.2.0", | ||||
"react": "17.0.2", | "react": "17.0.2", | ||||
"react-dom": "17.0.2", | "react-dom": "17.0.2", | ||||
@@ -21,6 +23,7 @@ | |||||
"@types/node": "^15.0.2", | "@types/node": "^15.0.2", | ||||
"@types/react": "^17.0.5", | "@types/react": "^17.0.5", | ||||
"@types/styled-components": "^5.1.9", | "@types/styled-components": "^5.1.9", | ||||
"cypress": "^7.3.0", | |||||
"jest": "^26.6.3", | "jest": "^26.6.3", | ||||
"ts-jest": "^26.5.6", | "ts-jest": "^26.5.6", | ||||
"typescript": "^4.2.4" | "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 ( | 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}) => { | const MyApp = ({Component, pageProps}) => { | ||||
return <Component {...pageProps} /> | |||||
return ( | |||||
<> | |||||
<GlobalStyle/> | |||||
<Component {...pageProps} /> | |||||
</> | |||||
) | |||||
} | } | ||||
export default MyApp; | export default MyApp; |
@@ -1,11 +1,16 @@ | |||||
import {NextPage} from 'next' | import {NextPage} from 'next' | ||||
import {useRef} from 'react' | |||||
import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | import CreateRingtoneTemplate from '../../../../components/templates/CreateRingtone' | ||||
import RingtoneClient from '../../../../modules/ringtone/client' | |||||
type Props = {} | type Props = {} | ||||
const MyCreateRingtonePage: NextPage<Props> = () => { | const MyCreateRingtonePage: NextPage<Props> = () => { | ||||
const ringtoneClient = useRef(new RingtoneClient()) | |||||
return ( | return ( | ||||
<CreateRingtoneTemplate /> | |||||
<CreateRingtoneTemplate | |||||
onSubmit={ringtoneClient.current.save} | |||||
/> | |||||
) | ) | ||||
} | } | ||||
@@ -1,6 +1,7 @@ | |||||
{ | { | ||||
"extends": "./tsconfig.json", | "extends": "./tsconfig.json", | ||||
"compilerOptions": { | "compilerOptions": { | ||||
"jsx": "react-jsx" | |||||
} | |||||
"jsx": "react-jsx", | |||||
"types": ["jest"] | |||||
}, | |||||
} | } |