This commit is contained in:
2025-03-22 09:52:08 +07:00
parent c0518b4866
commit ef212024fd
11 changed files with 385 additions and 273 deletions

28
src/api/apiService.ts Normal file
View File

@@ -0,0 +1,28 @@
import { ArticleDetailDataType } from "@/types/article";
import { JobDetailDataType } from "@/types/job";
// Hàm chung để gọi API
const apiRequest = async (endpoint: string, method: string = "GET", body?: object) => {
const response = await fetch(`http://localhost:5000${endpoint}`, {
method,
headers: {
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : null,
});
if (!response.ok) throw new Error(`Error: ${response.statusText}`);
return response.json();
};
// API cho bài viết
export const fetchListArticles = () => apiRequest("/articles");
export const fetchArticleDetail = async (slug: string): Promise<ArticleDetailDataType> => {
return apiRequest(`/articleDetails?path=${slug}`);
};
// API cho công việc
export const fetchListJobs = () => apiRequest("/jobs");
export const fetchJobDetail = async (slug: string): Promise<JobDetailDataType> => {
return apiRequest(`/jobDetails?path=${slug}`);
};

View File

@@ -2,58 +2,55 @@
import { useParams } from "next/navigation";
import { format } from "date-fns";
import { useEffect, useState } from "react";
import { ArticleDetails } from "@/types/article";
import { ArticleDetailDataType } from "@/types/article";
import { fetchArticleDetail } from "@/api/apiService";
const ArticleDetail = () => {
const { slug } = useParams();
const [article, setArticle] = useState<ArticleDetails | null>(null);
const [ArticleDetail, setArticleDetails] =
useState<ArticleDetailDataType | null>(null);
useEffect(() => {
if (slug) {
const fetchArticleDetail = async () => {
const response = await fetch(
`http://localhost:5000/articleDetails?path=${slug}`
);
const data = await response.json();
setArticle(data[0]);
if (typeof slug === "string") {
const getArticleDetail = async () => {
const data = await fetchArticleDetail(slug);
setArticleDetails(data[0]);
};
fetchArticleDetail();
getArticleDetail();
}
}, [slug]);
if (!article) {
return (
<div className="text-center text-2xl py-[50px] font-bold italic">
Article not found.
</div>
);
}
return (
<div className="page-article">
<div className="container">
<div className="content-article-detail">
<div className="time">
{format(new Date(article.last_update * 1000), "dd/MM/yyyy")}
</div>
<h1 className="title-article">{article.title}</h1>
{ArticleDetail ? (
<div className="content-article-detail">
<div className="time">
{format(new Date(ArticleDetail.last_update * 1000), "dd/MM/yyyy")}
</div>
<h1 className="title-article">{ArticleDetail.title}</h1>
<div className="summary">{article.summary}</div>
<div className="summary">{ArticleDetail.summary}</div>
<div className="thumbnail">
<img
src={`https://hurasoft8.hurasoft.com/${article.image.large}`}
width="100%"
height="100%"
alt=""
<div className="thumbnail">
<img
src={`https://hurasoft8.hurasoft.com/${ArticleDetail.image.large}`}
width="100%"
height="100%"
alt=""
/>
</div>
<div
className="content nd"
dangerouslySetInnerHTML={{ __html: ArticleDetail.content }}
/>
</div>
<div
className="content nd"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
</div>
) : (
<div className="h-screen flex justify-center items-center text-2xl font-bold">
Bài viết đang cập nhật...!
</div>
)}
</div>
</div>
);

View File

@@ -2,29 +2,23 @@
import { useEffect, useState } from "react";
import { format } from "date-fns";
import Link from "next/link";
import { ArticlesType } from "@/types/article";
import { ArticleListDataType } from "@/types/article";
import { fetchListArticles } from "@/api/apiService";
const HomeArticle = () => {
const [article, setArticle] = useState<ArticlesType | null>(null);
const [articleList, setArticleList] = useState<ArticleListDataType | null>(
null
);
useEffect(() => {
const fetchArticles = async () => {
const response = await fetch(`http://localhost:5000/articles`);
const data = await response.json();
setArticle(data);
const fetchArticleList = async () => {
const data = await fetchListArticles();
setArticleList(data.list);
};
fetchArticles();
fetchArticleList();
}, []);
if (!article) {
return (
<div className="text-center text-2xl py-[50px] font-bold italic">
Article not found.
</div>
);
}
return (
<div className="page-article">
<div className="container">
@@ -34,106 +28,115 @@ const HomeArticle = () => {
Cập nhật từ Hurasoft
</div>
</div>
<div className="box-big-article flex">
<div className="box-big" id="js-holder-big">
{article.list.slice(0, 1).map((item) => (
<div className="item-article" key={item.id}>
<a href={`/article${item.url}`} className="image-article">
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</a>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
{articleList && Array.isArray(articleList) && articleList.length > 0 ? (
<div>
<div className="box-big-article flex">
<div className="box-big" id="js-holder-big">
{articleList.slice(0, 1).map((item) => (
<div className="item-article" key={item.id}>
<a href={`/article${item.url}`} className="image-article">
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</a>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
</div>
<a
href={`/article${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</a>
</div>
</div>
<a
href={`/article${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</a>
</div>
))}
</div>
))}
</div>
<div className="box-small" id="js-holder-small">
{article.list.slice(1, 3).map((item) => (
<div className="item-article" key={item.id}>
<a href={`/article/${item.url}`} className="image-article">
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</a>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
<div className="box-small" id="js-holder-small">
{articleList.slice(1, 3).map((item) => (
<div className="item-article" key={item.id}>
<a href={`/article/${item.url}`} className="image-article">
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</a>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
</div>
<a
href={`/article/${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</a>
</div>
</div>
<a
href={`/article/${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</a>
</div>
))}
</div>
))}
</div>
</div>
<div className="box-article-other mt-[50px]">
<h2 className="title text-xl font-bold mb-[25px]">
Xem thêm bài khác
</h2>
<div className="list-article flex flex-wrap" id="js-list-article">
{article.list.map((item) => (
<div className="item-article" key={item.id}>
<Link href={`/article/${item.url}`} className="image-article">
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</Link>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
</div>
<div className="box-article-other mt-[50px]">
<h2 className="title text-xl font-bold mb-[25px]">
Xem thêm bài khác
</h2>
<div className="list-article flex flex-wrap" id="js-list-article">
{articleList.map((item) => (
<div className="item-article" key={item.id}>
<Link
href={`/article/${item.url}`}
className="image-article"
>
<img
src={`https://hurasoft8.hurasoft.com/${item.image.large}`}
width={100}
height={100}
alt={item.title}
/>
</Link>
<div className="info">
<div className="time">
<i className="fa-regular fa-clock"></i>{" "}
<span>
{format(
new Date(item.last_update * 1000),
"dd/MM/yyyy HH:mm"
)}
</span>
</div>
<Link
href={`/article/${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</Link>
</div>
</div>
<Link
href={`/article/${item.url}`}
className="name-article line-clamp-2"
>
{item.title}
</Link>
</div>
))}
</div>
))}
</div>
</div>
</div>
) : (
<></>
)}
</div>
</div>
);

View File

@@ -1,5 +1,82 @@
"use client";
import React, { useState } from "react";
interface FormData {
name: string;
email: string;
phone: string;
website: string;
note: string;
}
interface Errors {
name?: string;
email?: string;
phone?: string;
website?: string;
note?: string;
}
// Hàm kiểm tra định dạng email và số điện thoại
const validateEmail = (email: string) =>
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email);
const validatePhone = (phone: string) => /^[0-9]{10,11}$/.test(phone);
const Contact = () => {
const [formData, setFormData] = useState<FormData>({
name: "",
email: "",
phone: "",
website: "",
note: "",
});
const [errors, setErrors] = useState<Errors>({});
// Cập nhật dữ liệu form
const handleChange = (e) =>
setFormData({ ...formData, [e.target.name]: e.target.value });
// Kiểm tra và gửi form
const handleSubmit = async (e) => {
e.preventDefault();
// Kiểm tra form trước khi gửi
const newErrors: Errors = {};
if (!formData.name) newErrors.name = "Tên không được để trống!";
if (!formData.email || !validateEmail(formData.email))
newErrors.email = "Email không hợp lệ!";
if (!formData.phone || !validatePhone(formData.phone))
newErrors.phone = "Số điện thoại không hợp lệ!";
if (!formData.note) newErrors.note = "Nội dung không được để trống!";
setErrors(newErrors);
if (Object.keys(newErrors).length > 0) return;
// Gửi dữ liệu
try {
const response = await fetch("http://localhost:5000/contacts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
if (response.ok) {
alert("Bạn đã gửi liên hệ thành công!");
setFormData({ name: "", email: "", phone: "", website: "", note: "" });
setErrors({});
} else {
alert(
"Liên hệ không thành công. Vui lòng kiểm tra lại thông tin của bạn."
);
}
} catch (error) {
console.error("Error submitting form:", error);
alert("An error occurred. Please try again.");
}
};
return (
<div className="page-contact">
<div className="container">
@@ -9,14 +86,13 @@ const Contact = () => {
<div className="note">
Đi ngũ hỗ trợ của Hurasoft luôn sẵn sàng hỗ trợ quý khách
</div>
<div className="list-info">
<div className="item">
<div className="icon">
<i className="icon_2024 map"></i>
</div>
<div className="txt">
Tầng 5 - Số 3,ngõ 18 Yên Lãng,Đng Đa, Nội
Tầng 5 - Số 3, ngõ 18 Yên Lãng, Đng Đa, Nội
</div>
</div>
<div className="item">
@@ -24,7 +100,7 @@ const Contact = () => {
<i className="icon_2024 phone"></i>
</div>
<div className="txt d-flex align-items">
<a href="02422138068">02422.138.068</a>
<a href="tel:02422138068">02422.138.068</a>
<span>-</span>
<a href="tel:0904580181">0904.580.181</a>
</div>
@@ -42,72 +118,44 @@ const Contact = () => {
<div className="contact-right">
<div className="box-form">
<b>Đ lại lời nhắn</b>
<div className="item-form">
<label>
Tên <span>*</span>
</label>
<input
type="text"
name="name"
className="input-item"
id="js-contact-name"
placeholder="Nhập họ và tên"
/>
<div className="note-error"></div>
</div>
<div className="item-form">
<label>
Email <span>*</span>
</label>
<input
type="text"
name="email"
className="input-item"
id="js-contact-email"
placeholder="hello@example.com..."
/>
<div className="note-error"></div>
</div>
<div className="item-form">
<label>
Số điện thoại <span>*</span>
</label>
<input
type="text"
name="phone"
className="input-item"
id="js-contact-tel"
placeholder="Nhập số điện thoại của bạn"
/>
<div className="note-error"></div>
</div>
<div className="item-form">
<label>website</label>
<input
type="text"
name="web"
className="input-item"
id="js-contact-web"
placeholder="Nhập website của bạn"
/>
<div className="note-error"></div>
</div>
<div className="item-form">
<label>Nội dung</label>
<textarea
name="note"
id="js-contact-content"
className="input-item"
placeholder="Nhập nội dung"
></textarea>
</div>
<a href="javascript:void(0)" className="btn btn-submit">
Gửi thông tin <i className="fa-solid fa-arrow-right-long"></i>
</a>
<form onSubmit={handleSubmit}>
{["name", "email", "phone", "website", "note"].map(
(field, idx) => (
<div className="item-form" key={idx}>
<label>
{field === "note"
? "Nội dung"
: `${
field.charAt(0).toUpperCase() + field.slice(1)
}`}{" "}
<span>{field === "note" ? "" : "*"}</span>
</label>
<input
type={
field === "email"
? "email"
: field === "phone"
? "tel"
: "text"
}
name={field}
className="input-item"
value={formData[field]}
onChange={handleChange}
placeholder={`Nhập ${
field === "note" ? "nội dung" : field
}`}
/>
{errors[field] && (
<div className="note-error">{errors[field]}</div>
)}
</div>
)
)}
<button type="submit" className="btn btn-submit">
Gửi thông tin <i className="fa-solid fa-arrow-right-long"></i>
</button>
</form>
</div>
</div>
</div>

View File

@@ -1,41 +1,38 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { JobDetailType } from "@/types/job";
import { JobDetailDataType } from "@/types/job";
import { fetchJobDetail } from "@/api/apiService";
const JobDetails = () => {
const [job, setJob] = useState<JobDetailType | null>(null);
const [JobDetail, setJobDetail] = useState<JobDetailDataType | null>(null);
const [activeTab, setActiveTab] = useState("#info");
const { slug } = useParams();
const showTab = (tab: string) => {
console.log(`Tab clicked: ${tab}`);
setActiveTab(tab);
};
useEffect(() => {
if (slug) {
const fetchJobDetail = async () => {
const response = await fetch(
`http://localhost:5000/jobDetails?path=${slug}`
);
const data = await response.json();
setJob(data[0]);
if (typeof slug === "string") {
const getJobDetail = async () => {
const data = await fetchJobDetail(slug);
setJobDetail(data[0]);
};
fetchJobDetail();
getJobDetail();
}
}, [slug]);
return (
<div className="page-job detail">
{job ? (
{JobDetail ? (
<div className="container-job">
<h2 className="title">{job.title}</h2>
<h2 className="title">{JobDetail.title}</h2>
<div className="content-job flex">
<div className="left-job">
<div className="item">
<p>Đa điểm</p>
<b>{job.location}</b>
<b>{JobDetail.location}</b>
</div>
<div className="item">
<p>Hình thức làm việc</p>
@@ -43,7 +40,7 @@ const JobDetails = () => {
</div>
<div className="item">
<p>Số lượng tuyển</p>
<b>{job.applicant_count}</b>
<b>{JobDetail.applicant_count}</b>
</div>
</div>
<div className="right-job">
@@ -73,7 +70,9 @@ const JobDetails = () => {
}`}
id="info"
>
<div dangerouslySetInnerHTML={{ __html: job.description }} />
<div
dangerouslySetInnerHTML={{ __html: JobDetail.description }}
/>
<a
href="javascript:void(0)"
onClick={() => showTab("#formjob")}

View File

@@ -1,28 +1,28 @@
"use client";
import Link from "next/link";
import { useState, useEffect } from "react";
import { JobType } from "@/types/job";
import { ListJobDataType } from "@/types/job";
import { fetchListJobs } from "@/api/apiService";
const HomeJob = () => {
const [job, setJob] = useState<JobType | null>(null);
const [ListJob, setListJob] = useState<ListJobDataType | null>(null);
useEffect(() => {
const fetchJob = async () => {
const response = await fetch(`http://localhost:5000/jobs`);
const data = await response.json();
setJob(data);
const getListJob = async () => {
const data = await fetchListJobs();
setListJob(data.list);
};
fetchJob();
getListJob();
}, []);
return (
<div className="page-job list">
<div className="container-job">
<h2 className="title">Tuyển dụng</h2>
{job && job.list && job.list.length > 0 ? (
{ListJob && Array.isArray(ListJob) && ListJob.length > 0 ? (
<div className="list-job">
{job.list.map((item) => (
{ListJob.map((item) => (
<div className="item-job" key={item.id}>
<div className="job-left">
<Link href={`${item.url}`} className="name line-clamp-1">

View File

@@ -4,10 +4,13 @@ import Image from "next/image";
import Link from "next/link";
import { format } from "date-fns";
import { homePageEffect } from "@/effects/homeEffect";
import { ArticlesType } from "@/types/article";
import { ArticleListDataType } from "@/types/article";
import { fetchListArticles } from "@/api/apiService";
export default function Home() {
const [article, setArticle] = useState<ArticlesType | null>(null);
const [articleList, setArticleList] = useState<ArticleListDataType | null>(
null
);
useEffect(() => {
const typingNode = document.getElementById("typewriter") as HTMLElement;
@@ -19,9 +22,8 @@ export default function Home() {
homePageEffect.startCarousel("#navheight", true, 3000);
const fetchArticleNews = async () => {
const response = await fetch(`http://localhost:5000/articles`);
const data = await response.json();
setArticle(data);
const data = await fetchListArticles();
setArticleList(data.list);
};
fetchArticleNews();
@@ -548,13 +550,13 @@ export default function Home() {
</div>
</div>
</div>
{article && article.list && article.list.length > 0 ? (
{articleList && Array.isArray(articleList) && articleList.length > 0 ? (
<div className="box-article">
<div className="container">
<h2 className="title"> mới?</h2>
<div className="content-item-article" id="js-article-new">
{article.list.slice(0, 1).map((item) => (
{articleList.slice(0, 1).map((item) => (
<div className="flex" key={item.id}>
<div className="info">
<div className="tag-blog flex items-center">

View File

@@ -1110,6 +1110,12 @@ main {
.page-contact .item-form {
margin-bottom: 10px;
}
.page-contact .item-form .note-error {
color: red;
margin-top: 5px;
font-style: italic;
}
.page-contact .input-item {
background: #fff;
}

View File

@@ -21,7 +21,7 @@ export interface Article {
url: string;
}
export interface ArticleDetails {
export interface ArticleDetailDataType {
id: number;
title: string;
summary: string;
@@ -35,7 +35,7 @@ export interface ArticleDetails {
last_update: number;
}
export interface ArticlesType {
export interface ArticleListDataType {
total: number;
list: Article[];
}

View File

@@ -3,8 +3,7 @@ export interface JobImage {
large: string; // Đường dẫn ảnh lớn
}
export interface Job {
export interface JobdataType {
id: number;
title: string;
summary: string;
@@ -21,12 +20,12 @@ export interface Job {
}
export interface JobType {
export interface ListJobDataType {
total: number;
list: Job[];
list: JobdataType[];
}
export interface JobDetailType {
export interface JobDetailDataType {
title: string;
path: string;
salary: string;