diff --git a/api/src/kesalahan_api.rs b/api/src/kesalahan_api.rs index 2d777ac..4457ba8 100644 --- a/api/src/kesalahan_api.rs +++ b/api/src/kesalahan_api.rs @@ -28,6 +28,10 @@ pub enum KesalahanApi { #[error("DatabaseError: {0}")] KesalahanDatabase(surrealdb::Error), + #[allow(dead_code)] + #[error("Not Found")] + TidakDitemukan, + #[allow(dead_code)] #[error("{0}")] BadRequest(String), @@ -69,6 +73,7 @@ impl ResponseError for KesalahanApi { fn status_code(&self) -> StatusCode { match self { KesalahanApi::KesalahanDatabase(_) => StatusCode::INTERNAL_SERVER_ERROR, + KesalahanApi::TidakDitemukan => StatusCode::NOT_FOUND, KesalahanApi::KesalahanIO(_) => StatusCode::INTERNAL_SERVER_ERROR, KesalahanApi::KesalahanInternal(_) => StatusCode::INTERNAL_SERVER_ERROR, KesalahanApi::BadRequest(_) => StatusCode::BAD_REQUEST, diff --git a/api/src/rute/tulisan.rs b/api/src/rute/tulisan.rs index 42143f9..182680e 100644 --- a/api/src/rute/tulisan.rs +++ b/api/src/rute/tulisan.rs @@ -1,6 +1,6 @@ use crate::{ kesalahan_api::KesalahanApi, - surrealdb::model::tulisan::TulisanDiterbitkan, + surrealdb::model::tulisan::Tulisan, }; use actix_web::{ get, @@ -10,20 +10,35 @@ use actix_web::{ use serde_json::json; use surrealdb::{engine::remote::ws::Client, Surreal}; -#[get("/")] +#[get("/diterbitkan")] pub async fn get_semua_diterbitkan( db: Data> ) -> Result { - let tulisan = TulisanDiterbitkan::get_semua_diterbitkan(&db).await?; + let tulisan = Tulisan::get_semua_diterbitkan(&db).await?; Ok(HttpResponse::Ok().json(json!(tulisan))) } +#[get("/{id}")] +pub async fn get_tulisan( + path: web::Path, + db: Data> +) -> Result { + let id_tulisan = path.into_inner(); + let respon_tulisan = Tulisan::get_tulisan_id(id_tulisan.as_str(), &db).await?; + + match respon_tulisan { + Some(tulisan) => Ok(HttpResponse::Ok().json(json!(tulisan))), + None => Err(KesalahanApi::TidakDitemukan) + } +} + pub fn konfigurasi(konfig: &mut web::ServiceConfig) { konfig .service( - web::scope("/tulisan-diterbitkan") + web::scope("/tulisan") // .service(cek_koneksi_db) .service(get_semua_diterbitkan) + .service(get_tulisan) ); } \ No newline at end of file diff --git a/api/src/surrealdb/model/tulisan.rs b/api/src/surrealdb/model/tulisan.rs index 84d77cf..07fe95b 100644 --- a/api/src/surrealdb/model/tulisan.rs +++ b/api/src/surrealdb/model/tulisan.rs @@ -12,7 +12,7 @@ use crate::kesalahan_api::KesalahanApi; // const NAMA_TABEL : &str= "tulisan"; #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TulisanDiterbitkan { +pub struct Tulisan { pub id: Thing, pub judul: String, pub penulis: String, @@ -57,7 +57,7 @@ impl Into for Kategori { } } -impl Into for TulisanDiterbitkan { +impl Into for Tulisan { fn into(self) -> Value { let mut map = Object::default(); map.insert("id".to_string(), Value::Thing(self.id)); @@ -71,9 +71,9 @@ impl Into for TulisanDiterbitkan { } } -impl TulisanDiterbitkan { +impl Tulisan { /// Kueri dan kembalikan semua tulisan yang sudah diterbitkan dalam database - pub async fn get_semua_diterbitkan(db: &Data>) -> Result, KesalahanApi> { + pub async fn get_semua_diterbitkan(db: &Data>) -> Result, KesalahanApi> { let kueri = "SELECT id, @@ -91,8 +91,31 @@ impl TulisanDiterbitkan { let mut respon = db.query(kueri).await?; - let tulisan = respon.take::>(0)?; + let tulisan = respon.take::>(0)?; + Ok(tulisan) + } + /// Kueri dan kembalikan tulisan dengan spesifik id + pub async fn get_tulisan_id(id_tulisan: &str, db: &Data>) -> Result, KesalahanApi> { + let kueri = format!( + "SELECT + id, + judul, + penulis.nama_tampilan as penulis, + kategori, + konten, + dibuat, + dimodifikasi + FROM tulisan + WHERE id = tulisan:{0} + FETCH akun, kategori", + id_tulisan + ); + + let mut respon = db.query(kueri).await?; + + let tulisan = respon.take::>(0)?; + Ok(tulisan) } } \ No newline at end of file diff --git a/ui/src/fungsi/tulisan.ts b/ui/src/fungsi/tulisan.ts new file mode 100644 index 0000000..291d109 --- /dev/null +++ b/ui/src/fungsi/tulisan.ts @@ -0,0 +1,75 @@ +import { ResponTulisanProps, TulisanProps } from "../props/Tulisan.props"; + +export const fetchTulisanDiterbitkan = async ( + setDaftarTulisan: React.Dispatch> +) => { + try { + const APIEndpoint = import.meta.env.DEV ? import.meta.env.VITE_DEV_API : import.meta.env.VITE_PROD_API; + const APIUrl = `${APIEndpoint}/tulisan/diterbitkan` + const respon = await fetch(`${APIUrl}`, { + method: 'GET' + }) + + // return eraly jika gagal detch + if (!respon.ok) { + throw new Error(`Gagal fetch data dari ${APIUrl}`) + } + + // ubah respon menjadi json dan iterasi ektraksi tulisan + const data: [] = await respon.json(); + let arrayTulisan: TulisanProps[] = []; + if (data.length > 0) { + data.map((tulisan: ResponTulisanProps) => { + const dataTulisan: TulisanProps = { + id: tulisan.id.id.String, + judul: tulisan.judul, + penulis: tulisan.penulis, + kategori: tulisan.kategori, + konten: tulisan.konten, + dibuat: new Date(tulisan.dibuat), + dimodifikasi: new Date(tulisan.dimodifikasi) + }; + arrayTulisan.push(dataTulisan) + }); + }; + setDaftarTulisan(arrayTulisan); + } catch (kesalahan) { + console.error(kesalahan); + } +}; + +export const fetchTulisan = async ( + id: string, + setTulisan: React.Dispatch> +) => { + try { + const APIEndpoint = import.meta.env.DEV ? import.meta.env.VITE_DEV_API : import.meta.env.VITE_PROD_API; + const APIUrl = `${APIEndpoint}/tulisan/${id}` + const respon = await fetch(`${APIUrl}`, { + method: 'GET' + }) + + // return eraly jika gagal detch + if (!respon.ok) { + console.log(respon); + throw new Error(`Gagal fetch data dari ${APIUrl}: ${respon}`) + } + + // ubah respon menjadi json dan set tulisan + const data = await respon.json(); + let tulisan: TulisanProps = { + id: data.id.id.String, + judul: data.judul, + penulis: data.penulis, + kategori: data.kategori, + konten: data.konten, + dibuat: new Date(data.dibuat), + dimodifikasi: new Date(data.dimodifikasi) + }; + + // set stat tulisan + setTulisan(tulisan); + } catch (kesalahan) { + console.log(kesalahan); + } +}; \ No newline at end of file diff --git a/ui/src/halaman/Landing.tsx b/ui/src/halaman/Landing.tsx index c4647c5..2fe3351 100644 --- a/ui/src/halaman/Landing.tsx +++ b/ui/src/halaman/Landing.tsx @@ -1,26 +1,15 @@ -import { Suspense, lazy } from 'react'; - +import BodyLanding from "../komponen/BodyLanding"; import HeaderLanding from "../komponen/HeaderLanding"; -import LayarMemuat from '../komponen/LayarMemuat'; - -const Loadable = (Component: any) => (props: JSX.IntrinsicAttributes) => -( - }> - - -) - -const Body = Loadable(lazy(() => import("../komponen/BodyLanding"))) const Landing = () => { return ( <>
-
+
-
- +
+
diff --git a/ui/src/interface.ts b/ui/src/interface.ts index 942801f..a525530 100644 --- a/ui/src/interface.ts +++ b/ui/src/interface.ts @@ -1,4 +1,15 @@ export interface IRepoData { namaRepo: string, dataRepo: [] +} + +// const interface +export const opsiStringDate: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + hourCycle: "h12", + dayPeriod: "short", + timeZone: "Asia/Jakarta" } \ No newline at end of file diff --git a/ui/src/komponen/DaftarTulisan.tsx b/ui/src/komponen/DaftarTulisan.tsx index b36759e..570d242 100644 --- a/ui/src/komponen/DaftarTulisan.tsx +++ b/ui/src/komponen/DaftarTulisan.tsx @@ -1,50 +1,15 @@ import { useEffect, useState } from "react"; -import { ResponTulisanProps, TulisanProps } from "../props/Tulisan.props"; +import { TulisanProps } from "../props/Tulisan.props"; import TulisanCard from "./TulisanCard"; +import { fetchTulisanDiterbitkan } from "../fungsi/tulisan"; const DaftarTulisan = () => { const [daftarTulisan, setDaftarTulisan] = useState(null) useEffect(() => { - const fetchTulisan = async () => { - try { - const APIEndpoint = import.meta.env.DEV ? import.meta.env.VITE_DEV_API : import.meta.env.VITE_PROD_API; - const APIUrl = `${APIEndpoint}/tulisan-diterbitkan/` - const respon = await fetch(`${APIUrl}`, { - method: 'GET' - }) - - // return eraly jika gagal detch - if (!respon.ok) { - throw new Error(`Gagal fetch data dari ${APIUrl}`) - } - - // ubah respon menjadi json dan iterasi ektraksi tulisan - const data: [] = await respon.json(); - let arrayTulisan: TulisanProps[] = []; - if (data.length > 0) { - data.map((tulisan: ResponTulisanProps) => { - const dataTulisan: TulisanProps = { - id: tulisan.id.id.String, - judul: tulisan.judul, - penulis: tulisan.penulis, - kategori: tulisan.kategori, - konten: tulisan.konten, - dibuat: new Date(tulisan.dibuat), - dimodifikasi: new Date(tulisan.dimodifikasi) - }; - arrayTulisan.push(dataTulisan) - }); - }; - setDaftarTulisan(arrayTulisan); - } catch (kesalahan) { - console.error(kesalahan); - } - }; - - fetchTulisan(); - + fetchTulisanDiterbitkan(setDaftarTulisan); }, []) + return (
{daftarTulisan !== null && daftarTulisan.map(tulisan => { diff --git a/ui/src/komponen/HeaderLanding.tsx b/ui/src/komponen/HeaderLanding.tsx index c8573dc..20ffd2b 100644 --- a/ui/src/komponen/HeaderLanding.tsx +++ b/ui/src/komponen/HeaderLanding.tsx @@ -3,7 +3,7 @@ import HeaderLandingContent from "./HeaderLandingContent"; const HeaderLanding = () => { return ( <> -
+
diff --git a/ui/src/komponen/HeaderLandingContent.tsx b/ui/src/komponen/HeaderLandingContent.tsx index a4c9fbf..b411ac2 100644 --- a/ui/src/komponen/HeaderLandingContent.tsx +++ b/ui/src/komponen/HeaderLandingContent.tsx @@ -14,12 +14,12 @@ const HeaderLandingContent = () => { url: "/" }, { - teks: "archive", - url: "/archive" + teks: "archives", + url: "/archives" }, { - teks: "tags", - url: "/tags" + teks: "categories", + url: "/categories" }, { teks: "cv", diff --git a/ui/src/komponen/KategoriCard.tsx b/ui/src/komponen/KategoriCard.tsx new file mode 100644 index 0000000..0aae6eb --- /dev/null +++ b/ui/src/komponen/KategoriCard.tsx @@ -0,0 +1,24 @@ +import { IconStack2 } from "@tabler/icons-react"; +import { ResponKategoriProps } from "../props/Tulisan.props"; + +const KategoriCard = ({props}: {props:ResponKategoriProps}) => { + const lihatKategori = (id: string) => { + console.log(`Lihat tulisan dengan kategori id kategori:${id}`) + } + + return ( +
lihatKategori(props.id.id.String)} + > +
+ +
+
+ {props.deskripsi} +
+
+ ) +} + +export default KategoriCard; \ No newline at end of file diff --git a/ui/src/komponen/Tulisan.tsx b/ui/src/komponen/Tulisan.tsx index c794c25..eaea94f 100644 --- a/ui/src/komponen/Tulisan.tsx +++ b/ui/src/komponen/Tulisan.tsx @@ -1,54 +1,40 @@ +import { useParams } from "react-router-dom"; import { TulisanProps } from "../props/Tulisan.props"; +import { useEffect, useState } from "react"; +import { fetchTulisan } from "../fungsi/tulisan"; +import { opsiStringDate } from "../interface"; +import KategoriCard from "./KategoriCard"; const Tulisan = () => { - const contohDaftarTulisan: TulisanProps[] = [ - { - id: "1", - judul: "Walking Down The Gradient Descent", - penulis: "Pao", - dibuat: new Date(), - dimodifikasi: new Date(), - kategori: [ - { - id: { - tb: "", - id: { - String: "" - } - }, - deskripsi: "Machine Learning", - warna: "" - }, - { - id: { - tb: "", - id: { - String: "" - } - }, - deskripsi: "Optimization", - warna: "" - }], - konten: "A story of diminishing loss and increasing accuracy in one of my dream a couple of years ago, but, truth be told, this is just a dummy article to test the UI in johanespao.dev. It even hardcoded dude!" + const { idTulisan } = useParams(); + + const [tulisan, setTulisan] = useState(null) + + useEffect(() => { + if (idTulisan !== undefined) { + fetchTulisan(idTulisan, setTulisan); } - ] + }, []) - return ( -
-

- {contohDaftarTulisan[0].judul} -

-

- {contohDaftarTulisan[0].dibuat.toISOString()} -

-

- {contohDaftarTulisan[0].kategori?.[0].deskripsi} -

-

- {contohDaftarTulisan[0].konten} -

-
- ) + return tulisan ? + ( +
+
+ {tulisan.judul} +
+
+ {tulisan.dibuat.toLocaleDateString('en-ID', opsiStringDate)} +
+
+ {tulisan.kategori?.map(itemKategori => ( + + ))} +
+
+ {tulisan.konten} +
+
+ ) : null } export default Tulisan; \ No newline at end of file diff --git a/ui/src/komponen/TulisanCard.tsx b/ui/src/komponen/TulisanCard.tsx index d52e87c..47d31d7 100644 --- a/ui/src/komponen/TulisanCard.tsx +++ b/ui/src/komponen/TulisanCard.tsx @@ -1,5 +1,7 @@ import { useNavigate } from "react-router-dom"; import { TulisanProps } from "../props/Tulisan.props"; +import { opsiStringDate } from "../interface"; +import KategoriCard from "./KategoriCard"; const TulisanCard = ({data}: {data: TulisanProps}) => { @@ -9,19 +11,9 @@ const TulisanCard = ({data}: {data: TulisanProps}) => { return navigasi(`tulisan/${id}`) } - const opsiStringDate: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - hourCycle: "h12", - dayPeriod: "short", - timeZone: "Asia/Jakarta" - } - return ( <> -
+
{
{data.dibuat.toLocaleDateString('en-ID', opsiStringDate)}
-
- {data.penulis} -
- {data.kategori?.map(itemKategori => itemKategori.deskripsi)} + {data.kategori?.map(itemKategori => ( + + ))}
-
+
{data.konten}
diff --git a/ui/src/props/Tulisan.props.ts b/ui/src/props/Tulisan.props.ts index 877f79f..d84d825 100644 --- a/ui/src/props/Tulisan.props.ts +++ b/ui/src/props/Tulisan.props.ts @@ -8,7 +8,7 @@ export interface ResponTulisanProps { dimodifikasi: Date } -interface ResponKategoriProps { +export interface ResponKategoriProps { id: SurrealThingProps, deskripsi: string, warna: string diff --git a/ui/src/rute.tsx b/ui/src/rute.tsx index d2c2bce..0ef6bc6 100644 --- a/ui/src/rute.tsx +++ b/ui/src/rute.tsx @@ -2,8 +2,8 @@ import { Suspense, lazy } from "react"; import LayarMemuat from "./komponen/LayarMemuat"; import type { RouteObject } from "react-router"; -import Tulisan from "./komponen/Tulisan"; -import DaftarTulisan from "./komponen/DaftarTulisan"; +// import Tulisan from "./komponen/Tulisan"; +// import DaftarTulisan from "./komponen/DaftarTulisan"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const Loadable = (Component: any) => (props: JSX.IntrinsicAttributes) => @@ -16,6 +16,9 @@ const Loadable = (Component: any) => (props: JSX.IntrinsicAttributes) => // * LANDING const Landing = Loadable(lazy(() => import("./halaman/Landing"))); const CV = Loadable(lazy(() => import("./halaman/CV"))); +// * TULISAN +const DaftarTulisan = Loadable(lazy(() => import("./komponen/DaftarTulisan"))); +const Tulisan = Loadable(lazy(() => import("./komponen/Tulisan"))); const rute: RouteObject[] = [ {