FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-2-hour-2.mdx

Day 2 / Hour 2

Convert Static to Next.js

Move HTML/CSS prototype into TSX and Next.js app structure.

60 minutes
The static prototype renders inside Next.js.

Convert Static to Next.js

Day 2 - ชั่วโมงที่ 2: ย้าย Static Prototype เข้า Next.js

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงที่สองของ Day 2 ผู้เรียนควรสามารถ:

  1. เปลี่ยนโครงสร้าง HTML จาก Day 1 ให้เป็น TSX ได้
  2. ใช้ className, htmlFor และ TSX Comment ได้ถูกต้อง
  3. ย้าย Header, Form, Issue List และ Footer เข้า app/page.tsx ได้
  4. ย้าย CSS เดิมเข้า app/globals.css ได้
  5. ตรวจว่า Static Prototype แสดงผลใน Next.js ได้เหมือนเดิม

ไฟล์ที่ใช้ในชั่วโมงนี้

app/layout.tsx
app/page.tsx
app/globals.css

เปิด Development Server ด้วย npm run dev ระหว่างทำงาน


โครงสร้างเวลา 60 นาที

เวลาหัวข้อรูปแบบ
0-10 นาทีวางแผนการย้าย HTML/CSSอธิบาย
10-20 นาทีรู้จักความต่างระหว่าง HTML กับ TSXทำทีละขั้นตอน
20-40 นาทีย้ายโครงหน้า, Form และ Issue Listทำทีละขั้นตอน
40-50 นาทีย้าย CSS เข้า globals.cssทำทีละขั้นตอน
50-60 นาทีตรวจผลลัพธ์ใน Browserตรวจความพร้อม

Slide 1: ย้ายหน้าเดิมโดยยังไม่เพิ่ม Feature

Hour ที่แล้วเราสร้าง Next.js Project และเปิดหน้าแรกได้แล้ว

Hour นี้เราจะนำ Static Prototype จาก Day 1 เข้ามาอยู่ใน Project ใหม่:

  • Header
  • Form แจ้งปัญหา
  • Issue List และ Status Badge
  • Footer
  • Responsive CSS

ข้อมูลใน Table ยังเป็น Mock Data และ Form ยังไม่บันทึกข้อมูล เป้าหมายตอนนี้คือย้ายหน้าตาเดิมให้ทำงานใน Next.js ก่อน


Slide 2: ไฟล์เดิมจะย้ายไปที่ไหน

จาก Day 1ไปยัง Next.jsสิ่งที่ย้าย
<html> และ <body>app/layout.tsxภาษาและกรอบหลัก
เนื้อหาภายใน <body>app/page.tsxHeader, Main และ Footer
style.cssapp/globals.cssCSS เดิมทั้งหมด

เราไม่ย้าย <!doctype html>, <head> หรือ <link rel="stylesheet"> เข้า page.tsx เพราะ Next.js จัดการ Document และการโหลด CSS ผ่าน layout.tsx


Slide 3: TSX คืออะไร

TSX คือ Syntax ที่ทำให้เราเขียนโครงสร้าง UI คล้าย HTML อยู่ภายใน Code TypeScript ได้

ไฟล์ที่มีทั้ง TypeScript และ UI แบบนี้จึงใช้นามสกุล .tsx เช่น app/page.tsx

const title = "ระบบแจ้งปัญหา IT";
 
export default function Home() {
  return <h1>{title}</h1>;
}
  • <h1>...</h1> คือโครงสร้าง UI ที่มีหน้าตาคล้าย HTML
  • title คือค่าที่สร้างด้วย TypeScript
  • {title} นำค่านั้นมาแสดงภายใน UI

TSX ยังไม่ใช่ HTML ที่ Browser ใช้โดยตรง Next.js และ React จะนำ Code นี้ไปสร้าง Element บนหน้าเว็บให้เรา


Slide 4: กฎสำคัญเมื่อเปลี่ยน HTML เป็น TSX

HTMLTSX
class="form-group"className="form-group"
<label for="title"><label htmlFor="title">
<input required><input required />
rows="5"rows={5}
<!-- เพิ่ม Form -->{/* เพิ่ม Form */}

ชื่ออย่าง id, name, type, required และ aria-labelledby ยังใช้เหมือน HTML

