Skip to content

Commit

Permalink
Merge pull request #1 from felipeog/prototype
Browse files Browse the repository at this point in the history
Prototype
  • Loading branch information
felipeog authored Dec 25, 2024
2 parents 03ff3ea + 6bd14fb commit afbe0df
Show file tree
Hide file tree
Showing 12 changed files with 466 additions and 141 deletions.
9 changes: 7 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>

<link rel="icon" type="image/svg+xml" href="/vite.svg" />

<title>Reddit Client</title>
</head>

<body>
<div id="root"></div>

<script type="module" src="/src/main.tsx"></script>
</body>
</html>
80 changes: 80 additions & 0 deletions src/AddBoardDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FormEvent, useRef, useState } from "react";
import { useBoardContext } from "./BoardContext";
import { useBoardExists } from "./useBoardExists";

export const ADD_BOARD_DIALOG_HEIGHT = "40px";

export function AddBoardDialog() {
const [board, setBoard] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const dialogRef = useRef<HTMLDialogElement>(null);
const { checkBoardExistance, isLoading } = useBoardExists();
const { boardContext } = useBoardContext();

async function handleFormSubmit(event: FormEvent) {
event.preventDefault();

const doesBoardExist = await checkBoardExistance(board);

if (!doesBoardExist) {
return alert("This board does not exist");
}

boardContext.addBoard(board);
closeDialog();
setBoard("");
}

function openDialog() {
dialogRef.current?.showModal();
inputRef.current?.focus();
}

function closeDialog() {
dialogRef.current?.close();
}

return (
<div
className="AddBoardDialog"
style={{
height: ADD_BOARD_DIALOG_HEIGHT,
display: "flex",
placeContent: "center",
padding: 8,
borderBottom: `1px solid var(--neutral-60)`,
}}
>
<button onClick={openDialog}>Add board</button>

<dialog
ref={dialogRef}
style={{
padding: "revert",
margin: "revert",
backgroundColor: "var(--neutral-90)",
color: "var(--neutral-10)",
borderColor: "var(--neutral-60)",
}}
>
<button onClick={closeDialog} disabled={isLoading}>
Close
</button>

<form onSubmit={handleFormSubmit}>
<p>Enter the name of the board</p>
<input
ref={inputRef}
type="text"
name="board"
required
disabled={isLoading}
value={board}
onChange={(event) => setBoard(event.target.value)}
/>
<button disabled={isLoading}>Add board</button>
</form>
</dialog>
</div>
);
}
42 changes: 0 additions & 42 deletions src/App.css

This file was deleted.

38 changes: 7 additions & 31 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,11 @@
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";

function App() {
const [count, setCount] = useState(0);
import { AddBoardDialog } from "./AddBoardDialog";
import { LanesContainer } from "./LanesContainer";

export function App() {
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
<div className="App">
<AddBoardDialog />
<LanesContainer />
</div>
);
}

export default App;
91 changes: 91 additions & 0 deletions src/BoardContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";

const LOCAL_STORAGE_KEY = "@felipeog/rm-reddit-client/boards";

type TBoardContext = {
boards: string[];
addBoard: (board: string) => void;
removeBoard: (board: string) => void;
reorderBoard: (from: string, to: string) => void;
};

type TBoardContextProviderProps = {
children: ReactNode;
};

const BoardContext = createContext<TBoardContext>(null! as TBoardContext);

export function BoardContextProvider(props: TBoardContextProviderProps) {
const [boards, setBoards] = useState<string[]>(() => {
const boardsString = localStorage.getItem(LOCAL_STORAGE_KEY) || "[]";
return JSON.parse(boardsString);
});

const addBoard = useCallback(
(board: string) =>
setBoards((prevBoards) => {
if (prevBoards.includes(board)) return prevBoards;
return [...prevBoards, board];
}),
[]
);

const removeBoard = useCallback(
(board: string) =>
setBoards((prevBoards) =>
prevBoards.filter((prevBoard) => prevBoard !== board)
),
[]
);

const reorderBoard = useCallback(
(from: string, to: string) =>
setBoards((prevBoards) => {
const boards = [...prevBoards];
const fromIndex = boards.indexOf(from);
const toIndex = boards.indexOf(to);

boards.splice(fromIndex, 1);
boards.splice(toIndex, 0, from);

return boards;
}),
[]
);

useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(boards));
}, [boards]);

return (
<BoardContext.Provider
value={{
boards,
addBoard,
removeBoard,
reorderBoard,
}}
>
{props.children}
</BoardContext.Provider>
);
}

export function useBoardContext() {
const boardContext = useContext(BoardContext);

if (!boardContext) {
throw new Error(
"useBoardContext has to be used within BoardContextProvider."
);
}

return { boardContext };
}
117 changes: 117 additions & 0 deletions src/LanesContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ADD_BOARD_DIALOG_HEIGHT } from "./AddBoardDialog";
import { useBoardContent } from "./useBoardContent";
import { useBoardContext } from "./BoardContext";
import { formatDate } from "./formatDate";
import { DragEvent } from "react";

export function LanesContainer() {
const { boardContext } = useBoardContext();

return (
<div
className="LanesContainer"
style={{
display: "flex",
height: `calc(100vh - ${ADD_BOARD_DIALOG_HEIGHT})`,
overflowX: "scroll",
}}
>
{boardContext.boards.map((board) => (
<Lane key={board} board={board} />
))}
</div>
);
}

type TLaneProps = {
board: string;
};

function Lane(props: TLaneProps) {
const { boardContext } = useBoardContext();
const { boardContent, isLoading, loadMore } = useBoardContent(props.board);

function onDragStart(event: DragEvent<HTMLDivElement>) {
event.dataTransfer.setData("text/plain", props.board);
}

function onDragEnter(event: DragEvent<HTMLDivElement>) {
event.preventDefault();
}

function onDragOver(event: DragEvent<HTMLDivElement>) {
event.preventDefault();
}

function onDrop(event: DragEvent<HTMLDivElement>) {
const board = event.dataTransfer.getData("text/plain");
if (board !== props.board) {
boardContext.reorderBoard(board, props.board);
}
}

return (
<div
className="Lane"
style={{
width: "30vw",
minWidth: "30vw",
height: "100%",
borderRight: "1px solid var(--neutral-60)",
overflowY: "scroll",
}}
draggable
onDragStart={onDragStart}
onDragEnter={onDragEnter}
onDragOver={onDragOver}
onDrop={onDrop}
>
<div
style={{
position: "sticky",
top: 0,
backgroundColor: "var(--neutral-90)",
paddingBottom: 10,
}}
>
<h1>
<a
href={`https://www.reddit.com/r/${props.board}`}
target="_blank"
rel="noopener noreferrer"
>
{props.board}
</a>
</h1>
<button onClick={() => boardContext.removeBoard(props.board)}>
Remove board
</button>
</div>

<div style={{ backgroundColor: "var(--neutral-90)" }}>
{boardContent.length > 0
? boardContent.map((item) => (
<div key={item.name}>
<p>
Title:{" "}
<a href={item.url} target="_blank" rel="noopener noreferrer">
{item.title}
</a>
</p>
<p>Author: {item.author}</p>
<p>Created: {formatDate(item.created * 1000)}</p>
<p>
Ups/Downs: {item.ups}/{item.downs}
</p>
<br />
</div>
))
: null}
</div>

<button onClick={loadMore} disabled={isLoading}>
Load more
</button>
</div>
);
}
Loading

0 comments on commit afbe0df

Please sign in to comment.