├── .eslintrc.json ├── styles └── globals.css ├── app ├── favicon.ico ├── api │ └── todo │ │ └── route.ts ├── layout.tsx ├── playground │ └── page.tsx └── page.tsx ├── postcss.config.js ├── next.config.js ├── components ├── SuspenseComponent.tsx ├── Counter.tsx ├── CompWithFetch.tsx ├── ErrorBoundary.tsx ├── VirtualizedList.tsx ├── ui │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── card.tsx │ ├── button.tsx │ ├── form.tsx │ └── dropdown-menu.tsx ├── UseCallbackExample.tsx ├── Column.tsx ├── SearchComponent.tsx ├── PostsComponent.tsx ├── UseMemoExample.tsx ├── Dashboard.tsx ├── TaskList.tsx ├── TaskItem.tsx └── CreateTaskForm.tsx ├── components.json ├── hooks ├── useDebounce.tsx ├── useFetch.tsx ├── useLocalStorage.tsx └── useWindowSize.ts ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── README.md ├── __tests__ ├── button.test.tsx └── counter.test.tsx ├── tsconfig.json ├── types └── types.ts ├── jest.config.ts ├── tailwind.config.js ├── dommyData.ts ├── package.json ├── lib └── utils.ts └── context └── BoardContext └── BoardContext.tsx /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/betomoedano/Porter-Task-Manager/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /components/SuspenseComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { fetcher } from "@/lib/utils"; 3 | import useSWR from "swr"; 4 | 5 | export default function SuspenseComponent() { 6 | const { data } = useSWR("/api/todo", fetcher, { suspense: true }); 7 | 8 | return

{data.message}

; 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /hooks/useDebounce.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useDebounce(value: T, delay: number): T { 4 | const [debounceValue, setDebounceValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebounceValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debounceValue; 17 | } 18 | -------------------------------------------------------------------------------- /components/Counter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Button } from "./ui/button"; 3 | 4 | export function Counter() { 5 | const [count, setCount] = useState(0); 6 | 7 | const increment = () => { 8 | setCount(count + 1); 9 | }; 10 | 11 | return ( 12 |
13 |

Counter: {count}

14 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.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 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /app/api/todo/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(request: Request) { 2 | // validate user 3 | // auth 4 | console.log("This is running on the server"); 5 | // Wait for one second 6 | 7 | await new Promise((resolve) => setTimeout(resolve, 3000)); 8 | 9 | // throw new Error("error"); 10 | return Response.json({ message: "Success again" }); 11 | } 12 | 13 | export async function POST(request: Request) { 14 | const body = await request.json(); 15 | console.log(body); 16 | return Response.json({ message: "Success POST", body }); 17 | } 18 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BoardProvider } from "@/context/BoardContext/BoardContext"; 2 | import "../styles/globals.css"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | export default function RootLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 | 12 | 13 |
14 | {children} 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/CompWithFetch.tsx: -------------------------------------------------------------------------------- 1 | import { getReq } from "@/lib/utils"; 2 | 3 | export default async function CompWithFetch() { 4 | const data = await getReq(); 5 | console.log("Server", data); 6 | return

{JSON.stringify(data)}

; 7 | } 8 | 9 | // "use client"; 10 | // import { fetcher } from "@/lib/utils"; 11 | // import useSWR from "swr"; 12 | 13 | // export default function CompWithFetch() { 14 | // const { data, error, isLoading } = useSWR("/api/todo", fetcher); 15 | 16 | // if (error) return

Error

; 17 | // if (isLoading) return

Loading...

; 18 | // return

{data.message}

; 19 | // } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Live Demo! 👉🏼 [porter-demo.vercel.app](https://porter-demo.vercel.app/) 4 | 5 | Lear to build this app at [codewithbeto.dev](https://codewithbeto.dev) 6 | 7 | Design provided by [Eco Studios](https://www.ecostudios.dev) 8 | 9 | ## Getting Started 10 | 11 | Install dependencies: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Then run the server: 18 | 19 | ```bash 20 | npm run dev 21 | ``` 22 | 23 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 24 | 25 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 26 | -------------------------------------------------------------------------------- /__tests__/button.test.tsx: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { Button } from "@/components/ui/button"; 4 | 5 | describe("Button Component", () => { 6 | test("renders with correct text", () => { 7 | render(); 8 | const buttonElement = screen.getByText("Click Me!"); 9 | expect(buttonElement).toBeInTheDocument(); 10 | }); 11 | 12 | test("renders correctly", () => { 13 | render(); 14 | const buttonElement = screen.getByTestId("button"); 15 | expect(buttonElement).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Component, ReactNode } from "react"; 3 | 4 | interface ErrorBoundaryState { 5 | hasError: boolean; 6 | } 7 | 8 | export class ErrorBoundary extends Component< 9 | React.PropsWithChildren<{}>, 10 | ErrorBoundaryState 11 | > { 12 | constructor(props: React.PropsWithChildren) { 13 | super(props); 14 | this.state = { hasError: false }; 15 | } 16 | 17 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 18 | return { hasError: true }; 19 | } 20 | 21 | render(): ReactNode { 22 | if (this.state.hasError) { 23 | return

Something went wrong

; 24 | } 25 | 26 | return this.props.children; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/counter.test.tsx: -------------------------------------------------------------------------------- 1 | import { Counter } from "@/components/Counter"; 2 | import { fireEvent, render } from "@testing-library/react"; 3 | 4 | describe("Counter Component", () => { 5 | test("Initial count is 0", () => { 6 | const { getByTestId } = render(); 7 | const countElement = getByTestId("count"); 8 | expect(countElement.textContent).toBe("Counter: 0"); 9 | }); 10 | 11 | test("Increments count by 1 when increment button is clicked", () => { 12 | const { getByTestId } = render(); 13 | const incrementButton = getByTestId("increment-button"); 14 | fireEvent.click(incrementButton); 15 | const countElement = getByTestId("count"); 16 | expect(countElement.textContent).toBe("Counter: 1"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /components/VirtualizedList.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Product } from "@/lib/utils"; 3 | import * as React from "react"; 4 | import { List, ListRowProps } from "react-virtualized"; 5 | 6 | export default function VirtualizedList({ 7 | products, 8 | }: { 9 | products: Product[]; 10 | }): JSX.Element { 11 | function rowRenderer({ key, index, style }: ListRowProps) { 12 | return ( 13 |
14 | {products[index].name} 15 |
16 | ); 17 | } 18 | 19 | return ( 20 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | import { DraggableLocation } from "react-beautiful-dnd"; 2 | 3 | export type Task = { 4 | id: string; 5 | task: string; 6 | tag: string; 7 | date: string; 8 | description: string; 9 | }; 10 | 11 | export type TaskMap = { 12 | [key: string]: Task[]; 13 | }; 14 | 15 | export type Board = { 16 | columns: TaskMap; 17 | ordered: string[]; 18 | }; 19 | 20 | export type BoardAction = 21 | | { type: "SET_TASKS"; payload: Board } 22 | | { type: "ADD_TASK"; payload: Task } 23 | | { type: "REMOVE_TASK"; payload: RemoveTaskPayload } 24 | | { type: "MOVE_TASK"; payload: OnDragPayload } 25 | | { type: "MOVE_COLUMN"; payload: OnDragPayload }; 26 | 27 | type OnDragPayload = { 28 | source: DraggableLocation; 29 | destination: DraggableLocation; 30 | }; 31 | 32 | type RemoveTaskPayload = { 33 | id: string; 34 | }; 35 | -------------------------------------------------------------------------------- /hooks/useFetch.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | interface FetchState { 4 | data: T | null; 5 | loading: boolean; 6 | error: Error | null; 7 | } 8 | 9 | export function useFetch(url: string, options?: RequestInit): FetchState { 10 | const [state, setState] = useState>({ 11 | data: null, 12 | loading: true, 13 | error: null, 14 | }); 15 | 16 | useEffect(() => { 17 | const fetchData = async () => { 18 | try { 19 | const response = await fetch(url, options); 20 | const data: T = await response.json(); 21 | setState({ data, loading: false, error: null }); 22 | } catch (error) { 23 | setState({ data: null, loading: false, error: error as Error }); 24 | } 25 | }; 26 | 27 | fetchData(); 28 | }, [url, options]); 29 | 30 | return state; 31 | } 32 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | import type { Config } from "jest"; 7 | import nextJest from "next/jest"; 8 | 9 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 10 | const createJestConfig = nextJest({ 11 | dir: "./", 12 | }); 13 | 14 | const config: Config = { 15 | // Automatically clear mock calls, instances, contexts and results before every test 16 | clearMocks: true, 17 | 18 | // Indicates which provider should be used to instrument code for coverage 19 | coverageProvider: "v8", 20 | testEnvironment: "jsdom", 21 | }; 22 | 23 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 24 | export default createJestConfig(config); 25 | -------------------------------------------------------------------------------- /hooks/useLocalStorage.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | function useLocalStorage( 4 | key: string, 5 | initialValue: T 6 | ): [T, (value: T) => void] { 7 | const [storedValue, setStoredValue] = useState(() => { 8 | try { 9 | const item = window.localStorage.getItem(key); 10 | return item ? JSON.parse(item) : initialValue; 11 | } catch (error) { 12 | console.log(error); 13 | return initialValue; 14 | } 15 | }); 16 | 17 | const setValue = (value: T) => { 18 | try { 19 | const valueToStore = 20 | value instanceof Function ? value(storedValue) : value; 21 | setStoredValue(valueToStore); 22 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 23 | } catch (error) { 24 | console.log(error); 25 | } 26 | }; 27 | 28 | return [storedValue, setValue]; 29 | } 30 | 31 | export default useLocalStorage; 32 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |