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

Day 3 / Hour 4

Mock CRUD and Database Prep

Mock update, close concept, filters, database transition, and Server Actions preview.

60 minutes
Learners understand the CRUD flow before Supabase.

Mock CRUD and Database Prep

Day 3 - ชั่วโมงที่ 4: เปลี่ยนสถานะในข้อมูลชั่วคราว

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

หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:

  1. อธิบาย Create, Read และ Update ในโปรเจกต์ปัจจุบันได้
  2. เปลี่ยน Status ของ Issue ใน State ได้
  3. เปลี่ยนข้อมูลใน Array และ Object โดยสร้างชุดใหม่แทนการแก้ชุดเดิม
  4. ส่งฟังก์ชันจาก Component ที่ครอบอยู่ไปให้ Component ลูกเรียกใช้
  5. ทำให้ IssueList ใช้ได้ทั้งแบบอ่านอย่างเดียวและแบบแก้สถานะได้
  6. อธิบายข้อจำกัดของข้อมูลชั่วคราว ก่อนเชื่อมฐานข้อมูลได้

ไฟล์ที่แก้

components/IssueBoard.tsx
components/IssueList.tsx

Slide 1: เพิ่มการเปลี่ยนสถานะต่อจาก Hour 3

หน้า Home สร้าง Issue ใหม่ได้แล้ว โดย IssueBoard เก็บ items ใน State หรือข้อมูลที่ Component จำไว้ แล้วส่งให้ IssueList

ชั่วโมงนี้จะให้ List ส่ง id และสถานะที่เลือกกลับไปให้ Board เปลี่ยน State

  • Create (เพิ่ม): เพิ่มรายการจากฟอร์มได้แล้ว
  • Read (อ่าน): แสดงรายการจาก State ได้แล้ว
  • Update (แก้ไข): กำลังจะเพิ่มการเปลี่ยนสถานะ
  • Delete (ลบ): ไม่เพิ่มการลบใน Project นี้ ใช้ DONE เพื่อระบุว่างานเสร็จแล้ว

ทั้งสี่คำเรียกรวมกันว่า CRUD ส่วนข้อมูลยังอยู่ชั่วคราวใน Browser


Slide 2: เปลี่ยน State ด้วยข้อมูลชุดใหม่

ตัวอย่างอธิบายที่ไม่ควรใช้ ไม่ต้องนำไปแก้ Project:

issue.status = "DONE";
setItems(items);

การส่ง Array เดิมกลับเข้า State อาจทำให้ React ข้ามการคำนวณหน้าจอใหม่ เพราะยังอ้างถึงข้อมูลก้อนเดิม แม้ค่าภายในจะเปลี่ยนแล้ว

วิธีที่เราจะใช้:

  1. .map() สร้าง Array ใหม่จากรายการเดิม
  2. ถ้า id ตรง ให้สร้าง Object ใหม่ด้วย { ...issue, status }
  3. ถ้า id ไม่ตรง ให้คืน Object เดิม

เรียกว่า Immutable Update คือสร้างข้อมูลชุดใหม่โดยไม่แก้ต้นฉบับ


Slide 3: เพิ่มฟังก์ชันเปลี่ยนสถานะใน Board

import type { Issue, IssueStatus } from "@/types/issue";

เพิ่ม Function นี้ภายใน IssueBoard หลัง handleCreateIssue และก่อน return:

function handleUpdateStatus(id: string, status: IssueStatus) {  setItems((currentItems) =>    currentItems.map((issue) =>      issue.id === id ? { ...issue, status } : issue    )  );}

id ระบุรายการที่จะเปลี่ยน ส่วน status คือค่าใหม่; เก็บ State และ Handler สร้างรายการเดิมไว้


Slide 4: อ่านการอัปเดตทีละขั้น

ตัวอย่างอธิบายจาก Handler ที่เพิ่งเพิ่ม:

issue.id === id ? { ...issue, status } : issue
  • เงื่อนไข ? ค่าถ้าจริง : ค่าถ้าเท็จ เลือกว่าจะคืนค่าใด
  • ...issue คัดลอกข้อมูลทุกช่องของ Object เดิม เช่นหัวข้อและผู้แจ้ง
  • status ที่เขียนตามหลังแทนเฉพาะสถานะด้วยค่าใหม่

setItems ส่งรายการล่าสุดให้ currentItems และ .map() คืน Array ใหม่ โดยจำนวนรายการเท่าเดิม

