├── .eslintignore ├── .prettierignore ├── .jest └── setup.ts ├── .prettierrc ├── public ├── favicon.ico ├── avatars │ ├── 0.jpg │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.jpg │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.jpg │ ├── 2.jpg │ ├── 20.jpg │ ├── 21.jpg │ ├── 22.jpg │ ├── 23.jpg │ ├── 24.jpg │ ├── 25.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ ├── 9.jpg │ └── 17.jpeg ├── servers │ ├── next.png │ ├── mirage.png │ └── tailwind.png ├── ginto │ └── ginto-semibold.woff ├── whitney │ ├── whitney-book.woff │ ├── whitney-light.woff │ ├── whitney-medium.woff │ └── whitney-semibold.woff └── vercel.svg ├── faker.d.ts ├── generators ├── templates │ ├── Component.tsx.hbs │ ├── stories.tsx.hbs │ └── test.tsx.hbs └── plopfile.js ├── postcss.config.js ├── .editorconfig ├── next-env.d.ts ├── .vscode └── settings.json ├── src ├── components │ ├── ServerHeader │ │ ├── test.tsx │ │ ├── stories.tsx │ │ └── index.tsx │ ├── Message │ │ ├── index.tsx │ │ ├── test.tsx │ │ └── stories.tsx │ ├── ChannelList │ │ ├── stories.tsx │ │ ├── test.tsx │ │ └── index.tsx │ ├── Icons │ │ ├── test.tsx │ │ ├── stories.tsx │ │ └── index.tsx │ ├── ChannelLink │ │ ├── stories.tsx │ │ ├── index.tsx │ │ └── test.tsx │ ├── ChannelTopbar │ │ ├── stories.tsx │ │ ├── test.tsx │ │ └── index.tsx │ ├── MessageWithUser │ │ ├── stories.tsx │ │ ├── test.tsx │ │ └── index.tsx │ └── NavLink │ │ ├── stories.tsx │ │ ├── test.tsx │ │ └── index.tsx ├── pages │ ├── _document.tsx │ ├── servers │ │ └── [sid] │ │ │ └── channels │ │ │ └── [cid].tsx │ └── _app.tsx ├── styles │ └── globals.css └── data.ts ├── next.config.js ├── .eslintrc.json ├── jest.config.js ├── .gitignore ├── tsconfig.json ├── .storybook ├── preview.js └── main.js ├── .github └── workflows │ └── ci.yml ├── tailwind.config.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | !.storybook 2 | !.jest 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | !.storybook 2 | !.jest 3 | -------------------------------------------------------------------------------- /.jest/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/0.jpg -------------------------------------------------------------------------------- /public/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/1.jpg -------------------------------------------------------------------------------- /public/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/10.jpg -------------------------------------------------------------------------------- /public/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/11.jpg -------------------------------------------------------------------------------- /public/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/12.jpg -------------------------------------------------------------------------------- /public/avatars/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/13.jpg -------------------------------------------------------------------------------- /public/avatars/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/14.jpg -------------------------------------------------------------------------------- /public/avatars/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/15.jpg -------------------------------------------------------------------------------- /public/avatars/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/16.jpg -------------------------------------------------------------------------------- /public/avatars/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/17.jpg -------------------------------------------------------------------------------- /public/avatars/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/18.jpg -------------------------------------------------------------------------------- /public/avatars/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/19.jpg -------------------------------------------------------------------------------- /public/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/2.jpg -------------------------------------------------------------------------------- /public/avatars/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/20.jpg -------------------------------------------------------------------------------- /public/avatars/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/21.jpg -------------------------------------------------------------------------------- /public/avatars/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/22.jpg -------------------------------------------------------------------------------- /public/avatars/23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/23.jpg -------------------------------------------------------------------------------- /public/avatars/24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/24.jpg -------------------------------------------------------------------------------- /public/avatars/25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/25.jpg -------------------------------------------------------------------------------- /public/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/3.jpg -------------------------------------------------------------------------------- /public/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/4.jpg -------------------------------------------------------------------------------- /public/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/5.jpg -------------------------------------------------------------------------------- /public/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/6.jpg -------------------------------------------------------------------------------- /public/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/7.jpg -------------------------------------------------------------------------------- /public/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/8.jpg -------------------------------------------------------------------------------- /public/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/9.jpg -------------------------------------------------------------------------------- /public/avatars/17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/avatars/17.jpeg -------------------------------------------------------------------------------- /public/servers/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/servers/next.png -------------------------------------------------------------------------------- /faker.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@faker-js/faker' { 2 | import faker from 'faker' 3 | export default faker 4 | } 5 | -------------------------------------------------------------------------------- /public/servers/mirage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/servers/mirage.png -------------------------------------------------------------------------------- /public/servers/tailwind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/servers/tailwind.png -------------------------------------------------------------------------------- /public/ginto/ginto-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/ginto/ginto-semibold.woff -------------------------------------------------------------------------------- /public/whitney/whitney-book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/whitney/whitney-book.woff -------------------------------------------------------------------------------- /public/whitney/whitney-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/whitney/whitney-light.woff -------------------------------------------------------------------------------- /public/whitney/whitney-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/whitney/whitney-medium.woff -------------------------------------------------------------------------------- /public/whitney/whitney-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willianjusten/discord-tailwind/HEAD/public/whitney/whitney-semibold.woff -------------------------------------------------------------------------------- /generators/templates/Component.tsx.hbs: -------------------------------------------------------------------------------- 1 | const {{pascalCase name}} = () => ( 2 |

{{pascalCase name}}

