├── .dockerignore ├── .eslintrc.js ├── .github └── workflows │ └── fly.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .sample.env ├── .storybook ├── main.ts └── preview.ts ├── Dockerfile ├── README.md ├── fly.toml ├── jira_clone.code-workspace ├── manifest.json ├── package.json ├── playwright.config.ts ├── public ├── avatars │ ├── andy-davis-min.webp │ ├── andy-davis.webp │ ├── buzz-lightyear-min.webp │ ├── buzz-lightyear.webp │ ├── emperor-zurg-min.webp │ ├── emperor-zurg.webp │ ├── jessie-min.webp │ ├── jessie.webp │ ├── little-green-men-min.webp │ ├── little-green-men.webp │ ├── mr-potato-min.webp │ ├── mr-potato.webp │ ├── ms-potato-min.webp │ ├── ms-potato.webp │ ├── woody-min.webp │ └── woody.webp ├── favicon.ico ├── fonts │ ├── CircularStd-Black.woff │ ├── CircularStd-Black.woff2 │ ├── CircularStd-Bold.woff │ ├── CircularStd-Bold.woff2 │ ├── CircularStd-Book.woff │ ├── CircularStd-Book.woff2 │ ├── CircularStd-Medium.woff │ └── CircularStd-Medium.woff2 └── images │ ├── default-project.png │ ├── error-404.svg │ ├── error-500.svg │ ├── logo.png │ ├── projects │ ├── 1.svg │ ├── 10.svg │ ├── 11.svg │ ├── 12.svg │ ├── 13.svg │ ├── 14.svg │ ├── 15.svg │ ├── 16.svg │ ├── 17.svg │ ├── 18.svg │ ├── 19.svg │ ├── 2.svg │ ├── 20.svg │ ├── 21.svg │ ├── 22.svg │ ├── 23.svg │ ├── 24.svg │ ├── 25.svg │ ├── 3.svg │ ├── 4.svg │ ├── 5.svg │ ├── 6.svg │ ├── 7.svg │ ├── 8.svg │ └── 9.svg │ ├── readme │ ├── issue-panel.png │ ├── login-dark.png │ ├── login.png │ ├── project.png │ ├── projects-new.png │ └── projects.png │ └── theme │ ├── barbie.png │ ├── dark.png │ ├── lava.png │ ├── light.png │ ├── lime.png │ └── system.png ├── remix.config.js ├── remix.env.d.ts ├── robots.txt ├── src ├── app │ ├── components │ │ ├── alert-dialog │ │ │ ├── alert-dialog.stories.tsx │ │ │ ├── alert-dialog.tsx │ │ │ └── index.ts │ │ ├── button │ │ │ ├── button.stories.tsx │ │ │ ├── button.tsx │ │ │ └── index.ts │ │ ├── description │ │ │ ├── description.stories.tsx │ │ │ ├── description.tsx │ │ │ └── index.ts │ │ ├── dialog │ │ │ ├── dialog.stories.tsx │ │ │ ├── dialog.tsx │ │ │ └── index.ts │ │ ├── error-404 │ │ │ ├── error-404.stories.tsx │ │ │ ├── error-404.tsx │ │ │ └── index.ts │ │ ├── error-500 │ │ │ ├── error-500.stories.tsx │ │ │ ├── error-500.tsx │ │ │ └── index.ts │ │ ├── error-base.tsx │ │ ├── icons.tsx │ │ ├── kbd-placeholder │ │ │ ├── index.ts │ │ │ ├── kbd-placeholder.stories.tsx │ │ │ └── kbd-placeholder.tsx │ │ ├── priority-icon │ │ │ ├── index.ts │ │ │ ├── priority-icon.stories.tsx │ │ │ └── priority-icon.tsx │ │ ├── scroll-area │ │ │ ├── index.ts │ │ │ ├── scroll-area.stories.tsx │ │ │ └── scroll-area.tsx │ │ ├── select │ │ │ ├── index.ts │ │ │ ├── select.stories.tsx │ │ │ └── select.tsx │ │ ├── textarea-autosize.tsx │ │ ├── title │ │ │ ├── index.ts │ │ │ ├── title.stories.tsx │ │ │ └── title.tsx │ │ ├── toast │ │ │ ├── index.ts │ │ │ ├── toast.stories.tsx │ │ │ └── toast.tsx │ │ ├── tooltip │ │ │ ├── index.ts │ │ │ ├── tooltip.stories.tsx │ │ │ └── tooltip.tsx │ │ └── user-avatar │ │ │ ├── index.ts │ │ │ ├── user-avatar.stories.tsx │ │ │ └── user-avatar.tsx │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── events │ │ ├── events.ts │ │ └── index.ts │ ├── hooks │ │ └── useSortBy.tsx │ ├── root.tsx │ ├── routes │ │ ├── 404.tsx │ │ ├── __main.tsx │ │ ├── __main │ │ │ ├── projects.$projectId.tsx │ │ │ ├── projects.$projectId │ │ │ │ ├── $.tsx │ │ │ │ ├── analytics.tsx │ │ │ │ ├── board.tsx │ │ │ │ ├── board │ │ │ │ │ └── issue │ │ │ │ │ │ ├── $issueId.tsx │ │ │ │ │ │ ├── issue-event.ts │ │ │ │ │ │ └── new.tsx │ │ │ │ └── server-error.tsx │ │ │ ├── projects.tsx │ │ │ └── projects │ │ │ │ └── new.tsx │ │ ├── action │ │ │ ├── logout.tsx │ │ │ └── set-theme.tsx │ │ ├── index.tsx │ │ ├── login.spec.ts │ │ └── login.tsx │ ├── session-storage │ │ ├── index.ts │ │ ├── shared.ts │ │ ├── theme-storage.server.ts │ │ └── user-storage.server.ts │ ├── store │ │ ├── theme.store.tsx │ │ └── user.store.tsx │ ├── stories │ │ ├── Introduction.mdx │ │ ├── assets │ │ │ ├── code-brackets.svg │ │ │ ├── colors.svg │ │ │ ├── comments.svg │ │ │ ├── direction.svg │ │ │ ├── flow.svg │ │ │ ├── plugin.svg │ │ │ ├── repo.svg │ │ │ └── stackalt.svg │ │ ├── color-palettes.tsx │ │ └── utils.tsx │ ├── styles │ │ ├── app.css │ │ └── fonts.css │ └── ui │ │ ├── login │ │ ├── index.ts │ │ ├── login.view.stories.tsx │ │ └── login.view.tsx │ │ └── main │ │ ├── header │ │ ├── header.stories.tsx │ │ ├── header.tsx │ │ ├── index.ts │ │ ├── select-theme.tsx │ │ └── user-profile.tsx │ │ ├── index.ts │ │ ├── main.layout.tsx │ │ ├── project │ │ ├── analytics │ │ │ ├── analytics.view.tsx │ │ │ └── index.ts │ │ ├── board │ │ │ ├── avatar-list.tsx │ │ │ ├── board.view.tsx │ │ │ ├── category-column │ │ │ │ ├── category-column.tsx │ │ │ │ ├── index.ts │ │ │ │ └── issue-card │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── issue-card.stories.tsx │ │ │ │ │ └── issue-card.tsx │ │ │ ├── index.ts │ │ │ ├── issue-panel │ │ │ │ ├── comment │ │ │ │ │ ├── create-comment.tsx │ │ │ │ │ ├── edit-box.tsx │ │ │ │ │ └── view-comment.tsx │ │ │ │ ├── created-updated-at.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── issue-panel.view.tsx │ │ │ │ ├── panel-header-issue.tsx │ │ │ │ ├── select-asignee.tsx │ │ │ │ ├── select-priority.tsx │ │ │ │ ├── select-status.tsx │ │ │ │ └── spinner.tsx │ │ │ ├── search.tsx │ │ │ └── select-sort.tsx │ │ ├── index.ts │ │ ├── project.store.tsx │ │ ├── project.view.tsx │ │ └── sidebar │ │ │ ├── index.ts │ │ │ ├── sidebar.stories.tsx │ │ │ └── sidebar.tsx │ │ └── projects │ │ ├── create-project-panel │ │ ├── create-project-panel-header.tsx │ │ ├── create-project-panel.stories.tsx │ │ ├── create-project-panel.view.tsx │ │ └── index.ts │ │ ├── index.ts │ │ ├── project-card │ │ ├── index.ts │ │ ├── project-card.stories.tsx │ │ └── project-card.tsx │ │ └── projects.view.tsx ├── domain │ ├── category │ │ ├── category.mock.ts │ │ ├── category.ts │ │ └── index.ts │ ├── comment │ │ ├── comment.mock.ts │ │ ├── comment.ts │ │ └── index.ts │ ├── filter │ │ ├── filter.ts │ │ └── index.ts │ ├── issue │ │ ├── index.ts │ │ ├── issue.mock.ts │ │ └── issue.ts │ ├── priority │ │ ├── index.ts │ │ ├── priority.mock.ts │ │ └── priority.ts │ ├── project │ │ ├── index.ts │ │ ├── project.mock.ts │ │ └── project.ts │ └── user │ │ ├── index.ts │ │ ├── user.mock.ts │ │ └── user.ts ├── infrastructure │ └── db │ │ ├── comment.ts │ │ ├── db.server.ts │ │ ├── issue.ts │ │ ├── project.ts │ │ ├── schema.prisma │ │ ├── seed.ts │ │ └── user.ts └── utils │ ├── dnull.ts │ ├── formatDateTime.ts │ ├── index.ts │ ├── meta.ts │ ├── random-project-image.ts │ └── text-are-only-spaces.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /public/build 4 | 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('eslint').Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: [ 5 | "@remix-run/eslint-config", 6 | "@remix-run/eslint-config/node", 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:storybook/recommended", 11 | ], 12 | rules: { 13 | "react/react-in-jsx-scope": "off", 14 | "@typescript-eslint/consistent-type-imports": "off", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 8 | jobs: 9 | deploy: 10 | name: Deploy app 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: superfly/flyctl-actions/setup-flyctl@master 15 | - run: flyctl deploy --remote-only 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.cache 6 | /public/build 7 | build 8 | .env 9 | src/app/styles/app-compiled.css 10 | src/infrastructure/db/jira-clone.db 11 | src/infrastructure/db/sqlite.db 12 | src/infrastructure/db/sqlite.db-journal 13 | package-lock.json 14 | pnpm-lock.yaml 15 | /test-results/ 16 | /playwright-report/ 17 | /playwright/.cache/ 18 | /test-results/ 19 | /playwright-report/ 20 | /playwright/.cache/ 21 | src/infrastructure/db/db.sqlite 22 | storybook-static 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "useTabs": false, 7 | "printWidth": 100, 8 | "overrides": [ 9 | { 10 | "files": "*.tsx", 11 | "options": { 12 | "printWidth": 80 13 | } 14 | } 15 | ], 16 | "plugins": ["prettier-plugin-tailwindcss"], 17 | "tailwindFunctions": ["cx", "twix", "twMerge"] 18 | } 19 | -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | # This is a sample file. Crate a .env file with the following environmental variables: 2 | SESSION_SECRET= 3 | DATABASE_URL= # Path relative to the schema.prisma file. For a sqlite file, prepend with file: -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/react-vite"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 5 | staticDirs: ["../public/avatars", "../public/fonts", "../public/images"], 6 | addons: [ 7 | "@storybook/addon-links", 8 | "@storybook/addon-essentials", 9 | "@storybook/addon-interactions", 10 | { 11 | name: "@storybook/addon-styling", 12 | options: {}, 13 | }, 14 | ], 15 | framework: { 16 | name: "@storybook/react-vite", 17 | options: {}, 18 | }, 19 | docs: { 20 | autodocs: "tag", 21 | }, 22 | }; 23 | export default config; 24 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/react"; 2 | 3 | import { withThemeByClassName } from "@storybook/addon-styling"; 4 | 5 | /* TODO: update import to your tailwind styles file. If you're using Angular, inject this through your angular.json config instead */ 6 | import "../src/app/styles/app-compiled.css"; 7 | 8 | const preview: Preview = { 9 | parameters: { 10 | actions: { argTypesRegex: "^on[A-Z].*" }, 11 | controls: { 12 | matchers: { 13 | color: /(background|color)$/i, 14 | date: /Date$/, 15 | }, 16 | }, 17 | backgrounds: { 18 | default: "surface", 19 | values: [ 20 | { name: "surface", value: "var(--color-elevation-surface)" }, 21 | { name: "overlay", value: "var(--color-elevation-surface-overlay)" }, 22 | { name: "raised", value: "var(--color-elevation-surface-raised)" }, 23 | { name: "sunken", value: "var(--color-elevation-surface-sunken)" }, 24 | ], 25 | }, 26 | }, 27 | 28 | decorators: [ 29 | // Adds theme switching support. 30 | // NOTE: requires setting "darkMode" to "class" in your tailwind config 31 | // @ts-ignore 32 | withThemeByClassName({ 33 | themes: { 34 | light: "light", 35 | dark: "dark", 36 | lava: "lava", 37 | lime: "lime", 38 | }, 39 | defaultTheme: "light", 40 | }), 41 | ], 42 | }; 43 | 44 | export default preview; 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # base node image 2 | FROM node:16-bullseye-slim as base 3 | 4 | # Install openssl for Prisma 5 | RUN apt-get update && apt-get install -y openssl 6 | 7 | # Install all node_modules, including dev dependencies 8 | FROM base as deps 9 | 10 | RUN mkdir /app 11 | WORKDIR /app 12 | 13 | ADD package.json ./ 14 | RUN npm install --production=false 15 | 16 | # Setup production node_modules 17 | FROM base as production-deps 18 | 19 | RUN mkdir /app 20 | WORKDIR /app 21 | 22 | COPY --from=deps /app/node_modules /app/node_modules 23 | ADD package.json ./ 24 | RUN npm prune --production 25 | 26 | # Build the app 27 | FROM base as build 28 | 29 | ENV NODE_ENV=production 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | COPY --from=deps /app/node_modules /app/node_modules 35 | 36 | # If we're using Prisma, uncomment to cache the prisma schema 37 | ADD src/infrastructure/db . 38 | RUN npx prisma generate 39 | 40 | ADD . . 41 | RUN npm run build 42 | 43 | # Finally, build the production image with minimal footprint 44 | FROM base 45 | 46 | ENV NODE_ENV=production 47 | 48 | RUN mkdir /app 49 | WORKDIR /app 50 | 51 | COPY --from=production-deps /app/node_modules /app/node_modules 52 | 53 | # Uncomment if using Prisma 54 | COPY --from=build /app/node_modules/.prisma /app/node_modules/.prisma 55 | 56 | COPY --from=build /app/build /app/build 57 | COPY --from=build /app/public /app/public 58 | ADD . . 59 | EXPOSE 8080 60 | 61 | CMD ["npm", "run", "start:migrate"] 62 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for jira-clone on 2023-01-13T19:03:42+01:00 2 | 3 | app = "jira-clone" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [env] 9 | DATABASE_URL = "file:/data/jira_clone.db" 10 | PORT = "8080" 11 | 12 | [mounts] 13 | destination = "/data" 14 | source = "data" 15 | 16 | [[statics]] 17 | guest_path = "/app/public/images" 18 | url_prefix = "/static/images" 19 | 20 | [[services]] 21 | http_checks = [] 22 | internal_port = 8080 23 | processes = ["app"] 24 | protocol = "tcp" 25 | script_checks = [] 26 | tcp_checks = [] 27 | [services.concurrency] 28 | hard_limit = 25 29 | soft_limit = 20 30 | type = "connections" 31 | 32 | [[services.ports]] 33 | force_https = true 34 | handlers = ["http"] 35 | port = 80 36 | http_options = { response = { headers = { Expect-CT = false } } } 37 | 38 | [[services.ports]] 39 | handlers = ["tls", "http"] 40 | port = 443 41 | -------------------------------------------------------------------------------- /jira_clone.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "jira-clone" 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: "./src/app", 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env.CI, 18 | /* Retry on CI only */ 19 | retries: process.env.CI ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env.CI ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: "html", 24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 25 | use: { 26 | /* Base URL to use in actions like `await page.goto('/')`. */ 27 | baseURL: "http://127.0.0.1:3000", 28 | 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: "on-first-retry", 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: "chromium", 37 | use: { ...devices["Desktop Chrome"] }, 38 | }, 39 | 40 | { 41 | name: "firefox", 42 | use: { ...devices["Desktop Firefox"] }, 43 | }, 44 | 45 | { 46 | name: "webkit", 47 | use: { ...devices["Desktop Safari"] }, 48 | }, 49 | 50 | /* Test against mobile viewports. */ 51 | // { 52 | // name: 'Mobile Chrome', 53 | // use: { ...devices['Pixel 5'] }, 54 | // }, 55 | // { 56 | // name: 'Mobile Safari', 57 | // use: { ...devices['iPhone 12'] }, 58 | // }, 59 | 60 | /* Test against branded browsers. */ 61 | // { 62 | // name: 'Microsoft Edge', 63 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 64 | // }, 65 | // { 66 | // name: 'Google Chrome', 67 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 68 | // }, 69 | ], 70 | 71 | /* Run your local dev server before starting the tests */ 72 | // webServer: { 73 | // command: 'npm run start', 74 | // url: 'http://127.0.0.1:3000', 75 | // reuseExistingServer: !process.env.CI, 76 | // }, 77 | }); 78 | -------------------------------------------------------------------------------- /public/avatars/andy-davis-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/andy-davis-min.webp -------------------------------------------------------------------------------- /public/avatars/andy-davis.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/andy-davis.webp -------------------------------------------------------------------------------- /public/avatars/buzz-lightyear-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/buzz-lightyear-min.webp -------------------------------------------------------------------------------- /public/avatars/buzz-lightyear.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/buzz-lightyear.webp -------------------------------------------------------------------------------- /public/avatars/emperor-zurg-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/emperor-zurg-min.webp -------------------------------------------------------------------------------- /public/avatars/emperor-zurg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/emperor-zurg.webp -------------------------------------------------------------------------------- /public/avatars/jessie-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/jessie-min.webp -------------------------------------------------------------------------------- /public/avatars/jessie.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/jessie.webp -------------------------------------------------------------------------------- /public/avatars/little-green-men-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/little-green-men-min.webp -------------------------------------------------------------------------------- /public/avatars/little-green-men.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/little-green-men.webp -------------------------------------------------------------------------------- /public/avatars/mr-potato-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/mr-potato-min.webp -------------------------------------------------------------------------------- /public/avatars/mr-potato.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/mr-potato.webp -------------------------------------------------------------------------------- /public/avatars/ms-potato-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/ms-potato-min.webp -------------------------------------------------------------------------------- /public/avatars/ms-potato.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/ms-potato.webp -------------------------------------------------------------------------------- /public/avatars/woody-min.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/woody-min.webp -------------------------------------------------------------------------------- /public/avatars/woody.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/avatars/woody.webp -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/CircularStd-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Black.woff -------------------------------------------------------------------------------- /public/fonts/CircularStd-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Black.woff2 -------------------------------------------------------------------------------- /public/fonts/CircularStd-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Bold.woff -------------------------------------------------------------------------------- /public/fonts/CircularStd-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/CircularStd-Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Book.woff -------------------------------------------------------------------------------- /public/fonts/CircularStd-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Book.woff2 -------------------------------------------------------------------------------- /public/fonts/CircularStd-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Medium.woff -------------------------------------------------------------------------------- /public/fonts/CircularStd-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/fonts/CircularStd-Medium.woff2 -------------------------------------------------------------------------------- /public/images/default-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/default-project.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/logo.png -------------------------------------------------------------------------------- /public/images/projects/1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_parrot 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/images/projects/12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_website 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/images/projects/16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_power 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/images/projects/17.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_notes_flag 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/images/projects/18.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_refresh 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/images/projects/2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_cloud 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/images/projects/20.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_science 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/images/projects/22.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_spanner 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/images/projects/23.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_storm 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/images/projects/3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_disc 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/images/projects/4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_code 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/images/projects/5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_coffee 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/images/projects/6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | project_avatar_design 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/images/readme/issue-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/issue-panel.png -------------------------------------------------------------------------------- /public/images/readme/login-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/login-dark.png -------------------------------------------------------------------------------- /public/images/readme/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/login.png -------------------------------------------------------------------------------- /public/images/readme/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/project.png -------------------------------------------------------------------------------- /public/images/readme/projects-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/projects-new.png -------------------------------------------------------------------------------- /public/images/readme/projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/readme/projects.png -------------------------------------------------------------------------------- /public/images/theme/barbie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/barbie.png -------------------------------------------------------------------------------- /public/images/theme/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/dark.png -------------------------------------------------------------------------------- /public/images/theme/lava.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/lava.png -------------------------------------------------------------------------------- /public/images/theme/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/light.png -------------------------------------------------------------------------------- /public/images/theme/lime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/lime.png -------------------------------------------------------------------------------- /public/images/theme/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daniserrano7/jira-clone/07e1f2b0c893c788ff31e3740a36506aa91d88d1/public/images/theme/system.png -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | future: { 4 | v2_normalizeFormMethod: true, 5 | v2_meta: true, 6 | }, 7 | ignoredRouteFiles: ["**/.*", "**/*.spec.ts"], 8 | appDirectory: "./src/app", 9 | serverDependenciesToBundle: [ 10 | "react-dnd", 11 | "react-dnd-html5-backend", 12 | "react-dnd-touch-backend", 13 | "@react-dnd/invariant", 14 | "dnd-core", 15 | "@react-dnd/shallowequal", 16 | "@react-dnd/asap", 17 | ], 18 | assetsBuildDirectory: "public/build", 19 | publicPath: "/build/", 20 | }; 21 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/app/components/alert-dialog/alert-dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import * as AlertDialog from "./alert-dialog"; 3 | import { Button } from "../button"; 4 | 5 | const meta: Meta = { 6 | title: "Components/AlertDialog", 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | 11 | argTypes: {}, 12 | }; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | render: () => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Alert title 27 | 28 | This is the description of the alert dialog. Here you can add more 29 | information about the alert. 30 | 31 |
32 | Cancel 33 | Action 34 |
35 |
36 |
37 |
38 | ), 39 | }; 40 | -------------------------------------------------------------------------------- /src/app/components/alert-dialog/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as AlertDialog from "@radix-ui/react-alert-dialog"; 2 | import { twix } from "tailwindcss-radix-ui"; 3 | import { Button, Props as ButtonProps } from "../button"; 4 | 5 | export const Root = AlertDialog.Root; 6 | export const Trigger = AlertDialog.Trigger; 7 | export const Portal = AlertDialog.Portal; 8 | export const Overlay = twix( 9 | AlertDialog.Overlay, 10 | "fixed top-0 left-0 z-50 h-full w-full backdrop-blur-sm" 11 | ); 12 | export const Content = twix( 13 | AlertDialog.Content, 14 | "fixed top-1/2 text-font left-1/2 z-50 -translate-x-1/2 -translate-y-1/2 rounded bg-elevation-surface-overlay p-5 shadow-lg" 15 | ); 16 | export const Title = twix( 17 | AlertDialog.Title, 18 | "mb-5 font-primary-black text-3xl" 19 | ); 20 | export const Description = twix( 21 | AlertDialog.Description, 22 | "mt-8 flex w-full justify-end gap-4" 23 | ); 24 | export const Cancel = ({ children, ...rest }: ButtonProps): JSX.Element => ( 25 | 26 | 29 | 30 | ); 31 | export const Action = ({ children, ...rest }: ButtonProps): JSX.Element => ( 32 | 35 | ); 36 | -------------------------------------------------------------------------------- /src/app/components/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./alert-dialog"; 2 | -------------------------------------------------------------------------------- /src/app/components/button/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./button"; 2 | -------------------------------------------------------------------------------- /src/app/components/description/description.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { Description } from "./description"; 3 | 4 | const meta: Meta = { 5 | title: "Components/Description", 6 | parameters: { 7 | layout: "centered", 8 | }, 9 | argTypes: { 10 | initDescription: { 11 | defaultValue: "Description", 12 | control: { 13 | type: "text", 14 | }, 15 | }, 16 | readOnly: { 17 | defaultValue: false, 18 | control: { 19 | type: "boolean", 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | export const Default: Story = { 29 | render: (_) => ( 30 |
31 | {[DefaultDescription, InitDescription, ReadOnly].map( 32 | (DescriptionStory, index) => ( 33 | 34 | ) 35 | )} 36 |
37 | ), 38 | }; 39 | 40 | export const DefaultDescription: Story = {}; 41 | 42 | export const InitDescription: Story = { 43 | args: { 44 | initDescription: "Default description", 45 | }, 46 | }; 47 | 48 | export const ReadOnly: Story = { 49 | args: { 50 | initDescription: "Read only description", 51 | readOnly: true, 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/app/components/description/description.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { TextareaAutosize } from "@app/components/textarea-autosize"; 3 | 4 | export const Description = ({ 5 | initDescription = "", 6 | readOnly, 7 | }: DescriptionProps): JSX.Element => { 8 | const [description, setDescription] = useState(initDescription); 9 | 10 | const updateDescription = (newDescription: string) => { 11 | setDescription(newDescription); 12 | }; 13 | 14 | return ( 15 |
16 | 23 |
24 | ); 25 | }; 26 | 27 | interface DescriptionProps { 28 | initDescription?: string; 29 | readOnly?: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/description/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./description"; 2 | -------------------------------------------------------------------------------- /src/app/components/dialog/dialog.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import * as Dialog from "./dialog"; 3 | import { Button } from "../button"; 4 | 5 | const meta: Meta = { 6 | title: "Components/Dialog", 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | argTypes: {}, 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Default: Story = { 17 | render: () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | Alert title 26 | 27 | Here you can add the content of the dialog 28 | 29 |
30 | 31 | 32 | 35 | 36 |
37 |
38 |
39 |
40 |
41 | ), 42 | }; 43 | -------------------------------------------------------------------------------- /src/app/components/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as Dialog from "@radix-ui/react-dialog"; 2 | import cx from "classix"; 3 | import { twix } from "tailwindcss-radix-ui"; 4 | 5 | export const Root = Dialog.Root; 6 | export const Trigger = Dialog.Trigger; 7 | export const Portal = Dialog.Portal; 8 | export const Description = Dialog.Description; 9 | export const Close = Dialog.Close; 10 | 11 | export const Overlay = twix( 12 | Dialog.Overlay, 13 | cx( 14 | "absolute top-0 left-0 z-50 box-border grid h-full w-full place-items-center overflow-y-auto py-[40px] px-[40px]", 15 | "radix-state-open:animate-fade-in duration-300 backdrop-blur-md" 16 | ) 17 | ); 18 | export const Content = twix( 19 | Dialog.Content, 20 | cx( 21 | "relative z-50 text-font w-4/5 max-w-[1000px] rounded-md bg-elevation-surface py-6 px-8 shadow-lg", 22 | "duration-300 radix-state-open:animate-slide-up" 23 | ) 24 | ); 25 | 26 | export const Title = twix(Dialog.Title, "mb-5 font-primary-black text-3xl"); 27 | -------------------------------------------------------------------------------- /src/app/components/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dialog"; 2 | -------------------------------------------------------------------------------- /src/app/components/error-404/error-404.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Error404 } from "./error-404"; 4 | 5 | const meta: Meta = { 6 | title: "Components/Error404", 7 | component: Error404, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | argTypes: { 12 | message: { 13 | control: { 14 | type: "text", 15 | }, 16 | }, 17 | href: { 18 | control: { 19 | type: "text", 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | export const Default: Story = {}; 29 | 30 | export const Message: Story = { 31 | args: { 32 | message: "This is the error message", 33 | }, 34 | }; 35 | 36 | export const Link: Story = { 37 | args: { 38 | href: "/link-to-safe-place", 39 | }, 40 | }; 41 | 42 | export const MessageLink: Story = { 43 | args: { 44 | message: "This is the error message", 45 | href: "/link-to-safe-place", 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/components/error-404/error-404.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBase } from "../error-base"; 2 | 3 | export const Error404 = ({ message = "Error 404: Not Found", href }: Props) => { 4 | return ; 5 | }; 6 | 7 | interface Props { 8 | message: string; 9 | href: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/components/error-404/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error-404"; 2 | -------------------------------------------------------------------------------- /src/app/components/error-500/error-500.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Error500 } from "./error-500"; 4 | 5 | const meta: Meta = { 6 | title: "Components/Error500", 7 | component: Error500, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | argTypes: { 12 | message: { 13 | control: { 14 | type: "text", 15 | }, 16 | }, 17 | href: { 18 | control: { 19 | type: "text", 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | export default meta; 26 | type Story = StoryObj; 27 | 28 | export const Default: Story = {}; 29 | 30 | export const Message: Story = { 31 | args: { 32 | message: "This is the error message", 33 | }, 34 | }; 35 | 36 | export const Link: Story = { 37 | args: { 38 | href: "/link-to-safe-place", 39 | }, 40 | }; 41 | 42 | export const MessageLink: Story = { 43 | args: { 44 | message: "This is the error message", 45 | href: "/link-to-safe-place", 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/app/components/error-500/error-500.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBase } from "../error-base"; 2 | 3 | export const Error500 = ({ 4 | message = "Error 500: Server error", 5 | href, 6 | }: Props) => { 7 | return ; 8 | }; 9 | 10 | interface Props { 11 | message: string; 12 | href: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/components/error-500/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error-500"; 2 | -------------------------------------------------------------------------------- /src/app/components/error-base.tsx: -------------------------------------------------------------------------------- 1 | export const ErrorBase = ({ variant, message, href }: Props) => { 2 | const imgPath = `/images/error-${variant}.svg`; 3 | 4 | return ( 5 |
6 | Server error 11 | {href ? ( 12 | 16 | {message} 17 | 18 | ) : ( 19 | {message} 20 | )} 21 |
22 | ); 23 | }; 24 | 25 | interface Props { 26 | variant: "500" | "404"; 27 | message: string; 28 | href: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import { FaCheckSquare } from "react-icons/fa"; 2 | import cx from "classix"; 3 | 4 | export const TaskIcon = ({ 5 | size = 24, 6 | className = "", 7 | }: IconProps): JSX.Element => ( 8 | 9 | 14 | 15 | ); 16 | 17 | interface IconProps { 18 | size?: number; 19 | className?: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/kbd-placeholder/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./kbd-placeholder"; 2 | -------------------------------------------------------------------------------- /src/app/components/kbd-placeholder/kbd-placeholder.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | 3 | import { Kbd } from "./kbd-placeholder"; 4 | 5 | const meta: Meta = { 6 | title: "Components/Kbd", 7 | component: Kbd, 8 | parameters: { 9 | layout: "centered", 10 | }, 11 | argTypes: { 12 | children: { 13 | control: { 14 | type: "text", 15 | }, 16 | }, 17 | }, 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Default: Story = { 24 | args: { 25 | children: "Key", 26 | }, 27 | }; 28 | 29 | export const Sentence: Story = { 30 | render: (_) => ( 31 |

32 | Press Shift + S to save 33 |

34 | ), 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/components/kbd-placeholder/kbd-placeholder.tsx: -------------------------------------------------------------------------------- 1 | export const Kbd = ({ children }: Props): JSX.Element => { 2 | return ( 3 | 7 | {children} 8 | 9 | ); 10 | }; 11 | 12 | interface Props { 13 | children: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/components/priority-icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./priority-icon"; 2 | -------------------------------------------------------------------------------- /src/app/components/priority-icon/priority-icon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import { PriorityId } from "@domain/priority"; 3 | import { PriorityIcon } from "./priority-icon"; 4 | 5 | const meta: Meta = { 6 | title: "Components/PriorityIcon", 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | argTypes: { 11 | priority: { 12 | defaultValue: "low", 13 | control: { 14 | type: "select", 15 | options: ["low", "medium", "high"], 16 | }, 17 | }, 18 | size: { 19 | control: { 20 | type: "number", 21 | }, 22 | }, 23 | }, 24 | }; 25 | 26 | export default meta; 27 | type Story = StoryObj; 28 | 29 | const priorities: PriorityId[] = ["low", "medium", "high"]; 30 | const sizes = [18, 24, 32, 48]; 31 | 32 | export const Default: Story = { 33 | render: (_) => ( 34 |
35 | 36 | {sizes.map((size) => ( 37 | {size}px 38 | ))} 39 | {priorities.map((priority) => ( 40 | <> 41 | {priority} 42 | {sizes.map((size) => ( 43 | 48 | ))} 49 | 50 | ))} 51 |
52 | ), 53 | }; 54 | 55 | export const Low: Story = { 56 | args: { 57 | priority: "low", 58 | }, 59 | }; 60 | 61 | export const Medium: Story = { 62 | args: { 63 | priority: "medium", 64 | }, 65 | }; 66 | 67 | export const High: Story = { 68 | args: { 69 | priority: "high", 70 | }, 71 | }; 72 | 73 | export const Size24: Story = { 74 | args: { 75 | priority: "low", 76 | size: 24, 77 | }, 78 | }; 79 | 80 | export const Size32: Story = { 81 | args: { 82 | priority: "low", 83 | size: 32, 84 | }, 85 | }; 86 | 87 | export const Size48: Story = { 88 | args: { 89 | priority: "low", 90 | size: 48, 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /src/app/components/priority-icon/priority-icon.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classix"; 2 | import { HiFlag } from "react-icons/hi"; 3 | import { PriorityId } from "@domain/priority"; 4 | 5 | export const PriorityIcon = ({ 6 | priority, 7 | size = 18, 8 | }: PriorityIconProps): JSX.Element => ( 9 | 17 | 18 | 19 | ); 20 | 21 | interface PriorityIconProps { 22 | priority: PriorityId; 23 | size?: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/components/scroll-area/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scroll-area"; 2 | -------------------------------------------------------------------------------- /src/app/components/scroll-area/scroll-area.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "@storybook/react"; 2 | import cx from "classix"; 3 | import { ScrollArea } from "./scroll-area"; 4 | 5 | const meta: Meta = { 6 | title: "Components/ScrollArea", 7 | parameters: { 8 | layout: "centered", 9 | }, 10 | argTypes: { 11 | className: { 12 | defaultValue: "", 13 | control: { 14 | type: "text", 15 | }, 16 | }, 17 | children: { 18 | defaultValue: "Scroll area", 19 | control: { 20 | type: "text", 21 | }, 22 | }, 23 | }, 24 | }; 25 | 26 | export default meta; 27 | type Story = StoryObj; 28 | 29 | const height = 250; 30 | 31 | export const Default: Story = { 32 | render: (_) => ( 33 | <> 34 |

Height: {height}px

35 |
39 | 40 | {Array.from({ length: 100 }, (_, index) => ( 41 |
48 | Scroll item content 49 |
50 | ))} 51 |
52 |
53 | 54 | ), 55 | }; 56 | -------------------------------------------------------------------------------- /src/app/components/scroll-area/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 2 | import cx from "classix"; 3 | 4 | export const ScrollArea = ({ 5 | className, 6 | children, 7 | }: ScrollAreaProps): JSX.Element => ( 8 | 13 | 14 | {children} 15 | 16 | 22 | 28 | 29 | 30 | 31 | ); 32 | 33 | interface ScrollAreaProps { 34 | className?: string; 35 | children: JSX.Element | JSX.Element[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/components/select/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./select"; 2 | -------------------------------------------------------------------------------- /src/app/components/select/select.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import type { Meta, StoryObj } from "@storybook/react"; 3 | import { User, UserId, usersMock } from "@domain/user"; 4 | import * as Select from "@app/components/select"; 5 | import { UserAvatar } from "@app/components/user-avatar"; 6 | 7 | const meta: Meta = { 8 | title: "Components/Select", 9 | parameters: { 10 | layout: "centered", 11 | }, 12 | argTypes: {}, 13 | }; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const SelectUser: Story = { 19 | render: () => , 20 | }; 21 | 22 | const SelectUserComponent = () => { 23 | const defaultUser = usersMock[0]; 24 | 25 | const [selectedValue, setSelectedValue] = useState(defaultUser); 26 | 27 | const onValueChange = (userId: UserId) => { 28 | const asignee = usersMock.find((user) => user.id === userId); 29 | 30 | if (asignee) { 31 | setSelectedValue(asignee); 32 | } 33 | }; 34 | 35 | return ( 36 | 41 | 42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 | 50 | 51 | {usersMock.map((user, index) => ( 52 | 53 | 54 | 55 | {user.name} 56 | 57 | ))} 58 | 59 | 60 | 61 | 62 |
63 | ); 64 | }; 65 | 66 | export const SelectText: Story = { 67 | render: () => { 68 | const items = ["Item 1", "Item 2", "Item 3"]; 69 | 70 | return ( 71 | 72 | 73 | Trigger 74 | 75 | 76 | 77 | 78 | 79 | 80 | {items.map((item, index) => ( 81 | 82 | 83 | {item} 84 | 85 | ))} 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | }, 93 | }; 94 | -------------------------------------------------------------------------------- /src/app/components/select/select.tsx: -------------------------------------------------------------------------------- 1 | import * as Select from "@radix-ui/react-select"; 2 | import { twix } from "tailwindcss-radix-ui"; 3 | import cx from "classix"; 4 | import { RiArrowDropDownLine } from "react-icons/ri"; 5 | 6 | export const Root = Select.Root; 7 | export const Value = Select.Value; 8 | export const ItemText = Select.ItemText; 9 | export const ScrollUpButton = Select.ScrollUpButton; 10 | export const ScrollDownButton = Select.ScrollUpButton; 11 | export const Viewport = Select.Viewport; 12 | export const Separator = Select.Separator; 13 | 14 | export const Trigger = twix( 15 | Select.Trigger, 16 | cx( 17 | "flex cursor-pointer items-center rounded border-none py-1.5 px-2", 18 | "bg-background-neutral hover:bg-background-neutral-hovered active:bg-background-neutral-pressed", 19 | "font-primary-bold text-sm text-font" 20 | ) 21 | ); 22 | 23 | export const TriggerIcon = (): JSX.Element => ( 24 | 25 | 26 | 27 | ); 28 | 29 | export const Content = twix( 30 | Select.Content, 31 | "bg-elevation-surface-overlay py-1 shadow-md" 32 | ); 33 | 34 | export const Item = twix( 35 | Select.Item, 36 | cx( 37 | "relative flex items-center gap-2 cursor-pointer select-none border-l-[3px] border-l-transparent p-2 pl-8", 38 | "font-primary-bold text-sm text-font hover:bg-background-selected active:bg-background-selected-pressed", 39 | "focus:border-l-[3px] focus:border-l-border-selected focus:bg-background-selected focus-visible:outline-none outline-none" 40 | ) 41 | ); 42 | 43 | export const ItemIndicator = twix( 44 | Select.ItemIndicator, 45 | "absolute left-3 top-1/2 h-[7px] w-[7px] -translate-y-1/2 rounded-full bg-border-selected" 46 | ); 47 | -------------------------------------------------------------------------------- /src/app/components/textarea-autosize.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState, useRef } from "react"; 2 | import cx from "classix"; 3 | 4 | export const TextareaAutosize = (props: TitleProps): JSX.Element => { 5 | const { 6 | name, 7 | value, 8 | setValue, 9 | placeholder, 10 | readOnly, 11 | autofocus, 12 | textareaClassName, 13 | onFocus, 14 | onBlur, 15 | } = props; 16 | 17 | const [textareaHeight, setTextareaHeight] = useState(40); 18 | const textareaRef = useRef(null); 19 | 20 | const handleOnFocus = (e: React.FocusEvent) => { 21 | const target = e.currentTarget; 22 | const length = target.value.length; 23 | // Place cursor at the end of the current text 24 | target.setSelectionRange(length, length); 25 | if (onFocus) onFocus(); 26 | }; 27 | 28 | const handleTitleChange = (e: React.FormEvent): void => { 29 | const value = e.currentTarget.value; 30 | setValue(value); 31 | }; 32 | 33 | const valueIsNotOnlySpaces = (): boolean => { 34 | return !/^( )\1*$/.test(value); 35 | }; 36 | 37 | useLayoutEffect(() => { 38 | if (!textareaRef.current) return; 39 | 40 | setTextareaHeight(textareaRef.current.scrollHeight); 41 | }, [value]); 42 | 43 | return ( 44 |
45 |