├── src ├── vite-env.d.ts ├── App.css ├── components │ ├── Footer.tsx │ ├── Notification.tsx │ ├── Header.tsx │ ├── Results.tsx │ ├── Instructions.tsx │ ├── NameContainer.tsx │ ├── Switch.tsx │ └── Counter.tsx ├── main.tsx ├── index.css └── App.tsx ├── tsconfig.node.json ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── tests ├── setup.ts ├── Header.test.tsx ├── Switch.test.tsx ├── Notification.test.tsx ├── Footer.test.tsx ├── Results.test.tsx ├── NameContainer.test.tsx ├── Counter.test.tsx └── App.test.tsx ├── vite.config.ts ├── README.md ├── tsconfig.json ├── package.json └── public └── vite.svg /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | /* padding: 2rem; */ 5 | text-align: center; 6 | } 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
Made by Jasmine N.
4 | 5 | ) 6 | }; 7 | 8 | export default Footer; 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | export type NotificationProps = { 2 | message: string | null | undefined; 3 | } 4 | const Notification = ({ message }: NotificationProps) => { 5 | if (message === null) { 6 | return null; 7 | } 8 | return
{message}
; 9 | }; 10 | 11 | export default Notification; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | coverage 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Pickiest 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { expect, afterEach } from 'vitest' 2 | import { cleanup } from '@testing-library/react' 3 | // import '@testing-library/jest-dom'; 4 | import matchers from '@testing-library/jest-dom/matchers' 5 | 6 | 7 | // extends Vitest's expect method with methods from react-testing-library 8 | expect.extend(matchers) 9 | 10 | // hooks are reset before each suite so this does cleanup after each test case 11 | afterEach(() => { 12 | cleanup() 13 | }) -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | 4 | import { defineConfig } from 'vite' 5 | import react from '@vitejs/plugin-react' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react()], 10 | test: { 11 | globals: true, 12 | environment: 'jsdom', 13 | setupFiles: './tests/setup.ts', 14 | // include: ['./tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'] 15 | } 16 | }) -------------------------------------------------------------------------------- /tests/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { describe, it, expect } from 'vitest' 3 | import { render, screen } from '@testing-library/react' 4 | import Header from '../src/components/Header' 5 | 6 | describe('Header', () => { 7 | it('Header renders and shows correct text', () => { 8 | render(
) 9 | expect(screen.getByText('Pickiest')).toBeInTheDocument() 10 | expect(screen.getByText('The easiest way to pick randomly')).toBeInTheDocument() 11 | expect(screen.queryByText('Add')).not.toBeInTheDocument() 12 | }) 13 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pickiest 2 | 3 | A web app inspired from the Chwazi mobile app to let you choose a set of individual items or create groups from inputted items. 4 | 5 | ## Instructions: 6 | 1. Use the toggle to select if you want to choose specific individuals from your items or if you want to create groups of the items 7 | 2. Select how many items/groups should be picked (for example, choosing 1 item from your list or creating 2 groups from the items 8 | 3. Input each item using the text field. All items will appear on the page. 9 | 4. Click Pick to see your picked items/groups! 10 | 5. If you want to reset the names, click reset 11 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | // import Instructions from "./Instructions" 2 | 3 | 4 | function Header() { 5 | // const [showInstructions, setShowInstructions] = useState(false) 6 | 7 | // function toggleInstructions() { 8 | // setShowInstructions(!showInstructions) 9 | // } 10 | 11 | return ( 12 | <> 13 |

Pickiest

14 |

The easiest way to pick randomly