3 | ) 4 | 5 | export default {{pascalCase name}} 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /generators/templates/stories.tsx.hbs: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import {{pascalCase name}} from '.' 3 | 4 | export default { 5 | title: '{{pascalCase name}}', 6 | component: {{pascalCase name}} 7 | } as Meta 8 | 9 | export const Default: Story = () => <{{pascalCase name}} /> 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": true 6 | }, 7 | "emmet.triggerExpansionOnTab": true, 8 | "emmet.includeLanguages": { 9 | "javascript": "javascriptreact" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ServerHeader/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import ServerHeader from '.' 4 | 5 | describe('', () => { 6 | it('should render correctly', () => { 7 | render() 8 | 9 | expect(screen.getByRole('button', { name: /NextJS/i })).toBeInTheDocument() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/Message/index.tsx: -------------------------------------------------------------------------------- 1 | export type MessageProps = { 2 | message: { 3 | text: string 4 | } 5 | } 6 | 7 | function Message({ message }: MessageProps) { 8 | return ( 9 |
10 |

{message.text}

11 |
12 | ) 13 | } 14 | 15 | export default Message 16 | -------------------------------------------------------------------------------- /src/components/ServerHeader/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import ServerHeader, { ServerHeaderProps } from '.' 3 | 4 | export default { 5 | title: 'ServerHeader', 6 | component: ServerHeader 7 | } as Meta 8 | 9 | export const Default: Story = (args) => ( 10 | 11 | ) 12 | 13 | Default.args = { 14 | name: 'NextJS' 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Message/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import Message from '.' 4 | 5 | describe('', () => { 6 | it('should render the heading', () => { 7 | const message = { 8 | text: 'lorem lorem' 9 | } 10 | render() 11 | 12 | expect(screen.getByText(message.text)).toBeInTheDocument() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | // this is only needed until they fix faker-js error 5 | typescript: { 6 | ignoreBuildErrors: true 7 | }, 8 | async redirects() { 9 | return [ 10 | { 11 | source: '/', 12 | destination: '/servers/1/channels/1', 13 | permanent: true 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ChannelList/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import ChannelList, { ChannelListProps } from '.' 3 | 4 | import { data } from 'data' 5 | 6 | export default { 7 | title: 'ChannelList', 8 | component: ChannelList 9 | } as Meta 10 | 11 | export const Default: Story = (args) => ( 12 | 13 | ) 14 | 15 | Default.args = { 16 | server: data[0] 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next", 4 | "next/core-web-vitals", 5 | "prettier", 6 | "plugin:storybook/recommended", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "ignorePatterns": ["*.config.js"], 10 | "rules": { 11 | "import/order": [ 12 | "error", 13 | { 14 | "alphabetize": { 15 | "order": "asc" 16 | } 17 | } 18 | ], 19 | "tailwindcss/no-custom-classname": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /generators/templates/test.tsx.hbs: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import {{pascalCase name}} from '.' 4 | 5 | describe('<{{pascalCase name}} />', () => { 6 | it('should render the heading', () => { 7 | const { container } = render(<{{pascalCase name}} />) 8 | 9 | expect(screen.getByRole('heading', { name: /{{pascalCase name}}/i })).toBeInTheDocument() 10 | 11 | expect(container.firstChild).toMatchSnapshot() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/components/Icons/test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | 3 | import * as Icons from '.' 4 | 5 | describe('', () => { 6 | const iconsIndex = Object.keys(Icons) 7 | 8 | iconsIndex.map((icon) => { 9 | it(`should render the ${icon} Icon correctly`, () => { 10 | const { container } = render(<>{Icons[icon as keyof typeof Icons]({})}) 11 | 12 | expect(container.querySelector('svg')).toBeInTheDocument() 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/ChannelLink/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import ChannelLink, { ChannelLinkProps } from '.' 3 | 4 | export default { 5 | title: 'ChannelLink', 6 | component: ChannelLink 7 | } as Meta 8 | 9 | export const Default: Story = (args) => ( 10 | 11 | ) 12 | 13 | Default.args = { 14 | serverId: 1, 15 | channel: { 16 | id: 1, 17 | label: 'welcome', 18 | icon: 'Book' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | testPathIgnorePatterns: ['/node_modules/', '/.next/'], 4 | collectCoverage: true, 5 | collectCoverageFrom: [ 6 | 'src/**/*.ts(x)?', 7 | '!src/**/stories.tsx', 8 | '!src/pages/**/*.ts(x)' 9 | ], 10 | setupFilesAfterEnv: ['/.jest/setup.ts'], 11 | modulePaths: ['/src/'], 12 | transform: { 13 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ChannelTopbar/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import ChannelTopbar, { ChannelTopbarProps } from '.' 3 | 4 | export default { 5 | title: 'ChannelTopbar', 6 | component: ChannelTopbar 7 | } as Meta 8 | 9 | export const Default: Story = (args) => ( 10 | 11 | ) 12 | 13 | Default.args = { 14 | channel: { 15 | label: 'welcome', 16 | description: 'Introduction to the Tailwind CSS framework and community.' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/MessageWithUser/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import MessageWithUser, { MessageWithUserProps } from '.' 3 | 4 | export default { 5 | title: 'MessageWithUser', 6 | component: MessageWithUser 7 | } as Meta 8 | 9 | export const Default: Story = (args) => ( 10 | 11 | ) 12 | 13 | Default.args = { 14 | message: { 15 | text: 'Lorem Lorem', 16 | avatarUrl: '/avatars/0.jpg', 17 | user: 'John Doe', 18 | date: '09/24/2021' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /src/components/Message/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import Message, { MessageProps } from '.' 3 | 4 | export default { 5 | title: 'Message', 6 | component: Message 7 | } as Meta 8 | 9 | export const Default: Story = (args) => 10 | 11 | Default.args = { 12 | message: { 13 | text: 'Lorem ipsum dolor, sit amet consectetur adipisicing elit. Minus quo in modi corporis vero culpa voluptatem molestias, magni ratione debitis sunt, eligendi perferendis, aperiam nam quia hic necessitatibus aliquam quidem.' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ChannelTopbar/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import ChannelTopbar from '.' 4 | 5 | describe('', () => { 6 | it('should render correctly', () => { 7 | const channel = { 8 | label: 'welcome', 9 | description: 'Introduction to the Tailwind CSS framework and community.' 10 | } 11 | render() 12 | 13 | expect(screen.getByText(channel.label)).toBeInTheDocument() 14 | expect(screen.getByText(channel.description)).toBeInTheDocument() 15 | expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/ServerHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import { Check, Chevron, Verified } from 'components/Icons' 2 | 3 | export type ServerHeaderProps = { 4 | name: string 5 | } 6 | 7 | function ServerHeader({ name }: ServerHeaderProps) { 8 | return ( 9 | 17 | ) 18 | } 19 | 20 | export default ServerHeader 21 | -------------------------------------------------------------------------------- /src/components/NavLink/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import Image from 'next/image' 3 | import NavLink, { NavLinkProps } from '.' 4 | 5 | export default { 6 | title: 'NavLink', 7 | component: NavLink, 8 | args: { 9 | href: '#', 10 | children: 'W' 11 | } 12 | } as Meta 13 | 14 | export const Default: Story = (args) => 15 | 16 | export const WithImage: Story = (args) => ( 17 | 18 | tailwind 19 | 20 | ) 21 | 22 | export const Active: Story = (args) => 23 | 24 | Active.args = { 25 | active: true 26 | } 27 | -------------------------------------------------------------------------------- /src/components/MessageWithUser/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import MessageWithUser from '.' 4 | 5 | describe('', () => { 6 | it('should render correctly', () => { 7 | const message = { 8 | text: 'Lorem Lorem', 9 | avatarUrl: '/avatars/0.jpg', 10 | user: 'John Doe', 11 | date: '09/24/2021' 12 | } 13 | render() 14 | 15 | expect(screen.getByRole('img', { name: message.user })).toBeInTheDocument() 16 | expect(screen.getByText(message.user)).toBeInTheDocument() 17 | expect(screen.getByText(message.date)).toBeInTheDocument() 18 | expect(screen.getByText(message.text)).toBeInTheDocument() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/styles/globals.css' 2 | import { RouterContext } from 'next/dist/shared/lib/router-context' 3 | import * as NextImage from 'next/image' 4 | 5 | const OriginalNextImage = NextImage.default 6 | 7 | Object.defineProperty(NextImage, 'default', { 8 | configurable: true, 9 | value: (props) => 10 | }) 11 | 12 | export const parameters = { 13 | backgrounds: { 14 | default: 'dark' 15 | }, 16 | nextRouter: { 17 | Provider: RouterContext.Provider 18 | }, 19 | actions: { argTypesRegex: '^on[A-Z].*' }, 20 | controls: { 21 | matchers: { 22 | color: /(background|color)$/i, 23 | date: /Date$/ 24 | } 25 | }, 26 | previewTabs: { 27 | 'storybook/docs/panel': { index: -1 } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Icons/stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story, Meta } from '@storybook/react' 2 | import * as Icons from '.' 3 | 4 | export default { 5 | title: 'Icons', 6 | args: { 7 | className: 'w-12', 8 | color: 'black' 9 | } 10 | } as Meta 11 | 12 | export const AllIcons: Story = (args) => { 13 | const icons = Object.keys(Icons) 14 | 15 | return ( 16 |
17 | {icons.map((iconIndex, i) => ( 18 |
22 | {Icons[iconIndex as keyof typeof Icons]({ 23 | ...args 24 | })} 25 | {`<${iconIndex}/>`} 26 |
27 | ))} 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | staticDirs: ['../public'], 3 | stories: ['../src/components/**/stories.tsx'], 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | 'storybook-addon-next-router', 8 | { 9 | /** 10 | * NOTE: fix Storybook issue with PostCSS@8 11 | * @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085 12 | */ 13 | name: '@storybook/addon-postcss', 14 | options: { 15 | postcssLoaderOptions: { 16 | implementation: require('postcss') 17 | } 18 | } 19 | } 20 | ], 21 | framework: '@storybook/react', 22 | core: { 23 | builder: 'webpack5' 24 | }, 25 | webpackFinal: (config) => { 26 | config.resolve.modules.push(`${process.cwd()}/src`) 27 | return config 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /generators/plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = (plop) => { 2 | plop.setGenerator('component', { 3 | description: 'Create a component', 4 | prompts: [ 5 | { 6 | type: 'input', 7 | name: 'name', 8 | message: 'What is your component name?' 9 | } 10 | ], 11 | actions: [ 12 | { 13 | type: 'add', 14 | path: '../src/components/{{pascalCase name}}/index.tsx', 15 | templateFile: 'templates/Component.tsx.hbs' 16 | }, 17 | { 18 | type: 'add', 19 | path: '../src/components/{{pascalCase name}}/stories.tsx', 20 | templateFile: 'templates/stories.tsx.hbs' 21 | }, 22 | { 23 | type: 'add', 24 | path: '../src/components/{{pascalCase name}}/test.tsx', 25 | templateFile: 'templates/test.tsx.hbs' 26 | } 27 | ] 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 14 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default MyDocument 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout Repository 9 | uses: actions/checkout@v2 10 | 11 | - name: Setup Node 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 14.x 15 | 16 | - uses: actions/cache@v2 17 | id: yarn-cache 18 | with: 19 | path: | 20 | ~/cache 21 | !~/cache/exclude 22 | **/node_modules 23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-yarn- 26 | - name: Install dependencies 27 | run: yarn install 28 | 29 | - name: Linting 30 | run: yarn lint 31 | 32 | - name: Test 33 | run: yarn test:ci 34 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Whitney"; 7 | src: url("/whitney/whitney-light.woff") format("woff"); 8 | font-weight: 400; 9 | font-style: normal; 10 | } 11 | 12 | @font-face { 13 | font-family: "Whitney"; 14 | src: url("/whitney/whitney-book.woff") format("woff"); 15 | font-weight: 500; 16 | font-style: normal; 17 | } 18 | 19 | @font-face { 20 | font-family: "Whitney"; 21 | src: url("/whitney/whitney-medium.woff") format("woff"); 22 | font-weight: 600; 23 | font-style: normal; 24 | } 25 | 26 | @font-face { 27 | font-family: "Whitney"; 28 | src: url("/whitney/whitney-semibold.woff") format("woff"); 29 | font-weight: 700; 30 | font-style: normal; 31 | } 32 | 33 | @font-face { 34 | font-family: "Ginto"; 35 | src: url("/ginto/ginto-semibold.woff") format("woff"); 36 | font-weight: 600; 37 | font-style: normal; 38 | } 39 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/NavLink/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import NavLink from '.' 4 | 5 | const useRouter = jest.spyOn(require('next/router'), 'useRouter') 6 | 7 | let asPath = '' 8 | useRouter.mockImplementation(() => ({ 9 | asPath 10 | })) 11 | 12 | describe('', () => { 13 | it('should render correctly with children and link', () => { 14 | render(Discord) 15 | 16 | expect(screen.getByText(/discord/i)).toBeInTheDocument() 17 | expect(screen.getByRole('link')).toHaveAttribute('href', '/some-link') 18 | }) 19 | 20 | it('should render with active styles if the prop is passed', () => { 21 | render( 22 | 23 | Discord 24 | 25 | ) 26 | 27 | expect(screen.getByText(/discord/i)).toHaveClass( 28 | 'rounded-2xl bg-brand text-white' 29 | ) 30 | }) 31 | 32 | it('should render with active styles if the route is active', () => { 33 | asPath = '/some-link' 34 | render(Discord) 35 | 36 | expect(screen.getByText(/discord/i)).toHaveClass( 37 | 'rounded-2xl bg-brand text-white' 38 | ) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require("tailwindcss/defaultTheme"); 2 | 3 | module.exports = { 4 | mode: 'jit', 5 | content: [ 6 | './src/pages/**/*.{js,ts,jsx,tsx}', 7 | './src/components/**/*.{js,ts,jsx,tsx}' 8 | ], 9 | theme: { 10 | boxShadow: { 11 | sm: "0 1px 0 rgba(4,4,5,0.2),0 1.5px 0 rgba(6,6,7,0.05),0 2px 0 rgba(4,4,5,0.05)", 12 | md: "0 4px 4px rgba(0,0,0,0.16)", 13 | lg: "0 8px 16px rgba(0,0,0,0.24)", 14 | }, 15 | extend: { 16 | fontFamily: { 17 | sans: ["Whitney", "Open Sans", ...defaultTheme.fontFamily.sans], 18 | title: ["Ginto", "Open Sans", ...defaultTheme.fontFamily.sans], 19 | }, 20 | colors: { 21 | brand: "#5965F2", 22 | gray: { 23 | 50: "#ECEDEE", 24 | 100: "#DCDDDE", 25 | 200: "#B9BBBE", 26 | 300: "#8E9297", 27 | 400: "#72767D", 28 | 500: "#5C6067", 29 | 550: "#4f545c", 30 | 600: "#464950", 31 | 700: "#36393F", 32 | 800: "#2F3136", 33 | 900: "#202225", 34 | 950: "#040405", 35 | }, 36 | }, 37 | }, 38 | }, 39 | variants: { 40 | extend: {}, 41 | }, 42 | plugins: [require("@tailwindcss/forms"), require('tailwind-scrollbar-hide')], 43 | } 44 | -------------------------------------------------------------------------------- /src/components/MessageWithUser/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export type MessageWithUserProps = { 4 | message: { 5 | text: string 6 | avatarUrl: string 7 | user: string 8 | date: string 9 | } 10 | } 11 | 12 | function MessageWithUser({ message }: MessageWithUserProps) { 13 | return ( 14 |
15 |
16 | {message.user} 26 |
27 |
28 |

29 | 30 | {message.user} 31 | 32 | 33 | {message.date} 34 | 35 |

36 |

{message.text}

37 |
38 |
39 | ) 40 | } 41 | 42 | export default MessageWithUser 43 | -------------------------------------------------------------------------------- /src/components/NavLink/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { useRouter } from 'next/router' 3 | 4 | export type NavLinkProps = { 5 | href: string 6 | active?: boolean 7 | children?: React.ReactElement | string 8 | } 9 | 10 | function NavLink({ href, active, children }: NavLinkProps) { 11 | let router = useRouter() 12 | active ||= router.asPath === href 13 | 14 | return ( 15 | 16 | 17 |
18 |
25 |
26 | 27 |
28 |
35 | {children} 36 |
37 |
38 |
39 | 40 | ) 41 | } 42 | 43 | export default NavLink 44 | -------------------------------------------------------------------------------- /src/pages/servers/[sid]/channels/[cid].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | 3 | import ChannelList from 'components/ChannelList' 4 | import ChannelTopbar from 'components/ChannelTopbar' 5 | import Message from 'components/Message' 6 | import MessageWithUser from 'components/MessageWithUser' 7 | import ServerHeader from 'components/ServerHeader' 8 | 9 | import { data } from 'data' 10 | 11 | export default function Server() { 12 | let router = useRouter() 13 | let server = data.find((server) => +server.id === +router.query.sid!)! 14 | 15 | let channel = server.categories 16 | .map((c) => c.channels) 17 | .flat() 18 | .find((channel) => +channel.id === +router.query.cid!) 19 | 20 | return ( 21 | <> 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | {channel?.messages.map((message, i) => ( 32 |
33 | {i === 0 || message.user !== channel?.messages[i - 1].user ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 |
39 | ))} 40 |
41 |
42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ChannelList/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | 4 | import ChannelList from '.' 5 | 6 | import { data } from 'data' 7 | 8 | const useRouter = jest.spyOn(require('next/router'), 'useRouter') 9 | let cid = 1 10 | 11 | useRouter.mockImplementation(() => ({ 12 | query: { 13 | cid 14 | } 15 | })) 16 | 17 | describe('', () => { 18 | it('should render the list', () => { 19 | render() 20 | 21 | // headers 22 | expect(screen.getByText(/tailwind css/i)).toBeInTheDocument() 23 | expect(screen.getByText(/tailwind labs/i)).toBeInTheDocument() 24 | expect(screen.getByText(/off topic/i)).toBeInTheDocument() 25 | expect(screen.getByText(/community/i)).toBeInTheDocument() 26 | 27 | // channel link 28 | expect(screen.getByText(/general/i)).toBeInTheDocument() 29 | }) 30 | 31 | it('should collapse read channels when clicking in the header', () => { 32 | render() 33 | 34 | // started showing 35 | expect(screen.getByText(/internals/i)).toBeInTheDocument() 36 | 37 | userEvent.click(screen.getByText(/tailwind css/i)) 38 | 39 | // it should be collapsed 40 | expect(screen.queryByText(/internals/i)).not.toBeInTheDocument() 41 | 42 | userEvent.click(screen.getByText(/tailwind css/i)) 43 | 44 | // it should show again 45 | expect(screen.getByText(/internals/i)).toBeInTheDocument() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/components/ChannelLink/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { useRouter } from 'next/router' 3 | import * as Icons from 'components/Icons' 4 | 5 | export type ChannelLinkProps = { 6 | serverId: number 7 | channel: { 8 | id: number 9 | icon?: string 10 | label: string 11 | unread?: boolean 12 | } 13 | } 14 | 15 | function ChannelLink({ serverId, channel }: ChannelLinkProps) { 16 | let Icon = channel.icon 17 | ? Icons[channel.icon as keyof typeof Icons] 18 | : Icons.Hashtag 19 | let router = useRouter() 20 | let active = +channel.id === +router.query.cid! 21 | 22 | let state: keyof typeof classes = active 23 | ? 'active' 24 | : channel.unread 25 | ? 'inactiveUnread' 26 | : 'inactiveRead' 27 | 28 | let classes = { 29 | active: 'text-white bg-gray-550/[0.32]', 30 | inactiveUnread: 31 | 'text-white hover:bg-gray-550/[0.16] active:bg-gray-550/[0.24]', 32 | inactiveRead: 33 | 'text-gray-300 hover:text-gray-100 hover:bg-gray-550/[0.16] active:bg-gray-550/[0.24]' 34 | } 35 | 36 | return ( 37 | 38 | 41 | {state === 'inactiveUnread' && ( 42 |
43 | )} 44 | 45 | {channel.label} 46 | 47 |
48 | 49 | ) 50 | } 51 | 52 | export default ChannelLink 53 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import Head from 'next/head' 3 | 4 | import Image from 'next/image' 5 | import { useRouter } from 'next/router' 6 | import { useEffect, useState } from 'react' 7 | 8 | import { Discord } from 'components/Icons' 9 | import NavLink from 'components/NavLink' 10 | 11 | import { data } from 'data' 12 | 13 | import 'styles/globals.css' 14 | 15 | function MyApp({ Component, pageProps }: AppProps) { 16 | let router = useRouter() 17 | let [isFirstRender, setIsFirstRender] = useState(true) 18 | 19 | useEffect(() => { 20 | setIsFirstRender(false) 21 | }, []) 22 | if (!router.isReady || isFirstRender) { 23 | return null 24 | } 25 | 26 | return ( 27 | <> 28 | 29 | Discord Clone 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 | 39 |
40 | 41 | {data.map((server) => ( 42 | 47 | {server.label} 55 | 56 | ))} 57 |
58 | 59 | 60 |
61 | 62 | ) 63 | } 64 | 65 | export default MyApp 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-tw", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "test": "jest --maxWorkers=50%", 10 | "test:watch": "jest --watch --maxWorkers=25%", 11 | "test:ci": "jest --runInBand", 12 | "generate": "yarn plop --plopfile generators/plopfile.js", 13 | "storybook": "start-storybook -p 6006", 14 | "build-storybook": "build-storybook" 15 | }, 16 | "dependencies": { 17 | "@faker-js/faker": "^6.0.0-alpha.3", 18 | "@tailwindcss/forms": "^0.4.0", 19 | "autoprefixer": "^10.4.2", 20 | "cssnano": "^5.0.15", 21 | "date-fns": "^2.28.0", 22 | "next": "12.0.7", 23 | "postcss": "^8.4.5", 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2", 26 | "tailwind-scrollbar-hide": "^1.1.7" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.16.7", 30 | "@storybook/addon-actions": "^6.4.12", 31 | "@storybook/addon-essentials": "^6.4.12", 32 | "@storybook/addon-links": "^6.4.12", 33 | "@storybook/addon-postcss": "^2.0.0", 34 | "@storybook/builder-webpack5": "^6.4.12", 35 | "@storybook/manager-webpack5": "^6.4.12", 36 | "@storybook/react": "^6.4.12", 37 | "@testing-library/jest-dom": "^5.16.1", 38 | "@testing-library/react": "^12.1.2", 39 | "@testing-library/user-event": "^13.5.0", 40 | "@types/faker": "^5.5.9", 41 | "@types/jest": "^27.4.0", 42 | "@types/node": "^17.0.8", 43 | "@types/react": "^17.0.38", 44 | "babel-loader": "^8.2.3", 45 | "eslint": "8.6.0", 46 | "eslint-config-next": "12.0.7", 47 | "eslint-config-prettier": "^8.3.0", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-storybook": "^0.5.5", 50 | "eslint-plugin-tailwindcss": "^3.1.2", 51 | "jest": "^27.4.7", 52 | "plop": "^3.0.5", 53 | "prettier": "2.5.1", 54 | "storybook-addon-next-router": "^3.1.1", 55 | "tailwindcss": "^3.0.12", 56 | "typescript": "4.5.4" 57 | }, 58 | "resolutions": { 59 | "webpack": "^5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ChannelList/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import ChannelLink from 'components/ChannelLink' 3 | import { Arrow } from 'components/Icons' 4 | import { ServerData } from 'data' 5 | 6 | export type ChannelListProps = { 7 | server: ServerData 8 | } 9 | 10 | function ChannelList({ server }: ChannelListProps) { 11 | let [closedCategories, setClosedCategories] = useState>([]) 12 | 13 | function toggleCategory(categoryId: number) { 14 | setClosedCategories((closedCategories) => 15 | closedCategories.includes(categoryId) 16 | ? closedCategories.filter((id) => id !== categoryId) 17 | : [...closedCategories, categoryId] 18 | ) 19 | } 20 | 21 | return ( 22 |
23 | {server.categories.map((category) => ( 24 |
25 | {category.label && ( 26 | 37 | )} 38 | 39 |
40 | {category.channels 41 | .filter((channel) => { 42 | let categoryIsOpen = !closedCategories.includes(category.id) 43 | 44 | return categoryIsOpen || channel.unread 45 | }) 46 | .map((channel) => ( 47 | 52 | ))} 53 |
54 |
55 | ))} 56 |
57 | ) 58 | } 59 | 60 | export default ChannelList 61 | -------------------------------------------------------------------------------- /src/components/ChannelLink/test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | 3 | import ChannelLink from '.' 4 | 5 | const useRouter = jest.spyOn(require('next/router'), 'useRouter') 6 | let cid = 3 7 | 8 | useRouter.mockImplementation(() => ({ 9 | query: { 10 | cid 11 | } 12 | })) 13 | 14 | describe('', () => { 15 | it('should render the link as inactive and unread', () => { 16 | const channel = { 17 | id: 1, 18 | label: 'welcome', 19 | icon: 'Book', 20 | unread: true 21 | } 22 | const serverId = 1 23 | 24 | const { container } = render( 25 | 26 | ) 27 | 28 | expect(screen.getByText(/welcome/i)).toBeInTheDocument() 29 | expect(container.querySelector('svg')).toBeInTheDocument() 30 | expect(screen.getByRole('link')).toHaveAttribute( 31 | 'href', 32 | `/servers/${serverId}/channels/${channel.id}` 33 | ) 34 | expect(screen.getByRole('link', { name: /welcome/i })).toHaveClass( 35 | 'text-white hover:bg-gray-550/[0.16]' 36 | ) 37 | }) 38 | 39 | it('should render with hashtag icon if icon is not passed', () => { 40 | const channel = { 41 | id: 1, 42 | label: 'welcome' 43 | } 44 | 45 | render() 46 | 47 | expect(screen.getByTitle(/hashtag/i)).toBeInTheDocument() 48 | }) 49 | 50 | it('should render as active if the route is the same as the channel', () => { 51 | cid = 1 52 | 53 | const channel = { 54 | id: 1, 55 | label: 'welcome', 56 | icon: 'Book' 57 | } 58 | 59 | render() 60 | 61 | expect(screen.getByRole('link', { name: /welcome/i })).toHaveClass( 62 | 'text-white bg-gray-550/[0.32]' 63 | ) 64 | }) 65 | 66 | it('should render as inactive and read', () => { 67 | cid = 1235 68 | const channel = { 69 | id: 1, 70 | label: 'welcome' 71 | } 72 | 73 | render() 74 | 75 | expect(screen.getByRole('link', { name: /welcome/i })).toHaveClass( 76 | 'text-gray-300 hover:text-gray-100 ' 77 | ) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Discord Tailwind 2 | 3 | > Just a Discord clone made with TailwindCSS. Initial idea from [egghead course](https://egghead.io/courses/craft-scalable-custom-made-interfaces-with-tailwind-css-8dfee898) with some tweaks to have tests and better component separation. 4 | 5 | ## What is inside? 6 | 7 | This project uses lot of stuff as: 8 | 9 | - [TypeScript](https://www.typescriptlang.org/) 10 | - [NextJS](https://nextjs.org/) 11 | - [TailwindCSS](https://tailwindcss.com/) 12 | - [Jest](https://jestjs.io/) 13 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) 14 | - [Storybook](https://storybook.js.org/) 15 | - [Eslint](https://eslint.org/) 16 | - [Prettier](https://prettier.io/) 17 | 18 | ## Getting Started 19 | 20 | First, run the development server: 21 | 22 | ```bash 23 | npm run dev 24 | # or 25 | yarn dev 26 | ``` 27 | 28 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 29 | 30 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 31 | 32 | ## Commands 33 | 34 | - `dev`: runs your application on `localhost:3000` 35 | - `build`: creates the production build version 36 | - `start`: starts a simple server with the build production code 37 | - `lint`: runs the linter in all components and pages 38 | - `test`: runs jest to test all components and pages 39 | - `test:watch`: runs jest in watch mode 40 | - `generate ComponentName`: to generate a component structure 41 | - `storybook`: runs storybook on `localhost:6006` 42 | - `build-storybook`: create the build version of storybook 43 | 44 | ## Learn More 45 | 46 | To learn more about Next.js, take a look at the following resources: 47 | 48 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 49 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 50 | 51 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 52 | 53 | ## Deploy on Vercel 54 | 55 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 56 | 57 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 58 | -------------------------------------------------------------------------------- /src/components/ChannelTopbar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Icons from 'components/Icons' 2 | 3 | export type ChannelTopbarProps = { 4 | channel?: { 5 | label?: string 6 | description?: string 7 | } 8 | } 9 | 10 | function ChannelTopbar({ channel }: ChannelTopbarProps) { 11 | return ( 12 |
13 |
14 | 15 | 16 | {channel?.label} 17 | 18 |
19 | 20 | {channel?.description && ( 21 | <> 22 |
23 |
24 | {channel?.description} 25 |
26 | 27 | )} 28 | 29 | {/* Mobile buttons */} 30 |
31 | 34 | 37 |
38 | 39 | {/* Desktop buttons */} 40 |
41 | 44 | 47 | 50 | 53 |
54 | 59 |
60 | 61 |
62 |
63 | 66 | 69 |
70 |
71 | ) 72 | } 73 | 74 | export default ChannelTopbar 75 | -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | import faker from '@faker-js/faker' 2 | import { format } from 'date-fns' 3 | 4 | faker.seed(123) 5 | 6 | export type MessageData = { 7 | id: number 8 | user: string 9 | avatarUrl: string 10 | date: string 11 | text: string 12 | } 13 | 14 | export type ChannelData = { 15 | id: number 16 | label: string 17 | description?: string 18 | icon?: string 19 | messages: MessageData[] 20 | unread?: boolean 21 | } 22 | 23 | export type CategoriesData = { 24 | id: number 25 | label: string 26 | channels: ChannelData[] 27 | } 28 | 29 | export type ServerData = { 30 | id: number 31 | label: string 32 | img: string 33 | categories: CategoriesData[] 34 | } 35 | 36 | export const data = [ 37 | { 38 | id: 1, 39 | label: 'Tailwind CSS', 40 | img: 'tailwind.png', 41 | categories: [ 42 | { 43 | id: 1, 44 | label: '', 45 | channels: [ 46 | { 47 | id: 1, 48 | label: 'welcome', 49 | description: 50 | 'Introduction to the Tailwind CSS framework and community.', 51 | icon: 'Book', 52 | messages: getMessages() 53 | }, 54 | { 55 | id: 2, 56 | label: 'announcements', 57 | icon: 'Speakerphone', 58 | messages: getMessages() 59 | } 60 | ] 61 | }, 62 | { 63 | id: 2, 64 | label: 'Tailwind CSS', 65 | channels: [ 66 | { 67 | id: 3, 68 | label: 'general', 69 | description: 70 | 'General discussion of Tailwind CSS (please move off-topic discussion in the off-topic channels).', 71 | unread: true, 72 | messages: getMessages() 73 | }, 74 | { 75 | id: 4, 76 | label: 'plugins', 77 | description: 'Tailwind CSS plugins.', 78 | unread: true, 79 | messages: getMessages() 80 | }, 81 | { 82 | id: 5, 83 | label: 'help', 84 | description: 85 | 'Help with Tailwind CSS and build process integration.', 86 | unread: true, 87 | messages: getMessages() 88 | }, 89 | { 90 | id: 6, 91 | label: 'internals', 92 | description: 'Development of the Tailwind CSS framework itself.', 93 | messages: getMessages() 94 | } 95 | ] 96 | }, 97 | { 98 | id: 3, 99 | label: 'Tailwind Labs', 100 | channels: [ 101 | { 102 | id: 7, 103 | label: 'tailwind-ui', 104 | description: 'General discussion of Tailwind UI.', 105 | messages: getMessages() 106 | }, 107 | { 108 | id: 8, 109 | label: 'headless-ui', 110 | description: 'General discussion of Headless UI.', 111 | messages: getMessages() 112 | }, 113 | { 114 | id: 9, 115 | label: 'refactoring-ui', 116 | description: 'General discussion of Refactoring UI.', 117 | unread: true, 118 | messages: getMessages() 119 | }, 120 | { 121 | id: 10, 122 | label: 'heroicons', 123 | description: 'General discussion of Heroicons.', 124 | unread: true, 125 | messages: getMessages() 126 | } 127 | ] 128 | }, 129 | { 130 | id: 4, 131 | label: 'Off topic', 132 | channels: [ 133 | { 134 | id: 11, 135 | label: 'design', 136 | description: 'General discussion of web design.', 137 | messages: getMessages() 138 | }, 139 | { 140 | id: 12, 141 | label: 'development', 142 | description: 'General discussion of web development.', 143 | messages: getMessages() 144 | }, 145 | { 146 | id: 13, 147 | label: 'random', 148 | description: 'General discussion of everything else!', 149 | unread: true, 150 | messages: getMessages() 151 | } 152 | ] 153 | }, 154 | { 155 | id: 5, 156 | label: 'Community', 157 | channels: [ 158 | { 159 | id: 14, 160 | label: 'jobs', 161 | description: 162 | 'Job board. Please put [HIRING] or [FOR HIRE] at the beginning of your post.', 163 | messages: getMessages() 164 | }, 165 | { 166 | id: 15, 167 | label: 'showcase', 168 | description: 'Share your projects built with Tailwind CSS!', 169 | unread: true, 170 | messages: getMessages() 171 | }, 172 | { 173 | id: 16, 174 | label: 'bots', 175 | description: 'Bot spam containment.', 176 | messages: getMessages() 177 | } 178 | ] 179 | } 180 | ] 181 | }, 182 | { 183 | id: 2, 184 | label: 'Next.js', 185 | img: 'next.png', 186 | categories: [ 187 | { 188 | id: 6, 189 | label: '', 190 | channels: [ 191 | { 192 | id: 17, 193 | label: 'welcome', 194 | icon: 'Book', 195 | messages: getMessages() 196 | }, 197 | { 198 | id: 18, 199 | label: 'announcements', 200 | icon: 'Speakerphone', 201 | description: 202 | 'Announcements related to this Discord server and Next.js', 203 | messages: getMessages() 204 | }, 205 | { 206 | id: 19, 207 | label: 'introductions', 208 | unread: true, 209 | description: 210 | 'Welcome to the server! Feel free to introduce yourself', 211 | messages: getMessages() 212 | } 213 | ] 214 | }, 215 | { 216 | id: 7, 217 | label: 'Need-Help', 218 | channels: [ 219 | { 220 | id: 20, 221 | label: 'community-help', 222 | description: 223 | 'Members of the community can help each other here, but we recommend checking GitHub discussions first: ', 224 | messages: getMessages() 225 | } 226 | ] 227 | }, 228 | { 229 | id: 8, 230 | label: 'Community', 231 | channels: [ 232 | { 233 | id: 21, 234 | label: 'general', 235 | icon: 'HashtagWithSpeechBubble', 236 | description: 'Discussions about Next.js in general', 237 | messages: getMessages() 238 | }, 239 | { 240 | id: 22, 241 | label: 'off-topic', 242 | unread: true, 243 | description: 244 | 'Discussions about topics not related to Next.js or other channels', 245 | messages: getMessages() 246 | }, 247 | { 248 | id: 23, 249 | label: 'showcase', 250 | unread: true, 251 | messages: getMessages() 252 | }, 253 | { 254 | id: 24, 255 | label: 'jobs-board', 256 | description: 257 | 'Is your company looking for Next.js developers? Discuss here!', 258 | messages: getMessages() 259 | }, 260 | { 261 | id: 25, 262 | label: 'hire-me', 263 | unread: true, 264 | description: 'Are you a developer looking to work with Next.js?', 265 | messages: getMessages() 266 | }, 267 | { 268 | id: 26, 269 | label: 'makers', 270 | description: 271 | 'Share as you build in public. Welcoming all makers and indie hackers.', 272 | messages: getMessages() 273 | }, 274 | { 275 | id: 27, 276 | label: 'moderation-feedback', 277 | description: 278 | 'Discussion about this Discord server and moderation topics', 279 | messages: getMessages() 280 | } 281 | ] 282 | } 283 | ] 284 | }, 285 | { 286 | id: 3, 287 | label: 'Mirage JS', 288 | img: 'mirage.png', 289 | categories: [ 290 | { 291 | id: 9, 292 | label: 'Text Channels', 293 | channels: [ 294 | { id: 28, label: 'general', messages: getMessages() }, 295 | { id: 29, label: 'graphql', unread: true, messages: getMessages() }, 296 | { 297 | id: 30, 298 | label: 'typescript', 299 | unread: true, 300 | messages: getMessages() 301 | } 302 | ] 303 | } 304 | ] 305 | } 306 | ] 307 | 308 | export function getMessages() { 309 | return [...Array(faker.datatype.number({ min: 7, max: 25 }))] 310 | .map(() => { 311 | let user = faker.internet.userName() 312 | let avatarUrl = `/avatars/${faker.datatype.number({ 313 | min: 0, 314 | max: 25 315 | })}.jpg` 316 | 317 | return [...Array(faker.datatype.number({ min: 1, max: 4 }))].map(() => ({ 318 | id: faker.datatype.number(), 319 | user, 320 | avatarUrl, 321 | date: format(new Date(faker.date.past()), 'MM/dd/yyyy'), 322 | text: faker.lorem.sentences(3) 323 | })) 324 | }) 325 | .flat() 326 | } 327 | -------------------------------------------------------------------------------- /src/components/Icons/index.tsx: -------------------------------------------------------------------------------- 1 | export function Discord(props: React.HTMLAttributes) { 2 | return ( 3 | 9 | 13 | 14 | ) 15 | } 16 | 17 | export function Verified(props: React.HTMLAttributes) { 18 | return ( 19 | 25 | 30 | 31 | ) 32 | } 33 | 34 | export function Check(props: React.HTMLAttributes) { 35 | return ( 36 | 42 | 46 | 47 | ) 48 | } 49 | 50 | export function Chevron(props: React.HTMLAttributes) { 51 | return ( 52 | 53 | 57 | 58 | ) 59 | } 60 | 61 | export function Book(props: React.HTMLAttributes) { 62 | return ( 63 | 64 | 70 | 71 | ) 72 | } 73 | 74 | export function Speakerphone(props: React.HTMLAttributes) { 75 | return ( 76 | 77 | 81 | 82 | ) 83 | } 84 | 85 | export function Arrow(props: React.HTMLAttributes) { 86 | return ( 87 | 88 | 94 | 95 | ) 96 | } 97 | 98 | export function AddPerson(props: React.HTMLAttributes) { 99 | return ( 100 | 101 | 105 | 106 | ) 107 | } 108 | 109 | export function Hashtag(props: React.HTMLAttributes) { 110 | return ( 111 | 112 | Hashtag 113 | 119 | 120 | ) 121 | } 122 | 123 | export function HashtagWithSpeechBubble( 124 | props: React.HTMLAttributes 125 | ) { 126 | return ( 127 | 128 | 132 | 136 | 137 | ) 138 | } 139 | 140 | export function Bell(props: React.HTMLAttributes) { 141 | return ( 142 | 143 | 149 | 150 | ) 151 | } 152 | 153 | export function Pin(props: React.HTMLAttributes) { 154 | return ( 155 | 156 | 160 | 161 | ) 162 | } 163 | 164 | export function People(props: React.HTMLAttributes) { 165 | return ( 166 | 167 | 173 | 179 | 183 | 184 | ) 185 | } 186 | 187 | export function Inbox(props: React.HTMLAttributes) { 188 | return ( 189 | 190 | 194 | 195 | ) 196 | } 197 | 198 | export function QuestionCircle(props: React.HTMLAttributes) { 199 | return ( 200 | 201 | 205 | 206 | ) 207 | } 208 | 209 | export function Spyglass(props: React.HTMLAttributes) { 210 | return ( 211 | 212 | 216 | 217 | ) 218 | } 219 | --------------------------------------------------------------------------------