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 ( +
+

Tickets

+
+ ); +} + +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 ( + + ); +} + +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; +}