เมื่อกดเปลี่ยนสถานะ จึงควรเปลี่ยนเพียงป้ายสถานะของรายการที่เลือก ไม่สร้างแถวเพิ่ม


Slide 5: List เดียวรองรับสองหน้า

IssueList ถูกใช้สองจุด และต้องรองรับทั้งคู่:

จุดที่เรียกใช้พฤติกรรมที่ต้องการ
Home ผ่าน IssueBoardแสดงปุ่มและเปลี่ยน State ได้
/issues ผ่าน app/issues/page.tsxแสดงข้อมูลอย่างเดียว

เราจะเพิ่ม Callback ชื่อ onUpdateStatus ให้ List เรียกเมื่อกดปุ่ม โดยเป็น Optional Prop หรือค่าที่จะส่งมาก็ได้ ไม่ส่งก็ได้ ใช้ ? เหมือนฟอร์มใน Hour 3

ส่ง Callback มา → แสดงคอลัมน์จัดการ; ไม่ส่งมา → ซ่อนทั้งหัวคอลัมน์และช่องปุ่ม

หน้า /issues จึงยังใช้ <IssueList issues={issues} /> เดิมได้


Slide 6: ประกาศ Callback ของ IssueList

"use client";

แก้ Type Import และ Props เดิม โดยคง Import อื่นไว้:

import type { Issue, IssueStatus } from "@/types/issue"; type IssueListProps = {  issues: Issue[];  onUpdateStatus?: (id: string, status: IssueStatus) => void;};

List กำลังจะรับการคลิกผ่าน onClick และหน้า /issues ที่ทำงานฝั่ง Server ยังนำไปใช้โดยตรง จึงใส่ "use client" ที่ไฟล์นี้ด้วย


Slide 7: รับ Callback และเตรียมสถานะ

const statuses: IssueStatus[] = ["OPEN", "IN_PROGRESS", "DONE"];

แก้เฉพาะบรรทัดประกาศ Function เดิมเป็น:

