diff --git a/index.html b/index.html
index 74c8543..9aed10c 100644
--- a/index.html
+++ b/index.html
@@ -2,8 +2,9 @@
Tickets
-
-
+
+
+
diff --git a/src/main.tsx b/src/main.tsx
index 7830e88..3512afe 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,4 +1,287 @@
-const root = document.getElementById('root');
-if (root) {
- root.innerHTML = 'Hello, world!
';
+/* @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,
+ 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 (
+
+ );
+}
+
+function Main({
+ tickets,
+ addTicket,
+ addComment,
+}: {
+ tickets: Ticket[];
+ addTicket: ({
+ title,
+ description,
+ }: {
+ title: string;
+ description: string;
+ }) => void;
+ addComment: (comment: string, ticketId: number) => void;
+}) {
+ return (
+
+
+
+
+ );
+}
+
+function CommentList({ comments }: { comments: IComment[] }) {
+ return (
+
+ {comments.map((comment) => (
+
+ {comment.comment}
+
+ ))}
+
+ );
+}
+
+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 (
+
+ );
+}
+
+function TicketList({
+ tickets,
+ addComment,
+}: {
+ tickets: Ticket[];
+ addComment: (comment: string, ticketId: number) => void;
+}) {
+ return (
+
+ {tickets.map((ticket) => (
+
+ ))}
+
+ );
+}
+
+function TicketItem({
+ ticket,
+ addComment,
+}: {
+ ticket: Ticket;
+ addComment: (comment: string, ticketId: number) => void;
+}) {
+ const handleClick = () => {
+ ticket.toggle();
+ };
+
+ return (
+
+ {ticket.title}
+ {ticket.description}
+
+
+
+ );
+}
+
+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 (
+
+ );
+}
+
+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(
+
+
+
+
+ );
+}
+
+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();
+ }
+});
diff --git a/ticket.css b/ticket.css
new file mode 100644
index 0000000..d04c556
--- /dev/null
+++ b/ticket.css
@@ -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;
+}