15 | {/*
How to use 16 | 17 |
*/} 18 | 19 | ) 20 | 21 | } 22 | export default Header -------------------------------------------------------------------------------- /src/components/Results.tsx: -------------------------------------------------------------------------------- 1 | export type ResultsProps = { 2 | showResults: boolean; 3 | results: object; 4 | isPerson: boolean; 5 | } 6 | function Results({ showResults, results, isPerson }: ResultsProps) { 7 | if (showResults) { 8 | return ( 9 |
10 |

Results

11 |
    12 | {Object.values(results).map((x, i) =>
  • {isPerson ? '' : `Group ${i + 1}:`}
    {x.join(", ")}
  • )} 13 |
14 |
15 | ) 16 | } else { 17 | return null 18 | } 19 | } 20 | export default Results -------------------------------------------------------------------------------- /tests/Switch.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { describe, it, expect, vi } from 'vitest' 3 | import { render, screen } from '@testing-library/react' 4 | import Switch, { SwitchProps } from '../src/components/Switch' 5 | 6 | vi.mock('react-switch-selector', () => { 7 | const Switch = vi.fn().mockImplementation(() => 'switch selector') 8 | return { default: Switch } 9 | }) 10 | 11 | const switchprops: SwitchProps = { 12 | handleToggle: () => vi.fn() 13 | } 14 | 15 | describe('Switch component', () => { 16 | it('Correctly displays all added names', () => { 17 | render() 18 | expect(screen.getByText('switch selector')).toBeInTheDocument() 19 | }) 20 | 21 | }) 22 | 23 | 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "types": ["vitest/globals", "@testing-library/jest-dom"], 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /tests/Notification.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { describe, it, expect } from 'vitest' 3 | import { render, screen } from '@testing-library/react' 4 | import Notification, { NotificationProps } from '../src/components/Notification' 5 | 6 | const notificationProps: NotificationProps = { 7 | message: 'Test Message' 8 | } 9 | 10 | describe('Notification Component', () => { 11 | 12 | it('Correctly displays non-null message', () => { 13 | render() 14 | expect(screen.getByText('Test Message')).toBeInTheDocument() 15 | }) 16 | 17 | it('Displays nothing when there is no message', () => { 18 | notificationProps.message = null 19 | const { container } = render() 20 | expect(container.innerHTML).toEqual(''); 21 | }) 22 | }) 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/Instructions.tsx: -------------------------------------------------------------------------------- 1 | type InstructionProps = { 2 | showInstructions: boolean; 3 | } 4 | 5 | function Instructions({ showInstructions }: InstructionProps) { 6 | if (showInstructions) { 7 | return ( 8 |
    9 |
  1. Use the toggle to select if you want to choose specific individuals from your items or if you want to create groups of the items
  2. 10 |
  3. Select how many items/groups should be picked (for example, choosing 1 item from your list or creating 2 groups from the items
  4. 11 |
  5. Input each item using the text field. All items will appear on the page
  6. 12 |
  7. Click Pick to see your picked items/groups
  8. 13 |
  9. If you want to reset the names, click reset
  10. 14 |
15 | ) 16 | } 17 | else return null 18 | } 19 | export default Instructions -------------------------------------------------------------------------------- /src/components/NameContainer.tsx: -------------------------------------------------------------------------------- 1 | type NameContainerProps = { 2 | names: string[]; 3 | isPerson: boolean; 4 | count: number; 5 | } 6 | 7 | // count is count of chosen things 8 | function NameContainer({ names, isPerson, count }: NameContainerProps) { 9 | const containerText = isPerson ? "Individual" : "Group"; 10 | const plural = count > 1 ? "s" : ""; 11 | 12 | if (names.length !== 0) { 13 | return ( 14 |
15 |

{`Pick ${count} ${containerText}${plural} from:`}

16 |
17 | {names.map((x, i) => ( 18 |
22 | {x} 23 |
24 | ))} 25 |
26 |
27 | ); 28 | } else return null; 29 | } 30 | 31 | export default NameContainer; 32 | -------------------------------------------------------------------------------- /tests/Footer.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { describe, it, expect } from 'vitest' 3 | import { render, screen } from '@testing-library/react' 4 | import Footer from '../src/components/Footer' 5 | 6 | describe('Footer', () => { 7 | 8 | it('Footer shows the correct text', () => { 9 | const { getByText } = render(