export function IssueList({ issues, onUpdateStatus }: IssueListProps) {

เก็บเงื่อนไข Empty State และ return ของตารางภายใน Function ไว้ทั้งหมด

Array statuses จะใช้สร้างปุ่มสามปุ่ม โดยแต่ละปุ่มส่งสถานะของตัวเองให้ Callback


Slide 8: เพิ่มหัวคอลัมน์เมื่อมี Callback

<th className="px-3 py-3 font-semibold">รายละเอียด</th>{onUpdateStatus && (  <th className="px-3 py-3 font-semibold">จัดการ</th>)}

บรรทัด “รายละเอียด” เป็นจุดอ้างอิงเดิม ไม่เพิ่มซ้ำ

onUpdateStatus && (...) แสดงหัวคอลัมน์เมื่อมี Callback เท่านั้น ตอนนี้ยังไม่เห็นคอลัมน์ เพราะ Board ยังไม่ได้ส่ง Callback


Slide 9: เตรียมช่องปุ่มในแต่ละแถว

{onUpdateStatus && (  <td className="px-3 py-4">    <div className="flex flex-wrap gap-2">      {/* เพิ่มปุ่มสถานะตรงนี้ในขั้นถัดไป */}    </div>  </td>)}

เมื่อมี Callback จึงสร้างช่องจัดการให้ตรงกับหัวคอลัมน์; เก็บช่องรายละเอียดและลิงก์เดิมไว้

Comment เป็นจุดวางปุ่มในขั้นถัดไป ส่วน flex-wrap จะช่วยให้ปุ่มขึ้นบรรทัดใหม่ได้


Slide 10: สร้างปุ่มจากรายการสถานะ

{statuses.map((status) => (  <button    key={status} type="button"    disabled={issue.status === status}    onClick={() => onUpdateStatus(issue.id, status)}  >    {status}  </button>))}

ตัวอย่างเริ่มที่ .map() เมื่อวางจริงให้เยื้องเข้าไปภายใน <div> ของช่องปุ่ม

onClick เรียก Callback ตอนคลิก; disabled ปิดปุ่มสถานะปัจจุบัน และ type="button" ระบุว่าไม่ใช่ปุ่มส่ง Form


Slide 11: ตกแต่งปุ่มสถานะ

<button  key={status} type="button"  disabled={issue.status === status}  onClick={() => onUpdateStatus(issue.id, status)}  className="rounded-md border border-slate-300 px-2 py-1 text-xs    font-semibold text-slate-700 hover:bg-slate-50    focus-visible:outline-2 focus-visible:outline-teal-700    disabled:cursor-not-allowed disabled:bg-slate-100 disabled:text-slate-400">

แสดงเฉพาะ Tag เปิด ให้เก็บ {status}, </button> และโครง .map() เดิมไว้

disabled:* ทำให้ปุ่มที่กดไม่ได้ดูจางลง ส่วน focus-visible:* ช่วยเห็นตำแหน่งเมื่อใช้แป้นพิมพ์


Slide 12: เพิ่มพื้นที่ให้คอลัมน์จัดการ

<table className="w-full min-w-[920px] border-collapse text-sm">

เปลี่ยนจาก min-w-[720px] เป็น min-w-[920px] โดยเก็บเนื้อหาและ Tag ปิดเดิม

กล่องที่ครอบตารางต้องยังมี overflow-x-auto เพื่อให้เลื่อนเฉพาะตารางบนจอเล็ก

ค่าขั้นต่ำนี้ใช้ทั้ง Home และ /issues เพราะใช้ตารางเดียวกัน แม้ /issues ไม่มีคอลัมน์จัดการ


Slide 13: ต่อ Callback จาก Board

<IssueList  issues={items}  onUpdateStatus={handleUpdateStatus}/>

เก็บ <IssueForm onCreateIssue={handleCreateIssue} /> เดิมไว้

หน้า Home ควรเห็นคอลัมน์จัดการแล้ว: คลิกปุ่ม → List ส่ง id และ status → Board เปลี่ยน items → List แสดงป้ายสถานะใหม่

app/issues/page.tsx ไม่ต้องแก้ เพราะยังแสดงข้อมูลอย่างเดียว


Slide 14: ลองเปลี่ยนสถานะที่ Home

  1. เลือกรายการที่เป็น OPEN แล้วกด IN_PROGRESS: ป้ายสถานะต้องเป็นสีเหลือง
  2. กด DONE: ป้ายสถานะต้องเป็นสีเขียว และปุ่ม DONE กดไม่ได้
  3. ตรวจว่าจำนวนแถวและข้อมูลของรายการอื่นไม่เปลี่ยน
  4. เพิ่มรายการใหม่จาก Form แล้วลองเปลี่ยนสถานะของรายการใหม่นั้น
  5. กด Tab ไปยังปุ่มสถานะ ต้องเห็นเส้นรอบปุ่มที่ถูกเลือก และใช้ Enter กดได้

ตัวอย่างนี้เลือกกลับไปสถานะอื่นได้ ยังไม่ได้จำกัดลำดับการเปลี่ยนสถานะ


Slide 15: ตรวจหน้าอ่านอย่างเดียวและจอเล็ก

  • /issues ต้องมีห้าคอลัมน์เดิม ไม่มีหัวข้อ “จัดการ” และไม่มีปุ่มสถานะ
  • Link รายละเอียดของ Mock Data ต้องยังเปิดได้
  • Empty State เดิมยังอยู่ก่อน return หลักและไม่ขึ้นกับ Callback
  • ที่ 375px และ 1024px ตารางที่กว้างเกินพื้นที่ต้องเลื่อนภายในกล่อง ไม่ดันทั้งหน้า
  • Console และ Terminal ต้องไม่มี Error

สถานะที่เปลี่ยนใน Home ยังไม่แสดงใน /issues เพราะหน้านี้อ่าน Mock Data เดิม


Slide 16: จาก State ชั่วคราวไปสู่ Database

ตอนนี้ Home เพิ่มรายการและเปลี่ยนสถานะได้ แต่ข้อมูลยังอยู่ใน State ของ IssueBoard

ตอนนี้หลังเชื่อม Database ใน Day 4
Refresh แล้วการเพิ่มและแก้หายบันทึกแล้วอ่านกลับได้
หน้าอื่นอ่านข้อมูลตัวอย่างเดิมทุกหน้าอ่านแหล่งข้อมูลเดียวกัน
เปลี่ยนเฉพาะใน Browser นี้Browser อื่นอ่านค่าที่บันทึกแล้วได้

ขั้นต่อไปคือสร้างโปรเจกต์บน Supabase และตารางในฐานข้อมูล ก่อนย้ายการอ่านและบันทึกไปฝั่ง Server

Server ยังต้องตรวจข้อมูลและตรวจว่าใครมีสิทธิ์แก้ไข การซ่อนปุ่มในหน้าเว็บอย่างเดียวไม่ได้ป้องกันการแก้ข้อมูล