feat: ...
This commit is contained in:
@@ -35,6 +35,7 @@ RUN npx shadcn add -y button badge card input tabs progress dropdown-menu toolti
|
||||
# ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app
|
||||
# และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next
|
||||
FROM base AS dev
|
||||
RUN apk add --no-cache git openssh-client ca-certificates
|
||||
# สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา)
|
||||
RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache
|
||||
# นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก)
|
||||
|
||||
70
frontend/Dockerfile.dev
Normal file
70
frontend/Dockerfile.dev
Normal file
@@ -0,0 +1,70 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
|
||||
############ Base ############
|
||||
FROM node:20-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache bash curl tzdata \
|
||||
&& ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \
|
||||
&& echo "Asia/Bangkok" > /etc/timezone
|
||||
ARG NEXT_PUBLIC_API_BASE
|
||||
ENV TZ=Asia/Bangkok \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
CHOKIDAR_USEPOLLING=true \
|
||||
WATCHPACK_POLLING=true \
|
||||
NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} \
|
||||
npm_config_yes=true
|
||||
# (ค่าพวกนี้ซ้ำกับ compose ได้ ไม่เป็นปัญหา)
|
||||
|
||||
############ Deps (install) ############
|
||||
FROM base AS deps
|
||||
COPY package.json package-lock.json* ./
|
||||
# ถ้ามี lock ใช้ npm ci; ถ้าไม่มีก็ npm i
|
||||
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm i --no-audit --no-fund; fi
|
||||
|
||||
# เพิ่ม shadcn/ui + tailwind deps
|
||||
RUN npm install -D tailwindcss postcss autoprefixer shadcn@latest \
|
||||
&& npm install class-variance-authority clsx framer-motion lucide-react tailwind-merge tailwindcss-animate
|
||||
|
||||
# init tailwind config (กัน No Tailwind CSS configuration found)
|
||||
RUN npx tailwindcss init -p
|
||||
|
||||
# bake components ของ shadcn แบบ non-interactive
|
||||
RUN npx shadcn add -y button badge card input tabs progress dropdown-menu tooltip switch
|
||||
|
||||
############ Dev (hot-reload) ############
|
||||
# ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app
|
||||
# และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next
|
||||
FROM base AS dev
|
||||
RUN apk add --no-cache git openssh-client ca-certificates
|
||||
# สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา)
|
||||
RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache
|
||||
# นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก)
|
||||
COPY --from=deps /app/node_modules /app/node_modules
|
||||
|
||||
# ไม่กำหนด USER ที่นี่ ปล่อยให้ compose คุม (ตอนนี้คุณใช้ user: "1000:1000")
|
||||
ENV NODE_ENV=development
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
############ Build (production) ############
|
||||
FROM deps AS builder
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
############ Prod runtime (optimized) ############
|
||||
FROM node:20-alpine AS prod
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# RUN apk add --no-cache libc6-compat \
|
||||
# && addgroup -g 1000 node && adduser -D -u 1000 -G node node
|
||||
# คัดเฉพาะของจำเป็น
|
||||
COPY --from=builder /app/package.json /app/package-lock.json* ./
|
||||
# ติดตั้งเฉพาะ prod deps
|
||||
RUN if [ -f package-lock.json ]; then npm ci --omit=dev --no-audit --no-fund; else npm i --omit=dev --no-audit --no-fund; fi
|
||||
COPY --from=builder --chown=node:node /app/.next ./.next
|
||||
COPY --from=builder --chown=node:node /app/public ./public
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
# docker compose -f docker-frontend-build.yml build --no-cache
|
||||
@@ -1,34 +1,74 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { API_BASE } from "@/lib/api";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
|
||||
async function onSubmit(e){
|
||||
e.preventDefault();
|
||||
setErr("");
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) { setErr("เข้าสู่ระบบไม่สำเร็จ"); return; }
|
||||
location.href = "/dashboard";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid place-items-center min-h-screen">
|
||||
<form onSubmit={onSubmit} className="bg-white/90 rounded-2xl p-6 w-full max-w-sm shadow">
|
||||
<div className="text-lg font-semibold mb-4">เข้าสู่ระบบ</div>
|
||||
<input className="w-full border rounded-xl p-2 mb-2" placeholder="อีเมล" value={email} onChange={e=>setEmail(e.target.value)} />
|
||||
<input type="password" className="w-full border rounded-xl p-2 mb-3" placeholder="รหัสผ่าน" value={password} onChange={e=>setPassword(e.target.value)} />
|
||||
{err && <div className="text-red-600 text-sm mb-2">{err}</div>}
|
||||
<button className="w-full rounded-xl p-2 text-white" style={{background:'#0D5C75'}}>เข้าสู่ระบบ</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { API_BASE } from "@/lib/api";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [err, setErr] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
async function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
setErr("");
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setErr(data.error || "เข้าสู่ระบบไม่สำเร็จ");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.token) {
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("refresh_token", data.refresh_token);
|
||||
location.href = "/dashboard";
|
||||
} else {
|
||||
setErr("ไม่ได้รับ Token");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login failed:", error);
|
||||
setErr("เกิดข้อผิดพลาดในการเชื่อมต่อ");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen place-items-center" style={{background: 'linear-gradient(to bottom right, #00c6ff, #0072ff)'}}>
|
||||
<form onSubmit={onSubmit} className="w-full max-w-sm p-8 space-y-4 shadow-lg bg-white/20 backdrop-blur-md rounded-3xl">
|
||||
<div className="text-2xl font-bold text-center text-white">เข้าสู่ระบบ</div>
|
||||
<input
|
||||
disabled={isLoading}
|
||||
className="w-full p-3 text-white placeholder-gray-200 border bg-white/30 border-white/30 rounded-xl focus:outline-none focus:ring-2 focus:ring-white/50 disabled:opacity-50"
|
||||
placeholder="ชื่อผู้ใช้"
|
||||
value={username}
|
||||
onChange={e=>setUsername(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
disabled={isLoading}
|
||||
className="w-full p-3 text-white placeholder-gray-200 border bg-white/30 border-white/30 rounded-xl focus:outline-none focus:ring-2 focus:ring-white/50 disabled:opacity-50"
|
||||
placeholder="รหัสผ่าน"
|
||||
value={password}
|
||||
onChange={e=>setPassword(e.target.value)}
|
||||
/>
|
||||
{err && <div className="text-sm text-center text-yellow-300">{err}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full p-3 font-bold text-white transition-colors duration-300 bg-blue-500 rounded-xl hover:bg-blue-600 disabled:bg-blue-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user