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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
max_line_length = 120
5 changes: 5 additions & 0 deletions src/d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
138 changes: 136 additions & 2 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,138 @@
const root = document.getElementById('root');
import { createElement } from "./utils/jsx";

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

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

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

function TicketList({ tickets }: { tickets: Ticket[] }) {
return (
<ul id="ticket-list">
{tickets.map((ticket) => (
<TicketItem ticket={ticket} />
))}
</ul>
);
}
Comment on lines +34 to +42
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

TicketList의 반복 렌더링
tickets 배열을 map으로 순회하고 있습니다. React 구조에서는 key 속성을 생략하면 성능 최적화와 경고 메시지 측면에서 문제가 있을 수 있습니다.

아래와 같이 key를 추가해 주세요:

-{tickets.map((ticket) => (
-  <TicketItem ticket={ticket} />
-))}
+{tickets.map((ticket) => (
+  <TicketItem key={ticket.id} ticket={ticket} />
+))}
📝 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
function TicketList({ tickets }: { tickets: Ticket[] }) {
return (
<ul id="ticket-list">
{tickets.map((ticket) => (
<TicketItem ticket={ticket} />
))}
</ul>
);
}
function TicketList({ tickets }: { tickets: Ticket[] }) {
return (
<ul id="ticket-list">
{tickets.map((ticket) => (
<TicketItem key={ticket.id} ticket={ticket} />
))}
</ul>
);
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 38-38: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)


function TicketItem({ ticket }: { ticket: Ticket }) {
const handleClick = () => {
ticket.toggle();
};

return (
<li>
<div className="title">{ticket.title}</div>
<div className="description">{ticket.description}</div>
<button className="status" onClick={handleClick}>
{ticket.status === "open" ? "Open" : "Closed"}
</button>
</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;
Comment on lines +69 to +71

Choose a reason for hiding this comment

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

medium

Consider adding type assertions or validation to ensure that the form data is of the expected type. This can help prevent runtime errors and improve the overall robustness of the application.

    const title = formData.get("title") as string;
    const description = formData.get("description") as string;

    if (!title || !description) {
      console.error("Title or description is missing");
      return;
    }


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" />
</div>
<div>
<label for="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,
}: {
root: HTMLElement;
tickets: Ticket[];
addTicket: ({ title, description }: { title: string; description: string }) => void;
}) {
root.replaceChildren(
<div>
<Header />
<Main tickets={tickets} addTicket={addTicket} />
</div>
);
}

const root = document.getElementById("root");

if (root) {
root.innerHTML = '<p>Hello, world!</p>';
const tickets: Ticket[] = [];

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

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

Choose a reason for hiding this comment

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

medium

The current implementation for generating ticket IDs is not ideal. It relies on finding the maximum existing ID and incrementing it, which is not thread-safe and can lead to collisions in concurrent environments. Consider using a more robust ID generation strategy, such as a UUID generator or a server-side auto-incrementing ID.

    const id = Date.now(); // Using timestamp for simplicity, consider UUID for production

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

tickets.push(ticket);

update();
};

update();
}
27 changes: 27 additions & 0 deletions src/utils/jsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function createElement(type: string | Function, props: any, ...children: any[]) {

Choose a reason for hiding this comment

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

medium

While the custom createElement function works, it might be beneficial to consider using a more established JSX transformer like Babel or TypeScript's built-in JSX support. This can provide better compatibility, performance, and features. If you stick with this approach, consider adding more type safety to the props and children.

export function createElement(type: string | Function, props: Record<string, any> | null, ...children: any[]) {

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

함수 타입 선언 개선이 필요합니다.

'Function' 타입은 너무 광범위하며, 타입 안전성 문제를 일으킬 수 있습니다. 정확한 함수 형태를 명시적으로 정의하는 것이 좋습니다.

다음과 같이 개선해 보세요:

- export function createElement(type: string | Function, props: any, ...children: any[]) {
+ export function createElement(
+   type: string | ((props: Record<string, any>) => HTMLElement),
+   props: Record<string, any> | null,
+   ...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
export function createElement(type: string | Function, props: any, ...children: any[]) {
export function createElement(
type: string | ((props: Record<string, any>) => HTMLElement),
props: Record<string, any> | null,
...children: any[]
) {
🧰 Tools
🪛 Biome (1.9.4)

[error] 1-1: 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)

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

const element = document.createElement(type);
Object.assign(element, props);

Comment on lines +6 to +8
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

속성 할당 방법 개선이 필요합니다.

Object.assign(element, props)는 DOM 속성이 아닌 커스텀 속성에 대해 제대로 작동하지 않을 수 있습니다.

다음과 같이 개선해 보세요:

const element = document.createElement(type);
- Object.assign(element, props);
+ if (props) {
+   Object.entries(props).forEach(([key, value]) => {
+     if (key === 'className' && typeof value === 'string') {
+       element.className = value;
+     } else if (key === 'style' && typeof value === 'object') {
+       Object.assign(element.style, value);
+     } else if (!key.startsWith('on') && typeof value !== 'function' && typeof value !== 'object') {
+       element.setAttribute(key, value);
+     }
+   });
+ }
📝 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
const element = document.createElement(type);
Object.assign(element, props);
const element = document.createElement(type);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key === 'className' && typeof value === 'string') {
element.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (!key.startsWith('on') && typeof value !== 'function' && typeof value !== 'object') {
element.setAttribute(key, value);
}
});
}

if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith("on") && typeof value === "function") {
const event = key.slice(2).toLocaleLowerCase();
element.addEventListener(event, value as EventListener);
}
});
}

children.forEach((child) => {
if (Array.isArray(child)) {
child.forEach((childItem) => element.append(childItem));
return;
}
element.append(child);
});

return element;
}
50 changes: 50 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,53 @@ html {
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;
}
}
14 changes: 7 additions & 7 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */

/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "ES2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "preserve", /* Specify what JSX code is generated. */
"jsx": "preserve" /* Specify what JSX code is generated. */,
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
Expand All @@ -26,7 +26,7 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
Expand Down Expand Up @@ -80,12 +80,12 @@
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,

/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
Expand All @@ -108,6 +108,6 @@

/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
7 changes: 7 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";

export default defineConfig({
esbuild: {
jsxFactory: "createElement",
},
});