ถ้าพบ Error หลัง Copy Code ให้เริ่มตรวจจากห้าจุดในตารางนี้ก่อน


Slide 5: เปลี่ยนภาษาหลักของหน้าเป็นภาษาไทย

<html lang="en"><html lang="th">

เก็บ Import, Font, Class และ {children} ที่ Next.js สร้างมาไว้เหมือนเดิม

lang="th" ช่วยให้ Browser และ Screen Reader ทราบว่าภาษาหลักของหน้าเป็นภาษาไทย


Slide 6: สร้างโครงหลักของ app/page.tsx

export default function Home() {  return (    <>      <header>        <h1>ระบบแจ้งปัญหา IT</h1>        <p>แจ้งและติดตามปัญหาการใช้งานระบบภายใน</p>      </header>       <main>        <section>          <h2>แจ้งปัญหาใหม่</h2>          {/* เพิ่ม Form ตรงนี้ */}        </section>         <section className="issue-list" aria-labelledby="issue-list-title">          <h2 id="issue-list-title">รายการปัญหาล่าสุด</h2>          <p>ตัวอย่างรายการปัญหาที่ถูกแจ้งเข้ามาในระบบ</p>          <div className="table-wrapper">{/* เพิ่ม Table ตรงนี้ */}</div>        </section>      </main>       <footer>        <p>ฝ่ายเทคโนโลยีสารสนเทศ</p>      </footer>    </>  );}

<>...</> เรียกว่า Fragment ใช้ครอบ Element หลายตัวโดยไม่สร้าง Tag เพิ่มบนหน้าเว็บ


Slide 7: ย้ายข้อมูลผู้แจ้งเข้า Form

<form>  <fieldset>    <legend>ข้อมูลปัญหา</legend>     <div className="form-row">      <div className="form-group">        <label htmlFor="reporterName">ชื่อผู้แจ้ง</label>        <input id="reporterName" name="reporterName" type="text" required />      </div>       <div className="form-group">        <label htmlFor="reporterEmail">อีเมลผู้แจ้ง</label>        <input id="reporterEmail" name="reporterEmail" type="email" required />      </div>    </div>     {/* เพิ่มข้อมูลปัญหาตรงนี้ */}  </fieldset></form>

Form ยังอยู่ใน Component Home เหมือนเดิม เพียงเปลี่ยน class เป็น className และ for เป็น htmlFor


Slide 8: เติมข้อมูลปัญหาและปุ่ม Submit

<div className="form-group">  <label htmlFor="title">หัวข้อปัญหา</label>  <input id="title" name="title" type="text" required /></div> <div className="form-group">  <label htmlFor="description">รายละเอียดปัญหา</label>  <textarea id="description" name="description" rows={5} required /></div> <button type="submit">ส่งข้อมูล</button>

ชื่อ Field ยังคงเป็น reporterName, reporterEmail, title และ description เพื่อให้ตรงกับข้อมูลที่จะสร้างในบทเรียนถัดไป


Slide 9: สร้างโครง Table ใน Issue List

<table>  <thead>    <tr>      <th>รหัส</th>      <th>หัวข้อ</th>      <th>ผู้แจ้ง</th>      <th>สถานะ</th>    </tr>  </thead>  <tbody>{/* เพิ่ม Rows ตรงนี้ */}</tbody></table>

โครงสร้าง Table ยังเหมือน HTML เดิม ส่วนที่เปลี่ยนเป็น TSX ชัดที่สุดคือ Comment ซึ่งต้องเขียนอยู่ภายใน {/* ... */}


Slide 10: เพิ่ม Mock Rows กลับเข้า Table

<tr>  <td>#001</td>  <td>Login เข้าระบบไม่ได้</td>  <td>Anan</td>  <td><span className="status status-open">OPEN</span></td></tr><tr>  <td>#002</td>  <td>ส่งแบบฟอร์มไม่ได้</td>  <td>Mali</td>  <td><span className="status status-progress">IN_PROGRESS</span></td></tr><tr>  <td>#003</td>  <td>ขอสิทธิ์เข้า Dashboard</td>  <td>Kanda</td>  <td><span className="status status-done">DONE</span></td></tr>

แต่ละ Badge ใช้ Class พื้นฐาน status ร่วมกัน และใช้ Class ตัวที่สองกำหนดสีเฉพาะสถานะ


