Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
<html lang="ko">
<head>
<title>Tickets</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="ticket.css" />
</head>
<body>
<div id="root"></div>
Expand Down
289 changes: 286 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,287 @@
const root = document.getElementById('root');
if (root) {
root.innerHTML = '<p>Hello, world!</p>';
/* @jsx createElement */

declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}

function capitalize(text: string) {
return text.charAt(0).toUpperCase() + text.slice(1);
}

function createElement(
type: string | Function,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

'Function' 타입 대신 명시적인 함수 타입을 사용하세요

Function 타입은 모든 함수 형태를 허용하기 때문에 오류의 원인이 될 수 있습니다. 대신 함수 타입을 명시적으로 정의하는 것이 좋습니다.

function createElement(
-  type: string | Function,
+  type: string | ((props: Record<string, unknown>) => Node),
  props: any,
  ...children: any[]
) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type: string | Function,
function createElement(
type: string | ((props: Record<string, unknown>) => Node),
props: any,
...children: any[]
) {
// function body...
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 14-14: Don't use 'Function' as a type.

Prefer explicitly define the function shape. This type accepts any function-like value, which can be a common source of bugs.

(lint/complexity/noBannedTypes)

props: any,
...children: any[]
) {
if (typeof type === "function") {
return type({ ...props, children });
}

const element = document.createElement(type);
Object.assign(element, props);
if (props) {
["click", "submit"].forEach((event) => {
const handler = props[`on${capitalize(event)}`];
if (handler) {
element.addEventListener(event, handler);
}
});
}
children.forEach((child) => {
if (Array.isArray(child)) {
child.forEach((childItem) => element.append(childItem));
return;
}
element.append(child);
});
return element;
}

interface IComment {
id: number;
comment: string;
}

interface Ticket {
id: number;
title: string;
description: string;
status: "open" | "closed";
toggle(): void;
comments: IComment[];
}

function Header() {
return (
<header>
<h1>Tickets</h1>
</header>
);
}

function Main({
tickets,
addTicket,
addComment,
}: {
tickets: Ticket[];
addTicket: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
addComment: (comment: string, ticketId: number) => void;
}) {
return (
<main>
<TicketForm addTicket={addTicket} />
<TicketList tickets={tickets} addComment={addComment} />
</main>
);
}

function CommentList({ comments }: { comments: IComment[] }) {
return (
<div>
{comments.map((comment) => (
<div className="comment-item" key={comment.id}>
{comment.comment}
</div>
))}
</div>
);
}

function CommentForm({
addComment,
ticketId,
}: {
addComment: (comment: string, ticketId: number) => void;
ticketId: number;
}) {
const handleAddComment = (event: Event) => {
event.preventDefault();

const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const comment = formData.get("comment") as string;

addComment(comment, ticketId);
};

return (
<form id="add-comment-form" onSubmit={handleAddComment}>
<div>
<label htmlFor="comment">Add a comment</label>
<input type="text" name="comment" id="comment" />
</div>
<button type="submit">Add Comment</button>
</form>
);
}

function TicketList({
tickets,
addComment,
}: {
tickets: Ticket[];
addComment: (comment: string, ticketId: number) => void;
}) {
return (
<ul id="ticket-list">
{tickets.map((ticket) => (
<TicketItem ticket={ticket} addComment={addComment} />
))}
</ul>
);
}

function TicketItem({
ticket,
addComment,
}: {
ticket: Ticket;
addComment: (comment: string, ticketId: number) => void;
}) {
const handleClick = () => {
ticket.toggle();
};

return (
<li key={ticket.id}>
<div className="title">{ticket.title}</div>
<div className="description">{ticket.description}</div>
<button className="status" onClick={handleClick}>
{ticket.status === "open" ? "OPEN" : "CLOSED"}
</button>
<div id="comments">
<CommentForm addComment={addComment} ticketId={ticket.id} />
<CommentList comments={ticket.comments} />
</div>
</li>
);
}

function TicketForm({
addTicket,
}: {
addTicket: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
}) {
const handleSubmit = (event: Event) => {
event.preventDefault();

const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const title = formData.get("title") as string;
const description = formData.get("description") as string;

addTicket({ title, description });
};

return (
<form id="add-ticket-form" onSubmit={handleSubmit}>
<div>
<label for="ticket-title">Title</label>
<input type="text" name="title" id="ticket-title" placeholder="Title" />
Comment on lines +194 to +195
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

HTML 레이블에 'for' 대신 'htmlFor' 속성을 사용하세요

JSX에서는 HTML의 for 속성 대신 htmlFor를 사용해야 합니다. for는 JavaScript의 예약어이기 때문입니다.

-      <label for="ticket-title">Title</label>
+      <label htmlFor="ticket-title">Title</label>
      <input type="text" name="title" id="ticket-title" placeholder="Title" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label for="ticket-title">Title</label>
<input type="text" name="title" id="ticket-title" placeholder="Title" />
<label htmlFor="ticket-title">Title</label>
<input type="text" name="title" id="ticket-title" placeholder="Title" />

</div>
<div>
<label for="ticket-description">Description</label>
<textarea
Comment on lines +198 to +199
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

HTML 레이블에 'for' 대신 'htmlFor' 속성을 사용하세요

JSX에서는 HTML의 for 속성 대신 htmlFor를 사용해야 합니다. for는 JavaScript의 예약어이기 때문입니다.

-      <label for="ticket-description">Description</label>
+      <label htmlFor="ticket-description">Description</label>
      <textarea
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label for="ticket-description">Description</label>
<textarea
<label htmlFor="ticket-description">Description</label>
<textarea

name="description"
id="ticket-description"
placeholder="Description"
></textarea>
</div>
<button type="submit" id="add-ticket">
Add Ticket
</button>
</form>
);
}

function render({
root,
tickets,
addTicket,
addComment,
}: {
root: HTMLElement;
tickets: Ticket[];
addTicket: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
addComment: (comment: string, ticketId: number) => void;
}) {
root.replaceChildren(
<div>
<Header />
<Main tickets={tickets} addTicket={addTicket} addComment={addComment} />
</div>
);
}

document.addEventListener("DOMContentLoaded", () => {
const root = document.getElementById("root");
if (root) {
const tickets: Ticket[] = [];

const update = () => {
render({ root, tickets, addTicket, addComment });
};

const addTicket = ({
title,
description,
}: {
title: string;
description: string;
}) => {
const id = Math.max(...tickets.map((ticket) => ticket.id), 0) + 1;

const ticket: Ticket = {
id,
title,
description,
status: "open",
toggle() {
this.status = this.status === "open" ? "closed" : "open";
update();
},
comments: [],
};

tickets.push(ticket);
update();
};

const addComment = (comment: string, ticketId: number) => {
const ticket = tickets.find((ticket) => ticket.id === ticketId);
if (ticket) {
ticket.comments = [
...ticket.comments,
{
id: Math.max(...ticket.comments.map((c) => c.id), 0) + 1,
comment,
},
];
update();
}
};

update();
}
});
82 changes: 82 additions & 0 deletions ticket.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
html {
box-sizing: border-box;
}

*,
*::before,
*::after {
box-sizing: inherit;
}

html {
font-size: 62.5%;
}

body {
font-size: 1.6rem;
}

#ticket-list {
margin: 0;
padding: 0;
list-style: none;

li {
margin-block: 1rem;
padding: 1rem;
border: 1px solid #ccc;

.title {
font-size: 2rem;
font-weight: 700;
}

.description {
margin-block: 0.5rem;
}
}
}

form {
margin-block: 2rem;
max-width: 40rem;

div {
margin-block: 1rem;
}

label {
display: block;
margin-bottom: 0.4rem;
}

input,
textarea {
width: 100%;
padding: 0.8rem 1rem;
}

textarea {
height: 10rem;
}

button {
width: 100%;
padding: 0.8rem 1rem;
}
}

#comments {
margin-block: 1rem;
}

.comment-item {
margin-block: 0.5rem;
padding: 0.8rem 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}

.status {
margin-block: 0.5rem;
}