first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { ApiContext, DEFAULT_API_CONTEXT } from '@/common/providers/api-context'
|
||||
import {
|
||||
{{ pascalCase name }}ApiResult,
|
||||
{{ pascalCase name }}CreateApiParams,
|
||||
{{ pascalCase name }}DeleteApiParams,
|
||||
{{ pascalCase name }}GetApiParams,
|
||||
{{ pascalCase name }}Id,
|
||||
{{ pascalCase name }}ListApiParams,
|
||||
{{ pascalCase name }}PaginatedApiResult,
|
||||
{{ pascalCase name }}UpdateApiParams,
|
||||
} from './{{ kebabCase name }}.types'
|
||||
|
||||
export const {{ camelCase name }}ApiProto = (
|
||||
baseUrl: string = process.env.NEXT_PUBLIC_API_ENDPOINT || '/api',
|
||||
defaultApiContext = DEFAULT_API_CONTEXT
|
||||
) => {
|
||||
const endpointUrl = `${baseUrl}/{{ kebabCase name }}`
|
||||
|
||||
type UrlParams = { resourceId?: {{ pascalCase name}}Id }
|
||||
const endpoint = (
|
||||
urlParams: UrlParams,
|
||||
queryParams: Record<string, string>
|
||||
) => {
|
||||
const queryParamString = new URLSearchParams(queryParams).toString()
|
||||
const resourceIdParam =
|
||||
urlParams.resourceId === undefined ? '' : `/${urlParams.resourceId}`
|
||||
|
||||
// TODO: Customize the endpoint url generation here
|
||||
return `${endpointUrl}${resourceIdParam}?${queryParamString}`
|
||||
}
|
||||
|
||||
return {
|
||||
async list(
|
||||
this: ApiContext,
|
||||
{ page, size, ...otherQueryParams }: {{ pascalCase name }}ListApiParams
|
||||
): Promise<{{ pascalCase name }}PaginatedApiResult> {
|
||||
const urlParams: UrlParams = {}
|
||||
const queryParams = {
|
||||
// TODO: Map the pagination params as required by the API
|
||||
page: `${page}`,
|
||||
size: `${size}`,
|
||||
// limit: `${size}`,
|
||||
// offset: `${Math.max((page - 1) * size, 0)}`,
|
||||
...otherQueryParams,
|
||||
}
|
||||
const url = endpoint(urlParams, queryParams)
|
||||
console.debug(
|
||||
`Listing {{ pascalCase name }} with page: ${page}, size: ${size}`,
|
||||
`on url: ${url}`
|
||||
)
|
||||
|
||||
const response = await this.client.get(url)
|
||||
|
||||
// TODO: Add code handle the response if needed
|
||||
|
||||
return response.data as {{ pascalCase name }}PaginatedApiResult
|
||||
},
|
||||
async delete(
|
||||
this: ApiContext,
|
||||
{ resourceId, ...queryParams }: {{ pascalCase name }}DeleteApiParams
|
||||
): Promise<boolean> {
|
||||
const urlParams: UrlParams = { resourceId }
|
||||
const url = endpoint(urlParams, queryParams)
|
||||
console.debug(
|
||||
`Deleting {{ pascalCase name }} with id:`,
|
||||
resourceId,
|
||||
`on url: ${url}`
|
||||
)
|
||||
|
||||
const response = await this.client.delete(url)
|
||||
|
||||
// TODO: Add code handle the response if needed
|
||||
|
||||
return response.status >= 200 && response.status < 300
|
||||
},
|
||||
async create(
|
||||
this: ApiContext,
|
||||
{ newResource, ...queryParams }: {{ pascalCase name }}CreateApiParams
|
||||
): Promise<{{ pascalCase name }}Id> {
|
||||
const urlParams: UrlParams = {}
|
||||
const url = endpoint(urlParams, queryParams)
|
||||
console.debug(
|
||||
`Creating {{ pascalCase name }} resource:`,
|
||||
newResource,
|
||||
`on url: ${url}`
|
||||
)
|
||||
const response = await this.client.post(url, newResource)
|
||||
|
||||
// TODO: Add code handle the response if needed
|
||||
|
||||
// TODO: Adapt code to handle the receiving of the resourceId (if any)
|
||||
const locationHeader = response.headers.location as
|
||||
| string
|
||||
| undefined
|
||||
|
||||
if (locationHeader) {
|
||||
const segments = new URL(locationHeader).pathname.split('/')
|
||||
const lastIdx = segments.length - 1
|
||||
const resourceId =
|
||||
segments[lastIdx] || segments[Math.max(lastIdx - 1, 0)]
|
||||
if (!resourceId)
|
||||
console.warn(new Error('Invalid location header received'))
|
||||
return resourceId as {{ pascalCase name }}Id
|
||||
}
|
||||
|
||||
console.warn(new Error('No location header received'))
|
||||
return '' as {{ pascalCase name }}Id
|
||||
},
|
||||
async update(
|
||||
this: ApiContext,
|
||||
{
|
||||
updatedResource,
|
||||
// resourceId,
|
||||
...queryParams
|
||||
}: {{ pascalCase name }}UpdateApiParams
|
||||
): Promise<boolean> {
|
||||
const urlParams: UrlParams = {
|
||||
// resourceId
|
||||
}
|
||||
const url = endpoint(urlParams, queryParams)
|
||||
console.debug(
|
||||
`updating {{ pascalCase name }} resource:`,
|
||||
updatedResource,
|
||||
`on url: ${url}`
|
||||
)
|
||||
const response = await this.client.put(url, updatedResource)
|
||||
|
||||
// TODO: Add code handle the response if needed
|
||||
|
||||
return response.status >= 200 && response.status < 300
|
||||
},
|
||||
async get(
|
||||
this: ApiContext,
|
||||
{ resourceId, ...queryParams }: {{ pascalCase name }}GetApiParams
|
||||
): Promise<{{ pascalCase name }}ApiResult> {
|
||||
const urlParams: UrlParams = {
|
||||
resourceId,
|
||||
}
|
||||
const url = endpoint(urlParams, queryParams)
|
||||
console.debug(
|
||||
`Getting {{ pascalCase name }} with id:`,
|
||||
resourceId,
|
||||
`on url: ${url}`
|
||||
)
|
||||
|
||||
const response = await this.client.get(url)
|
||||
|
||||
// TODO: Add code handle the response if needed
|
||||
|
||||
return response.data as {{ pascalCase name }}ApiResult
|
||||
},
|
||||
...defaultApiContext,
|
||||
}
|
||||
}
|
||||
|
||||
export const {{ camelCase name }}Api = {{ camelCase name }}ApiProto()
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Pagination } from '@/hookey'
|
||||
import { useApiContext } from '@/common/providers/api-context'
|
||||
import { {{ camelCase name }}Api } from './{{ kebabCase name }}.api'
|
||||
import { {{ pascalCase name }}GetApiParams } from './{{ kebabCase name }}.types'
|
||||
|
||||
export const use{{ pascalCase name }}s = Pagination.makePaginationHook({
|
||||
cacheKey: '{{ kebabCase name }}-api-list',
|
||||
clientFn: {{ camelCase name }}Api.list,
|
||||
useApiContext: useApiContext,
|
||||
// TODO: Connect getCount and getPageData with the list response data
|
||||
getCount: (data) => data.count,
|
||||
getPageData: (data) => data.results,
|
||||
})
|
||||
|
||||
export const use{{ pascalCase name }} = (params: {{ pascalCase name }}GetApiParams) => {
|
||||
return useQuery(
|
||||
['{{ kebabCase name }}-api-get', params] as [string, typeof params],
|
||||
({ queryKey: [_key, params] }) => {{ camelCase name }}Api.get(params)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Pagination } from '@/hookey'
|
||||
|
||||
export type {{ pascalCase name }} = {
|
||||
{{ camelCase name }}Id: {{ pascalCase name }}Id
|
||||
}
|
||||
|
||||
// TODO: Set the id type
|
||||
export type {{ pascalCase name }}Id = string | number
|
||||
|
||||
export type {{ pascalCase name }}ApiResult = {
|
||||
// TODO: Replace with actual get api result
|
||||
results: {{ pascalCase name }}
|
||||
}
|
||||
|
||||
export type {{ pascalCase name }}PaginatedApiResult = {
|
||||
// TODO: Replace with actual list api result
|
||||
results: {{ pascalCase name }}[]
|
||||
count: number
|
||||
}
|
||||
|
||||
export type {{ pascalCase name }}ListApiParams = Pagination.UsePaginatedQueryParams<{
|
||||
// TODO: Add other params here
|
||||
}>
|
||||
|
||||
export type {{ pascalCase name }}GetApiParams = {
|
||||
resourceId: {{ pascalCase name }}Id
|
||||
// TODO: Add other params here
|
||||
}
|
||||
|
||||
export type {{ pascalCase name }}CreateApiParams = {
|
||||
newResource: Omit<{{ pascalCase name }}, '{{ camelCase name }}Id'>
|
||||
// TODO: Add other params here
|
||||
}
|
||||
|
||||
export type {{ pascalCase name }}UpdateApiParams = {
|
||||
updatedResource: {{ pascalCase name }}
|
||||
// TODO: Switch params if the api requires an id in the url for updates
|
||||
// updatedResource: Omit<{{ pascalCase name }}, '{{ camelCase name }}Id'>
|
||||
// resourceId: {{ pascalCase name }}Id
|
||||
// TODO: Add other params here
|
||||
}
|
||||
|
||||
export type {{ pascalCase name }}DeleteApiParams = {
|
||||
resourceId: {{ pascalCase name }}Id
|
||||
// TODO: Add other params here
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './{{ kebabCase name }}.api'
|
||||
export * from './{{ kebabCase name }}.hooks'
|
||||
export * from './{{ kebabCase name }}.types'
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import styles from './{{ kebabCase name }}.module.css'
|
||||
|
||||
export type {{pascalCase name}}Props = {}
|
||||
|
||||
export function {{pascalCase name}}(props: {{pascalCase name}}Props) {
|
||||
return <div data-testid="{{ kebabCase name }}" className={styles.container}></div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { {{ pascalCase name }} } from './{{ kebabCase name }}.component'
|
||||
|
||||
const meta: Meta<typeof {{ pascalCase name }}> = {
|
||||
title: '{{ pascalCase name }}',
|
||||
component: {{ pascalCase name }},
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof {{ pascalCase name }}>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('{{ kebabCase name }}')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './{{kebabCase name}}.component'
|
||||
@@ -0,0 +1,8 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import styles from './{{ kebabCase name }}.module.css'
|
||||
|
||||
export type {{pascalCase name}}LayoutProps = PropsWithChildren<{}>
|
||||
|
||||
export function {{pascalCase name}}Layout(props: {{pascalCase name}}LayoutProps) {
|
||||
return <div data-testid="{{ kebabCase name }}-layout" className={styles.container}>{props.children}</div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { {{ pascalCase name }}Layout } from './{{ kebabCase name }}.layout'
|
||||
|
||||
const meta: Meta<typeof {{ pascalCase name }}Layout> = {
|
||||
title: '{{ pascalCase name }}Layout',
|
||||
component: {{ pascalCase name }}Layout,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof {{ pascalCase name }}Layout>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('{{ kebabCase name }}-layout')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './{{kebabCase name}}.layout'
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { {{ pascalCase name }}View } from './{{ kebabCase name }}.view'
|
||||
|
||||
const meta: Meta<typeof {{ pascalCase name }}View> = {
|
||||
title: '{{ pascalCase name }}View',
|
||||
component: {{ pascalCase name }}View,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof {{ pascalCase name }}View>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('{{ kebabCase name }}-view')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import styles from './{{ kebabCase name }}.module.css'
|
||||
|
||||
type {{pascalCase name}}ViewProps = {
|
||||
// query parameters
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
// url parameters
|
||||
params: { [key: string]: string | undefined }
|
||||
}
|
||||
|
||||
export function {{pascalCase name}}View(props: {{pascalCase name}}ViewProps) {
|
||||
return <div data-testid="{{ kebabCase name }}-view" className={styles.container}></div>
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './{{kebabCase name}}.view'
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { {{ pascalCase name }}Widget } from './{{ kebabCase name }}.widget'
|
||||
|
||||
const meta: Meta<typeof {{ pascalCase name }}Widget> = {
|
||||
title: '{{ pascalCase name }}Widget',
|
||||
component: {{ pascalCase name }}Widget,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof {{ pascalCase name }}Widget>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('{{ kebabCase name }}-widget')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import styles from './{{ kebabCase name }}.module.css'
|
||||
|
||||
export type {{pascalCase name}}WidgetProps = {}
|
||||
|
||||
export function {{pascalCase name}}Widget(props: {{pascalCase name}}WidgetProps) {
|
||||
return <div data-testid="{{ kebabCase name }}-widget" className={styles.container}></div>
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './{{kebabCase name}}.widget'
|
||||
5
examples/data-fetching/.commitlintrc.json
Normal file
5
examples/data-fetching/.commitlintrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"@commitlint/config-conventional"
|
||||
]
|
||||
}
|
||||
10
examples/data-fetching/.editorconfig
Normal file
10
examples/data-fetching/.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
4
examples/data-fetching/.eslintignore
Normal file
4
examples/data-fetching/.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
*.[tj]s
|
||||
*.[tj]sx
|
||||
|
||||
!src/**/*
|
||||
89
examples/data-fetching/.eslintrc.json
Normal file
89
examples/data-fetching/.eslintrc.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"plugins": [
|
||||
"sonarjs",
|
||||
"@typescript-eslint",
|
||||
"react",
|
||||
"react-hooks",
|
||||
"boundaries",
|
||||
"prettier",
|
||||
"jest-extended"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:sonarjs/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:boundaries/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"next",
|
||||
"plugin:storybook/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"@next/next/no-head-element": "off",
|
||||
"boundaries/element-types": [
|
||||
2,
|
||||
{
|
||||
"default": "allow",
|
||||
"rules": [
|
||||
{
|
||||
"from": ["component"],
|
||||
"allow": ["page", "globalStyle"],
|
||||
"message": "Components cannot import pages or global styles"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-misused-promises": [
|
||||
2,
|
||||
{
|
||||
"checksVoidReturn": {
|
||||
"attributes": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
],
|
||||
"@next/next/no-html-link-for-pages": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_|^props$" }
|
||||
],
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"no-unused-vars": "off"
|
||||
},
|
||||
"settings": {
|
||||
"boundaries/include": ["src/**"],
|
||||
"boundaries/ignore": ["src/**/*.spec.*", "src/**/*.test.*"],
|
||||
"boundaries/elements": [
|
||||
{
|
||||
"type": "component",
|
||||
"pattern": "components/*",
|
||||
"mode": "folder",
|
||||
"capture": ["component"]
|
||||
},
|
||||
{
|
||||
"type": "page",
|
||||
"pattern": "pages/**/*",
|
||||
"mode": "file",
|
||||
"capture": ["route", "elementName"]
|
||||
},
|
||||
{
|
||||
"type": "globalStyle",
|
||||
"pattern": "styles/*",
|
||||
"mode": "file",
|
||||
"capture": ["styleName"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
36
examples/data-fetching/.gitignore
vendored
Normal file
36
examples/data-fetching/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
6
examples/data-fetching/.husky/commit-msg
Executable file
6
examples/data-fetching/.husky/commit-msg
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx echo-cli "\033[0;34m>>> Checking commit message\033[0m"
|
||||
npx commitlint --edit $1
|
||||
npx echo-cli "\033[0;32mCommit message is OK\033[0m\n"
|
||||
5
examples/data-fetching/.husky/pre-commit
Executable file
5
examples/data-fetching/.husky/pre-commit
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx echo-cli "\033[0;34m>>> Linting the code\033[0m"
|
||||
npm run --silent lint
|
||||
8
examples/data-fetching/.prettierrc.json
Normal file
8
examples/data-fetching/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"editorconfig": true,
|
||||
"trailingComma": "es5",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf",
|
||||
"tabWidth": 4
|
||||
}
|
||||
32
examples/data-fetching/.storybook/main.ts
Normal file
32
examples/data-fetching/.storybook/main.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import path from 'path'
|
||||
import type { StorybookConfig } from '@storybook/nextjs'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-styling',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/nextjs',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
async webpackFinal(config, options) {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve?.alias,
|
||||
'@': path.resolve(__dirname, '../src/modules'),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
export default config
|
||||
16
examples/data-fetching/.storybook/preview.ts
Normal file
16
examples/data-fetching/.storybook/preview.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import '../src/styles/main.css'
|
||||
import type { Preview } from '@storybook/react'
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
19
examples/data-fetching/.vscode/extensions.json
vendored
Normal file
19
examples/data-fetching/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"christian-kohler.npm-intellisense",
|
||||
"aaron-bond.better-comments",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"formulahendry.auto-close-tag",
|
||||
"christian-kohler.path-intellisense",
|
||||
"mrmlnc.vscode-scss",
|
||||
"42crunch.vscode-openapi",
|
||||
"ms-playwright.playwright",
|
||||
"clinyong.vscode-css-modules",
|
||||
"esbenp.prettier-vscode",
|
||||
"teamchilla.blueprint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"csstools.postcss",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
16
examples/data-fetching/.vscode/launch.json
vendored
Normal file
16
examples/data-fetching/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run start:dev",
|
||||
"serverReadyAction": {
|
||||
"pattern": "started server on .+, url: (https?://.+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "debugWithChrome"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
42
examples/data-fetching/.vscode/settings.json
vendored
Normal file
42
examples/data-fetching/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnType": false,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"javascript.validate.enable": false,
|
||||
"javascript.suggestionActions.enabled": false,
|
||||
"blueprint.templatesPath": ["./.blueprints"],
|
||||
"eslint.validate": [
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"javascript",
|
||||
"javascriptreact"
|
||||
],
|
||||
"files.eol": "\n",
|
||||
"files.exclude": {
|
||||
// "**/.git": true,
|
||||
// "**/.svn": true,
|
||||
// "**/.hg": true,
|
||||
// "**/CVS": true,
|
||||
// "**/.DS_Store": true,
|
||||
// "**/Thumbs.db": true,
|
||||
// "**/next-env.d.ts": true,
|
||||
// "**/tsconfig.tsbuildinfo": true,
|
||||
// "**/package-lock.json": true,
|
||||
// "**/LICENSE": true,
|
||||
// "**/.next": true,
|
||||
// "**/.husky": true,
|
||||
// "**/.commitlintrc*": true,
|
||||
// "**/.prettierrc*": true,
|
||||
// "**/.gitignore": true,
|
||||
// "**/.eslint*": true,
|
||||
// "**/.vscode": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"css.validate": false,
|
||||
"files.associations": {
|
||||
"*.css": "css",
|
||||
"css": "css"
|
||||
}
|
||||
}
|
||||
7
examples/data-fetching/LICENSE
Normal file
7
examples/data-fetching/LICENSE
Normal file
@@ -0,0 +1,7 @@
|
||||
Copyright 2023 Daniel Peña Iglesias
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
38
examples/data-fetching/README.md
Normal file
38
examples/data-fetching/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
170
examples/data-fetching/global.d.ts
vendored
Normal file
170
examples/data-fetching/global.d.ts
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
/// <reference types="jest-extended" />
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module uxcale {
|
||||
declare type ApiResult<TData extends object> = {
|
||||
data: TData
|
||||
}
|
||||
|
||||
declare type PaginatedApiResult<TResource extends object> = ApiResult<
|
||||
TResource[]
|
||||
> & {
|
||||
pagination: {
|
||||
pageSize: number
|
||||
pageNumber: number
|
||||
totalPages: number
|
||||
totalElements: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare type EmptyObject = Record<string, never>
|
||||
|
||||
/**
|
||||
* Opaque
|
||||
* @desc Declares an Opaque type
|
||||
* @see https://dev.to/stereobooster/pragmatic-types-opaque-types-and-how-they-could-have-saved-mars-climate-orbiter-1551
|
||||
* @example
|
||||
* // Expect: "string | null"
|
||||
* NonUndefined<string | null | undefined>;
|
||||
*/
|
||||
declare type Opaque<K, T> = T & { __TYPE__: K }
|
||||
|
||||
/**
|
||||
* NonUndefined
|
||||
* @desc Exclude undefined from set `A`
|
||||
* @example
|
||||
* // Expect: "string | null"
|
||||
* NonUndefined<string | null | undefined>;
|
||||
*/
|
||||
declare type NonUndefined<A> = A extends undefined ? never : A
|
||||
|
||||
/**
|
||||
* FunctionKeys
|
||||
* @desc Get union type of keys that are functions in object type `T`
|
||||
* @example
|
||||
* type MixedProps = {name: string; setName: (name: string) => void; someKeys?: string; someFn?: (...args: any) => any;};
|
||||
*
|
||||
* // Expect: "setName | someFn"
|
||||
* type Keys = FunctionKeys<MixedProps>;
|
||||
*/
|
||||
declare type FunctionKeys<T extends object> = {
|
||||
[K in keyof T]-?: NonUndefined<T[K]> extends Function ? K : never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* NonFunctionKeys
|
||||
* @desc Get union type of keys that are non-functions in object type `T`
|
||||
* @example
|
||||
* type MixedProps = {name: string; setName: (name: string) => void; someKeys?: string; someFn?: (...args: any) => any;};
|
||||
*
|
||||
* // Expect: "name | someKey"
|
||||
* type Keys = NonFunctionKeys<MixedProps>;
|
||||
*/
|
||||
declare type NonFunctionKeys<T extends object> = {
|
||||
[K in keyof T]-?: NonUndefined<T[K]> extends Function ? never : K
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* RequiredKeys
|
||||
* @desc Get union type of keys that are required in object type `T`
|
||||
* @see https://stackoverflow.com/questions/52984808/is-there-a-way-to-get-all-required-properties-of-a-typescript-object
|
||||
* @example
|
||||
* type Props = { req: number; reqUndef: number | undefined; opt?: string; optUndef?: number | undefined; };
|
||||
*
|
||||
* // Expect: "req" | "reqUndef"
|
||||
* type Keys = RequiredKeys<Props>;
|
||||
*/
|
||||
declare type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* OptionalKeys
|
||||
* @desc Get union type of keys that are optional in object type `T`
|
||||
* @see https://stackoverflow.com/questions/52984808/is-there-a-way-to-get-all-required-properties-of-a-typescript-object
|
||||
* @example
|
||||
* type Props = { req: number; reqUndef: number | undefined; opt?: string; optUndef?: number | undefined; };
|
||||
*
|
||||
* // Expect: "opt" | "optUndef"
|
||||
* type Keys = OptionalKeys<Props>;
|
||||
*/
|
||||
declare type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
* PromiseType
|
||||
* @desc Obtain Promise resolve type
|
||||
* @example
|
||||
* // Expect: string;
|
||||
* type Response = PromiseType<Promise<string>>;
|
||||
*/
|
||||
declare type PromiseType<T extends Promise<any>> = T extends Promise<infer U>
|
||||
? U
|
||||
: never
|
||||
|
||||
/**
|
||||
* WithOptional
|
||||
* @desc From `T` make a set of properties by key `K` become optional
|
||||
* @example
|
||||
* type Props = {
|
||||
* name: string;
|
||||
* age: number;
|
||||
* visible: boolean;
|
||||
* };
|
||||
*
|
||||
* // Expect: { name?: string; age?: number; visible?: boolean; }
|
||||
* type Props = WithOptional<Props>;
|
||||
*
|
||||
* // Expect: { name: string; age?: number; visible?: boolean; }
|
||||
* type Props = WithOptional<Props, 'age' | 'visible'>;
|
||||
*/
|
||||
declare type WithOptional<T, K extends keyof T = keyof T> = Omit<T, K> &
|
||||
Partial<Pick<T, K>>
|
||||
|
||||
/**
|
||||
* WithRequired
|
||||
* @desc From `T` make a set of properties by key `K` become required
|
||||
* @example
|
||||
* type Props = {
|
||||
* name?: string;
|
||||
* age?: number;
|
||||
* visible?: boolean;
|
||||
* };
|
||||
*
|
||||
* // Expect: { name: string; age: number; visible: boolean; }
|
||||
* type Props = WithRequired<Props>;
|
||||
*
|
||||
* // Expect: { name?: string; age: number; visible: boolean; }
|
||||
* type Props = WithRequired<Props, 'age' | 'visible'>;
|
||||
*/
|
||||
declare type WithRequired<T, K extends keyof T = keyof T> = Omit<T, K> &
|
||||
Required<Pick<T, K>>
|
||||
|
||||
/**
|
||||
* FirstParam
|
||||
* @desc From a function `T` get the first parameter type
|
||||
* @example
|
||||
* type Props = {
|
||||
* name: string;
|
||||
* age: number;
|
||||
* visible: boolean;
|
||||
* };
|
||||
*
|
||||
* // Expect: { name?: string; age?: number; visible?: boolean; }
|
||||
* type Props = WithOptional<Props>;
|
||||
*
|
||||
* // Expect: { name: string; age?: number; visible?: boolean; }
|
||||
* type Props = WithOptional<Props, 'age' | 'visible'>;
|
||||
*/
|
||||
declare type FirstParam<T extends (...args: any) => any> = Parameters<T>[0]
|
||||
18
examples/data-fetching/jest.config.ts
Normal file
18
examples/data-fetching/jest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Config } from 'jest'
|
||||
import nextJest from 'next/jest'
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
dir: './',
|
||||
})
|
||||
|
||||
const config: Config = {
|
||||
modulePathIgnorePatterns: ['<rootDir>/.blueprints'],
|
||||
setupFiles: ['dotenv/config'],
|
||||
setupFilesAfterEnv: [
|
||||
'jest-extended/all',
|
||||
'@testing-library/jest-dom/extend-expect',
|
||||
],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
}
|
||||
|
||||
export default createJestConfig(config)
|
||||
15
examples/data-fetching/next.config.js
Normal file
15
examples/data-fetching/next.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
// Avoiding CORS issues
|
||||
// async rewrites() {
|
||||
// return [
|
||||
// {
|
||||
// source: '/api/:slug*',
|
||||
// destination: 'http://localhost:3000/api/:slug*'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
37722
examples/data-fetching/package-lock.json
generated
Normal file
37722
examples/data-fetching/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
110
examples/data-fetching/package.json
Normal file
110
examples/data-fetching/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "front-arch-ssr-react",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "npm run build:prod",
|
||||
"build:prod": "next build",
|
||||
"build:storybook": "storybook build",
|
||||
"serve": "next start",
|
||||
"lint": "npm run lint:all",
|
||||
"lint:all": "eslint .",
|
||||
"start": "npm run start:storybook",
|
||||
"start:dev": "next dev",
|
||||
"start:storybook": "storybook dev -p 6006 --no-open",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@tanstack/react-query": "^4.29.12",
|
||||
"axios": "^1.4.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"next": "^13.4.1",
|
||||
"next-auth": "^4.22.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-table": "^7.8.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.5.0",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mertasan/tailwindcss-variables": "^2.6.1",
|
||||
"@radix-ui/react-accordion": "^1.1.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.2",
|
||||
"@radix-ui/react-avatar": "^1.0.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.3",
|
||||
"@radix-ui/react-collapsible": "^1.0.2",
|
||||
"@radix-ui/react-context-menu": "^2.1.3",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@radix-ui/react-hover-card": "^1.0.5",
|
||||
"@radix-ui/react-label": "^2.0.1",
|
||||
"@radix-ui/react-menubar": "^1.0.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.2",
|
||||
"@radix-ui/react-popover": "^1.0.5",
|
||||
"@radix-ui/react-progress": "^1.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.1.2",
|
||||
"@radix-ui/react-scroll-area": "^1.0.3",
|
||||
"@radix-ui/react-select": "^1.2.1",
|
||||
"@radix-ui/react-separator": "^1.0.2",
|
||||
"@radix-ui/react-slider": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.0.1",
|
||||
"@radix-ui/react-switch": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.3",
|
||||
"@radix-ui/react-toast": "^1.1.3",
|
||||
"@radix-ui/react-toggle": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.5",
|
||||
"@storybook/addon-essentials": "^7.0.10",
|
||||
"@storybook/addon-interactions": "^7.0.10",
|
||||
"@storybook/addon-links": "^7.0.10",
|
||||
"@storybook/addon-styling": "^1.0.6",
|
||||
"@storybook/blocks": "^7.0.10",
|
||||
"@storybook/jest": "^0.1.0",
|
||||
"@storybook/nextjs": "^7.0.10",
|
||||
"@storybook/react": "^7.0.10",
|
||||
"@storybook/testing-library": "^0.0.14-next.2",
|
||||
"@tanstack/react-query-devtools": "^4.29.12",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/node": "18.15.7",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"classnames": "^2.3.2",
|
||||
"cmdk": "^0.2.0",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-boundaries": "^3.1.0",
|
||||
"eslint-plugin-css-modules": "^2.11.0",
|
||||
"eslint-plugin-jest-extended": "^2.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-sonarjs": "^0.19.0",
|
||||
"eslint-plugin-storybook": "^0.6.12",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-extended": "^3.2.4",
|
||||
"lucide-react": "^0.215.0",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-preset-env": "^8.3.2",
|
||||
"prettier": "^2.8.7",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"sass": "^1.62.1",
|
||||
"storybook": "^7.0.10",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"typescript": "5.0.2",
|
||||
"typescript-plugin-css-modules": "^5.0.0",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
8
examples/data-fetching/postcss.config.js
Normal file
8
examples/data-fetching/postcss.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'tailwindcss/nesting': {},
|
||||
'postcss-preset-env': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
examples/data-fetching/public/favicon.ico
Normal file
BIN
examples/data-fetching/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
examples/data-fetching/public/next.svg
Normal file
1
examples/data-fetching/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
examples/data-fetching/public/thirteen.svg
Normal file
1
examples/data-fetching/public/thirteen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
examples/data-fetching/public/vercel.svg
Normal file
1
examples/data-fetching/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||
|
After Width: | Height: | Size: 629 B |
1
examples/data-fetching/src/app/auth/sign-in/page.tsx
Normal file
1
examples/data-fetching/src/app/auth/sign-in/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { SignInView as default } from '@/auth/views/sign-in'
|
||||
17
examples/data-fetching/src/app/layout.tsx
Normal file
17
examples/data-fetching/src/app/layout.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import '../styles/main.css'
|
||||
import { RootLayout } from '@/common/layouts/root'
|
||||
import { ApplicationLayout } from '@/common/layouts/application'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next SSR Archetype',
|
||||
description: 'Sample skeleton webpage',
|
||||
}
|
||||
|
||||
export default function Layout(props: PropsWithChildren) {
|
||||
return (
|
||||
<RootLayout>
|
||||
<ApplicationLayout>{props.children}</ApplicationLayout>
|
||||
</RootLayout>
|
||||
)
|
||||
}
|
||||
1
examples/data-fetching/src/app/page.tsx
Normal file
1
examples/data-fetching/src/app/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { AppMainView as default } from '@/common/views/main'
|
||||
1
examples/data-fetching/src/app/users/page.tsx
Normal file
1
examples/data-fetching/src/app/users/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { UsersMainView as default } from '@/users/views/main'
|
||||
16
examples/data-fetching/src/app/word-list/page.tsx
Normal file
16
examples/data-fetching/src/app/word-list/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getRandomWordList } from "@/words/word-list/utils";
|
||||
|
||||
const getServerSideProps = async () => {
|
||||
const wordList = await getRandomWordList();
|
||||
console.log("Data is being fetch...", wordList)
|
||||
return { props: { wordList:wordList } }
|
||||
}
|
||||
|
||||
export default async function WordList(){
|
||||
const wordList = await getServerSideProps();
|
||||
return (
|
||||
<div data-testid="word-list-view">
|
||||
WordList server props: {JSON.stringify(wordList)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { LoadingButton } from '@/common/components/loading-button'
|
||||
import * as Form from '@/common/components/ui/form'
|
||||
import { z } from 'zod'
|
||||
|
||||
export type AuthFormCredentials = { username: string; password: string }
|
||||
export type AuthFormProps = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
'onSubmit'
|
||||
> & {
|
||||
onSubmit: (credentials: AuthFormCredentials) => Promise<void | string>
|
||||
}
|
||||
|
||||
export function AuthForm({ className, onSubmit, ...props }: AuthFormProps) {
|
||||
const form = Form.useZodForm<AuthFormCredentials>({
|
||||
criteriaMode: 'firstError',
|
||||
schema: z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: 'Username must be at least 2 characters',
|
||||
})
|
||||
.max(50),
|
||||
password: z.string().min(8, {
|
||||
message: 'Password must contain at least 8 characters',
|
||||
}),
|
||||
}),
|
||||
onSubmit: async (data) => {
|
||||
try {
|
||||
const error = await onSubmit(data)
|
||||
if (typeof error === 'string') {
|
||||
form.setError('root.submit', {
|
||||
type: 'server',
|
||||
message: error,
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('AuthForm.onSubmit error', e)
|
||||
form.setError('root.submit', {
|
||||
type: 'unknown',
|
||||
message: 'Unknown error',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="auth-form"
|
||||
className={cn('grid gap-6', className)}
|
||||
{...props}
|
||||
>
|
||||
<Form.Root {...form} className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Form.Input placeholder="pibone" {...field} />
|
||||
<Form.Message />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<Form.Item>
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Input
|
||||
placeholder="*****"
|
||||
type="password"
|
||||
{...field}
|
||||
/>
|
||||
<Form.Message />
|
||||
</Form.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Form.CustomMessage isError>
|
||||
{form.formState.errors.root?.submit?.message || null}
|
||||
</Form.CustomMessage>
|
||||
<LoadingButton
|
||||
loading={form.formState.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
Sign In
|
||||
</LoadingButton>
|
||||
</Form.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { AuthForm } from './auth-form.component'
|
||||
|
||||
const meta: Meta<typeof AuthForm> = {
|
||||
title: 'AuthForm',
|
||||
component: AuthForm,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AuthForm>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('auth-form')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './auth-form.component'
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useApiContext } from '@/common/providers/api-context'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { PropsWithChildren, useLayoutEffect } from 'react'
|
||||
|
||||
export const AuthToApiContextConnectionProvider = ({
|
||||
children,
|
||||
}: PropsWithChildren) => {
|
||||
const apiContext = useApiContext()
|
||||
const { data: session } = useSession()
|
||||
const apiSession = session?.apiSession
|
||||
|
||||
useLayoutEffect(() => {
|
||||
apiContext.setAuthorizationToken(apiSession?.accessToken)
|
||||
}, [apiContext, apiSession])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
'use client'
|
||||
export * from './auth-to-apicontext-connection.provider'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { getApiContext } from '@/common/providers/api-context/api-context.default'
|
||||
import { AuthOptions, User } from 'next-auth'
|
||||
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
},
|
||||
pages: {
|
||||
signIn: '/auth/sign-in',
|
||||
},
|
||||
callbacks: {
|
||||
async session({ session, token }) {
|
||||
if (token?.accessToken && session) {
|
||||
// Update server side API_CONTEXT
|
||||
getApiContext().setAuthorizationToken(token.accessToken)
|
||||
session.apiSession = {
|
||||
accessToken: token.accessToken,
|
||||
refreshToken: token.refreshToken,
|
||||
}
|
||||
}
|
||||
return session
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
if (user?.apiSession) {
|
||||
token.accessToken = user.apiSession.accessToken
|
||||
token.refreshToken = user.apiSession.refreshToken
|
||||
}
|
||||
return token
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: 'custom-credentials',
|
||||
credentials: {
|
||||
email: { type: 'email' },
|
||||
password: { type: 'password' },
|
||||
},
|
||||
authorize: async (credentials, _req) => {
|
||||
// TODO: Connect with login API in the backend
|
||||
const user: User = {
|
||||
id: '1',
|
||||
email: 'dpenai@pibone.com',
|
||||
name: 'Dani Peña Iglesias',
|
||||
role: 'admin',
|
||||
// TODO: Set the incoming api token here, if needed
|
||||
apiSession: {
|
||||
accessToken: 'jwt-token',
|
||||
// refreshToken: 'refresh-token-if-any',
|
||||
},
|
||||
}
|
||||
|
||||
return credentials?.password === 'safe-password' ? user : null
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Session } from 'next-auth'
|
||||
import { SessionProvider } from 'next-auth/react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
export const AuthProvider = ({
|
||||
children,
|
||||
session,
|
||||
}: PropsWithChildren<{ session?: Session }>) => {
|
||||
return <SessionProvider session={session}>{children}</SessionProvider>
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
'use client'
|
||||
export * from './auth.provider'
|
||||
28
examples/data-fetching/src/modules/auth/providers/auth/nextauth.d.ts
vendored
Normal file
28
examples/data-fetching/src/modules/auth/providers/auth/nextauth.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ApiSession, DefaultSession } from 'next-auth'
|
||||
import { DefaultJWT } from 'next-auth/jwt'
|
||||
|
||||
declare module 'next-auth' {
|
||||
declare type Role = 'user' | 'admin'
|
||||
declare interface ApiSession {
|
||||
accessToken: string
|
||||
refreshToken?: string
|
||||
}
|
||||
declare interface User {
|
||||
id: string
|
||||
role: Role
|
||||
image?: string
|
||||
name?: string
|
||||
email?: string
|
||||
apiSession?: ApiSession
|
||||
}
|
||||
|
||||
declare interface Session extends DefaultSession {
|
||||
apiSession?: ApiSession
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'next-auth/jwt' {
|
||||
declare interface JWT
|
||||
extends WithOptional<ApiSession, 'accessToken'>,
|
||||
DefaultJWT {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sign-in.view'
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { SignInView } from './sign-in.view'
|
||||
|
||||
const meta: Meta<typeof SignInView> = {
|
||||
title: 'SignInView',
|
||||
component: SignInView,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof SignInView>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('sign-in-view')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
|
||||
import { LoginWidget } from '../../widgets/login'
|
||||
|
||||
type SignInViewProps = {
|
||||
// query parameters
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
// url parameters
|
||||
params: { [key: string]: string | undefined }
|
||||
}
|
||||
|
||||
export function SignInView(props: SignInViewProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="container relative md:h-[800px] grid items-center justify-stretch md:justify-center lg:max-w-none p-0">
|
||||
<div
|
||||
className="absolute hidden md:block inset-0 bg-cover -z-10"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'url(https://images.unsplash.com/photo-1590069261209-f8e9b8642343?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1376&q=80)',
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center px-4 lg:px-6 pt-4 pb-6">
|
||||
<div className="absolute w-full h-full md:rounded-md bg-white p-2 -z-10" />
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
Account Sign In
|
||||
</h1>
|
||||
<p className="text-sm text-primary">
|
||||
Enter your username below to create your account
|
||||
</p>
|
||||
</div>
|
||||
<LoginWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { AccountWidget } from './account.widget'
|
||||
import { AuthProvider } from '@/auth/providers/auth'
|
||||
|
||||
const meta: Meta<typeof AccountWidget> = {
|
||||
title: 'AccountWidget',
|
||||
component: AccountWidget,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AccountWidget>
|
||||
|
||||
export const LoggedIn: Story = {
|
||||
render: (p) => (
|
||||
<AuthProvider
|
||||
session={{
|
||||
user: {
|
||||
email: 'example@pibone.com',
|
||||
name: 'Dani Peña Iglesias',
|
||||
image: 'https://github.com/pibone.png',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccountWidget {...p} />
|
||||
</AuthProvider>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('account-widget')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
|
||||
export const LoggedOut: Story = {
|
||||
render: (p) => (
|
||||
<AuthProvider session={undefined}>
|
||||
<AccountWidget {...p} />
|
||||
</AuthProvider>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('account-widget')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import styles from './account.module.css'
|
||||
import classNames from 'classnames'
|
||||
import { useSession, signIn, signOut } from 'next-auth/react'
|
||||
import * as Avatar from '@/common/components/ui/avatar'
|
||||
import * as Popover from '@/common/components/ui/popover'
|
||||
import { Button } from '@/common/components/ui/button'
|
||||
|
||||
export type AccountWidgetProps = {
|
||||
className: string
|
||||
}
|
||||
|
||||
export function AccountWidget(props: AccountWidgetProps) {
|
||||
const { status } = useSession()
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="account-widget"
|
||||
className={classNames(styles.container, props.className)}
|
||||
>
|
||||
{selectComponent(status)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function selectComponent(authStatus: ReturnType<typeof useSession>['status']) {
|
||||
return authStatus === 'authenticated' ? (
|
||||
<LoggedInUser />
|
||||
) : (
|
||||
<Button onClick={() => signIn()}>Sign In</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function LoggedInUser() {
|
||||
const { data: session } = useSession()
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild className="cursor-pointer">
|
||||
<Avatar.Root>
|
||||
<Avatar.Image src={session?.user?.image ?? undefined} />
|
||||
<Avatar.Fallback>
|
||||
{(session?.user?.name ?? '')
|
||||
.split(/\s+/)
|
||||
.map((v) => v[0].toUpperCase())
|
||||
.join('') || 'Unknown user'}
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<LoggedInMenu />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function LoggedInMenu() {
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
signOut({
|
||||
redirect: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
'use client'
|
||||
|
||||
export type { AccountWidgetProps } from './account.widget'
|
||||
export { AccountWidget } from './account.widget'
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './login.widget'
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { LoginWidget } from './login.widget'
|
||||
|
||||
const meta: Meta<typeof LoginWidget> = {
|
||||
title: 'LoginWidget',
|
||||
component: LoginWidget,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof LoginWidget>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('login-widget')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import { AuthForm } from '../../components/auth-form'
|
||||
import styles from './login.module.css'
|
||||
|
||||
export type LoginWidgetProps = {}
|
||||
|
||||
export function LoginWidget(props: LoginWidgetProps) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const callbackUrl = searchParams.get('callbackUrl') || '/profile'
|
||||
return (
|
||||
<div data-testid="login-widget" className={styles.container}>
|
||||
<AuthForm
|
||||
onSubmit={async (credentials) => {
|
||||
const result = await signIn('credentials', {
|
||||
redirect: false,
|
||||
...credentials,
|
||||
callbackUrl,
|
||||
})
|
||||
|
||||
console.debug('Login result:', result)
|
||||
|
||||
if (result?.error === 'CredentialsSignin') {
|
||||
return 'Invalid credentials'
|
||||
}
|
||||
|
||||
router.replace(callbackUrl)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ClipboardCheck,
|
||||
Copy,
|
||||
File,
|
||||
HelpCircle,
|
||||
Image,
|
||||
Loader2,
|
||||
Moon,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Settings,
|
||||
SunMedium,
|
||||
Trash,
|
||||
Twitter,
|
||||
User,
|
||||
X,
|
||||
Minus,
|
||||
Linkedin,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Youtube,
|
||||
} from 'lucide-react'
|
||||
|
||||
export const Icons = {
|
||||
logo: (props) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
</svg>
|
||||
),
|
||||
// informative icons
|
||||
spinner: Loader2,
|
||||
chevronLeft: ChevronLeft,
|
||||
chevronRight: ChevronRight,
|
||||
trash: Trash,
|
||||
file: File,
|
||||
media: Image,
|
||||
warning: AlertTriangle,
|
||||
user: User,
|
||||
arrowRight: ArrowRight,
|
||||
check: Check,
|
||||
// action icons
|
||||
darkTheme: Moon,
|
||||
lightTheme: SunMedium,
|
||||
close: X,
|
||||
settings: Settings,
|
||||
options: MoreVertical,
|
||||
add: Plus,
|
||||
remove: Minus,
|
||||
help: HelpCircle,
|
||||
copy: Copy,
|
||||
copyDone: ClipboardCheck,
|
||||
// social icons
|
||||
twitter: Twitter,
|
||||
linkedin: Linkedin,
|
||||
facebook: Facebook,
|
||||
instagram: Instagram,
|
||||
youtube: Youtube,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { Icons } from './icons.component'
|
||||
|
||||
const meta: Meta<typeof Icons> = {
|
||||
title: 'Icons',
|
||||
component: Icons,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Icons>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('icons')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './icons.component'
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './loading-button.component'
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, { ComponentPropsWithRef } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Icons } from '../icons'
|
||||
|
||||
export function LoadingButton({
|
||||
loading,
|
||||
children,
|
||||
...props
|
||||
}: ComponentPropsWithRef<typeof Button> & { loading?: boolean }) {
|
||||
return (
|
||||
<Button {...props} data-testid="loading-button">
|
||||
{loading && <Icons.spinner className="animate-spin w-8 h-8 mr-2" />}
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { LoadingButton } from './loading-button.component'
|
||||
|
||||
const meta: Meta<typeof LoadingButton> = {
|
||||
title: 'LoadingButton',
|
||||
component: LoadingButton,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof LoadingButton>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('loading-button')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import styles from './accordion.module.css'
|
||||
|
||||
export const Root = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Root
|
||||
data-testid="accordion"
|
||||
ref={ref}
|
||||
className={cn(styles.container, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Root>
|
||||
))
|
||||
export const AccordionRoot = Root
|
||||
Root.displayName = 'AccordionRoot'
|
||||
|
||||
export const Item = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(styles.item, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AccordionItem = Item
|
||||
|
||||
Item.displayName = 'AccordionItem'
|
||||
|
||||
export const Trigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className={styles.header}>
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(styles.trigger, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className={styles.icon} />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
export const AccordionTrigger = Trigger
|
||||
|
||||
Trigger.displayName = 'AccordionTrigger'
|
||||
|
||||
export const Content = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(styles.content, className)}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.contentWrapper}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
export const AccordionContent = Content
|
||||
|
||||
Content.displayName = 'AccordionContent'
|
||||
@@ -0,0 +1,24 @@
|
||||
div:where(.container) {
|
||||
:where(.item) {
|
||||
@apply border-b;
|
||||
}
|
||||
|
||||
:where(.header) {
|
||||
@apply flex;
|
||||
}
|
||||
|
||||
:where(.content) {
|
||||
@apply overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down;
|
||||
}
|
||||
|
||||
:where(.contentWrapper) {
|
||||
@apply pb-4 pt-0;
|
||||
}
|
||||
|
||||
:where(.trigger) {
|
||||
@apply flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180;
|
||||
}
|
||||
:where(.icon) {
|
||||
@apply h-4 w-4 transition-transform duration-200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import * as Accordion from './accordion.component'
|
||||
|
||||
const meta: Meta<typeof Accordion.Root> = {
|
||||
title: 'Accordion',
|
||||
component: Accordion.Root,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Accordion.Root>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (props) => (
|
||||
<Accordion.Root {...props}>
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Sección 1</Accordion.Trigger>
|
||||
<Accordion.Content>Contenido 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2" onClick={() => console.log('hola')}>
|
||||
<Accordion.Trigger>Sección 2</Accordion.Trigger>
|
||||
<Accordion.Content>Contenido 2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion.Root>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('accordion')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './accordion.component'
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { HTMLProps } from 'react'
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
import cn from 'classnames'
|
||||
import { buttonVariants } from '../button'
|
||||
import styles from './alert-dialog.module.css'
|
||||
|
||||
export const Root = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Root>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Root ref={ref} {...props}>
|
||||
{children}
|
||||
</AlertDialogPrimitive.Root>
|
||||
))
|
||||
export const AlertDialogRoot = Root
|
||||
Root.displayName = 'AlertDialogRoot'
|
||||
|
||||
export const Trigger = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Trigger>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Trigger
|
||||
data-testid="alert-dialog"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AlertDialogPrimitive.Trigger>
|
||||
))
|
||||
export const AlertDialogTrigger = Trigger
|
||||
Trigger.displayName = 'AlertDialogTrigger'
|
||||
|
||||
const AlertDialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
<div className={styles.portal}>{children}</div>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
)
|
||||
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(styles.overlay, className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
export const Modal = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(styles.content, className)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
export const AlertDialogModal = Modal
|
||||
Modal.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
export const Header = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLProps<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn(styles.header, className)} {...props} />
|
||||
))
|
||||
export const AlertDialogHeader = Header
|
||||
Header.displayName = 'AlertDialogHeader'
|
||||
|
||||
export const Footer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLProps<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn(styles.footer, className)} {...props} />
|
||||
))
|
||||
export const AlertDialogFooter = Footer
|
||||
Footer.displayName = 'AlertDialogFooter'
|
||||
|
||||
export const Title = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(styles.title, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AlertDialogTitle = Title
|
||||
Title.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
export const Description = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn(styles.description, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AlertDialogDescription = Description
|
||||
Description.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
export const Action = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AlertDialogAction = Action
|
||||
Action.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
export const Cancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
styles.cancel,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AlertDialogCancel = Cancel
|
||||
Cancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
@@ -0,0 +1,31 @@
|
||||
/* elements */
|
||||
div:where(.portal) {
|
||||
@apply fixed inset-0 z-50 flex items-end justify-center sm:items-center;
|
||||
:where(.overlay) {
|
||||
@apply fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity;
|
||||
}
|
||||
|
||||
:where(.content) {
|
||||
@apply fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg sm:rounded-lg md:w-full;
|
||||
}
|
||||
|
||||
:where(.header) {
|
||||
@apply flex flex-col space-y-2 text-center sm:text-left;
|
||||
}
|
||||
|
||||
:where(.footer) {
|
||||
@apply flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2;
|
||||
}
|
||||
|
||||
:where(.title) {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
|
||||
:where(.description) {
|
||||
@apply text-sm text-muted-fg;
|
||||
}
|
||||
|
||||
:where(.cancel) {
|
||||
@apply mt-2 sm:mt-0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import * as AlertDialog from './alert-dialog.component'
|
||||
import { Button } from '../button'
|
||||
|
||||
const meta: Meta<typeof AlertDialog.Root> = {
|
||||
title: 'AlertDialog',
|
||||
component: AlertDialog.Root,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AlertDialog.Root>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (p) => (
|
||||
<AlertDialog.Root {...p}>
|
||||
<AlertDialog.Trigger asChild>
|
||||
<Button variant="outline">Show Dialog</Button>
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Modal>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Are you sure?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This action cannot be undone
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action>Continue</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Modal>
|
||||
</AlertDialog.Root>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('alert-dialog')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './alert-dialog.component'
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { VariantProps, cva } from 'class-variance-authority'
|
||||
import cn from 'classnames'
|
||||
import styles from './alert.module.css'
|
||||
|
||||
export const alertVariants = cva(styles.container, {
|
||||
variants: {
|
||||
variant: {
|
||||
default: styles.defaultVariant,
|
||||
danger: styles.danger,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
})
|
||||
|
||||
export const Root = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
data-testid="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AlertRoot = Root
|
||||
Root.displayName = 'AlertRoot'
|
||||
|
||||
export const Title = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn(styles.title, className)} {...props} />
|
||||
))
|
||||
export const AlertTitle = Title
|
||||
Title.displayName = 'AlertTitle'
|
||||
|
||||
export const Description = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn(styles.description, className)} {...props} />
|
||||
))
|
||||
export const AlertDescription = Description
|
||||
Description.displayName = 'AlertDescription'
|
||||
@@ -0,0 +1,21 @@
|
||||
div:where(.container) {
|
||||
@apply relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11;
|
||||
|
||||
/* variants */
|
||||
:where(.defaultVariant) {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
:where(.danger) {
|
||||
@apply text-danger border-danger/50 dark:border-danger [&>svg]:text-danger;
|
||||
}
|
||||
|
||||
/* elements */
|
||||
:where(.title) {
|
||||
@apply flex mb-1 font-medium leading-none tracking-tight;
|
||||
}
|
||||
|
||||
:where(.description) {
|
||||
@apply text-sm [&_p]:leading-relaxed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import * as Alert from './alert.component'
|
||||
import { Terminal } from 'lucide-react'
|
||||
|
||||
const meta: Meta<typeof Alert.Root> = {
|
||||
title: 'Alert',
|
||||
component: Alert.Root,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Alert.Root>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (p) => (
|
||||
<Alert.Root {...p}>
|
||||
<Alert.Title>
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span className="pl-2">Heads up!</span>
|
||||
</Alert.Title>
|
||||
<Alert.Description>
|
||||
You can add new components to your app using the blueprints.
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('alert')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './alert.component'
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import { AspectRatio } from './aspect-ratio.component'
|
||||
|
||||
const meta: Meta<typeof AspectRatio> = {
|
||||
title: 'AspectRatio',
|
||||
component: AspectRatio,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof AspectRatio>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (p) => (
|
||||
<AspectRatio
|
||||
{...p}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'red',
|
||||
}}
|
||||
ratio={10}
|
||||
>
|
||||
<div style={{ height: 10, width: 10, backgroundColor: 'teal' }} />
|
||||
</AspectRatio>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('aspect-ratio')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './aspect-ratio.component'
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||
import styles from './avatar.module.css'
|
||||
|
||||
export const Root = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
data-testid="avatar"
|
||||
className={cn(styles.container, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AvatarRoot = Root
|
||||
Root.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
export const Image = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn(styles.image, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AvatarImage = Image
|
||||
Image.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
export const Fallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(styles.fallback, className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
export const AvatarFallback = Fallback
|
||||
Fallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
@@ -0,0 +1,10 @@
|
||||
span:where(.container) {
|
||||
@apply relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full;
|
||||
:where(.image) {
|
||||
@apply aspect-square h-full w-full;
|
||||
}
|
||||
|
||||
:where(.fallback) {
|
||||
@apply flex h-full w-full items-center justify-center rounded-full bg-muted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { expect } from '@storybook/jest'
|
||||
import { within } from '@storybook/testing-library'
|
||||
import * as Avatar from './avatar.component'
|
||||
|
||||
const meta: Meta<typeof Avatar.Root> = {
|
||||
title: 'Avatar',
|
||||
component: Avatar.Root,
|
||||
argTypes: {},
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof Avatar.Root>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (p) => (
|
||||
<Avatar.Root {...p}>
|
||||
<Avatar.Image src="https://github.com/pibone.png" alt="@pibone" />
|
||||
<Avatar.Fallback>PI</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('avatar')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultFallback: Story = {
|
||||
render: (p) => (
|
||||
<Avatar.Root {...p}>
|
||||
<Avatar.Image
|
||||
src="https://terrible-invalid-url.com/invalid-image.png"
|
||||
alt="@pibone"
|
||||
/>
|
||||
<Avatar.Fallback>PI</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
),
|
||||
args: {},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement)
|
||||
const container = canvas.getByTestId('avatar')
|
||||
|
||||
expect(container).toBeTruthy()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client'
|
||||
|
||||
export * from './avatar.component'
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { VariantProps, cva } from 'class-variance-authority'
|
||||
import styles from './badge.module.css'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
|
||||
export const badgeVariants = cva(styles.container, {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: styles.primary,
|
||||
secondary: styles.secondary,
|
||||
danger: styles.danger,
|
||||
outline: styles.outline,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
})
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ className, variant, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'div'
|
||||
return (
|
||||
<Comp
|
||||
data-testid="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Badge.displayName = 'Badge'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user