Slide 11: ย้าย CSS เดิมเข้า globals.css

การแทนทั้งไฟล์จะนำ CSS เริ่มต้นและ @import "tailwindcss"; ออก แล้วเหลือ Custom CSS ชุดเดียวสำหรับตรวจผลการย้ายหน้า

ไม่ต้อง Copy <link rel="stylesheet"> เพราะบรรทัดนั้นเป็น HTML และ globals.css ถูก Import จาก layout.tsx อยู่แล้ว


Slide 12: ทำไม Hour นี้ยังไม่ใช้ Tailwind

Project มี Tailwind Package จาก create-next-app อยู่แล้ว แต่บรรทัดนี้เป็นตัวเปิดใช้งาน Tailwind และ Preflight:

@import "tailwindcss";

Preflight คือ Style พื้นฐานที่ Tailwind ใส่ให้กับ Tag ต่าง ๆ ก่อนที่เราจะเริ่มใช้ Utility Class

ใน Hour นี้เรายังใช้ Custom CSS จาก Day 1 ซึ่งมี Selector ของ Tag เช่น main, section, button และ table อยู่แล้ว หากเปิด Tailwind พร้อมกัน ผลลัพธ์อาจเปลี่ยนจาก Cascade ของ CSS สองชุดและทำให้ Debug ยากขึ้น

การลบ @import ไม่ได้ถอน Tailwind ออกจาก Project เพียงยังไม่โหลด Tailwind ในหน้านี้เท่านั้น Day 3 เราจะใส่บรรทัดนี้กลับมา แล้วค่อย ๆ เปลี่ยน Custom CSS เป็น Tailwind Utility Class


Slide 13: Media Query ของ Custom CSS ยังทำงาน

Media Query ที่ Copy มาจาก Day 1 ยังคงอยู่ใน app/globals.css:

@media (min-width: 720px) {
  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
  }
}

เงื่อนไขนี้แบ่ง Layout เป็นสองกรณี:

  • ต่ำกว่า 720px: ชื่อและอีเมลเรียงลงมา 1 คอลัมน์
  • ตั้งแต่ 720px ขึ้นไป: ชื่อและอีเมลเรียงเป็น 2 คอลัมน์

Slide 14: ตรวจหน้าใน Browser

เปิด URL ของ Development Server แล้วตรวจ:

  • Header, Form, Issue List และ Footer แสดงครบ
  • Form มี Field ทั้ง 4 ช่อง
  • คลิก Label แล้ว Focus ไปยัง Field ที่ตรงกัน
  • Table แสดง Mock Data 3 รายการ
  • Status Badge มีทั้งสีและข้อความ
  • เมื่อ Viewport แคบกว่า 720px ช่องชื่อและอีเมลเรียงลงมา 1 คอลัมน์
  • เมื่อ Viewport กว้างตั้งแต่ 720px ช่องชื่อและอีเมลเปลี่ยนเป็น 2 คอลัมน์
  • เมื่อพื้นที่ของ Table แคบกว่า 680px สามารถเลื่อน Table แนวนอนได้
  • Scrollbar แนวนอนอยู่ใต้ Table เท่านั้น และไม่มี Scrollbar ใต้ทั้งหน้า
  • Terminal และ Browser ไม่มี Error

เมื่อกด Submit ข้อมูลยังไม่ถูกเพิ่มใน Table เพราะเรายังไม่ได้เขียน Logic หรือเชื่อม Database


Slide 15: ตอนนี้ Static Prototype อยู่ใน Next.js แล้ว

สิ่งที่เปลี่ยนใน Hour นี้:

HTML structure  → TSX ใน app/page.tsx
Document lang   → app/layout.tsx
CSS เดิม        → app/globals.css

สิ่งที่ยังเหมือนเดิม:

  • Mock Data ยังเขียนอยู่ใน Table
  • Form ยังไม่บันทึกข้อมูล
  • UI ยังไม่ได้แยกเป็น Component
  • Tailwind Package ยังอยู่ใน Project แต่ยังไม่ถูกเปิดใช้งานใน globals.css

Hour ถัดไปเราจะสร้าง TypeScript Data Model และเปลี่ยน Static Rows ให้สร้างจากข้อมูลด้วย .map()