-
-
-
- Agenda Donor Darah
+ )}
+
+
+
+
+
+ {isFetchingRiwayatAcaraDonor ? (
+
+
+ Riwayat Pengajuan Acara Donor Darah
- {statusAgenda === "loading" ? (
+
- ) : statusAgenda === "error" ? (
-
- ) : (
-
- {!agendaDonor ? (
-
- Belum ada agenda donor. Yuk daftar di{" "}
- sini
-
- ) : (
- <>
-
- Tanggal :
- {moment(agendaDonor.time_start).format("D MMMM YYYY")}
-
-
- Kecamatan :
- {agendaDonor.kecamatan}
-
-
- Lokasi :
- {agendaDonor.location}
-
-
- Jam :
- {moment(agendaDonor.time_start).format("HH:mm") +
- "-" +
- moment(agendaDonor.time_end).format("HH:mm")}
-
-
- Kuota :
- {agendaDonor.quota}
-
-
- Kategori :
- {agendaDonor.category === "public"
- ? "Umum"
- : "Tertutup"}
-
- >
- )}
+
+
+ ) : statusRiwayatAcaraDonor === "error" ? (
+
+
+ Riwayat Pengajuan Acara Donor Darah
+
+
+
+
+
+ ) : (
+
+ {dataRiwayatAcaraDonor.data.count === 0 ? (
+
+
+ Riwayat Pengajuan Acara Donor Darah
+
+
+
+ Belum ada riwayat pengajuan acara donor darah.
+
+
+ ) : (
+ <>
+
+ Riwayat Pengajuan Acara Donor Darah
+
+
+
+
+ Alamat |
+ Waktu |
+ Status |
+
+
+
+ {dataRiwayatAcaraDonor.data.results.map(
+ riwayatAcaraDonor => {
+ return (
+
+
+ {riwayatAcaraDonor.alamat_lokasi_donor}
+ |
+
+ {moment(riwayatAcaraDonor.waktu_mulai).format(
+ "D MMMM YYYY,"
+ )}
+
+ {moment(riwayatAcaraDonor.waktu_mulai).format(
+ "(HH:mm "
+ )}
+ {moment(
+ riwayatAcaraDonor.waktu_berakhir
+ ).format("- HH:mm)")}{" "}
+ |
+
+ {riwayatAcaraDonor.status == null ? (
+ Diajukan
+ ) : riwayatAcaraDonor.status ? (
+ Diterima
+ ) : (
+ Ditolak
+ )}
+ |
+
+ )
+ }
+ )}
+
+
+
+ -
+ pageRiwayatAcaraDonor > 1 &&
+ setPageRiwayatAcaraDonor(curPage => curPage - 1)
+ }
+ >
+
+ «
+
+ Halaman sebelumnya riwayat acara donor
+
+
+
+ {paginationRiwayatAcaraDonor.map(val => (
+ - setPageRiwayatAcaraDonor(val)}
+ key={val}
+ >
+
+ {val}
+
+ {`Halaman ${val} riwayat acara donor`}
+
+
+
+ ))}
+ -
+ pageRiwayatAcaraDonor < pageCountRiwayatAcaraDonor &&
+ setPageRiwayatAcaraDonor(curPage => curPage + 1)
+ }
+ >
+
+ »
+
+ Halaman selanjutnya riwayat acara donor
+
+
+
+
+ >
)}
-
-
-
-
-
-
-
-
-
- Riwayat Donor Darah
-
- {isFetchingRiwayatDonor ? (
+ )}
+
+
+
+ {isFetchingRiwayatDonor ? (
+
+
Riwayat Donor Darah
+
- ) : statusRiwayatDonor === "error" ? (
+
+
+ ) : statusRiwayatDonor === "error" ? (
+
+
Riwayat Donor Darah
+
+
+
+ ) : (
+
+ {dataRiwayatDonor.data.count === 0 ? (
+
+
Riwayat Donor Darah
+
+
+ Belum ada riwayat donor darah.
+
+
+
) : (
-
- {dataRiwayatDonor.data.count === 0 ? (
-
- Belum ada riwayat donor.
-
- ) : (
- <>
-
-
-
-
- Lokasi |
- Tanggal |
-
-
-
- {dataRiwayatDonor.data.results.map(
- riwayatDonor => {
- return (
-
- {riwayatDonor.location} |
-
- {moment(riwayatDonor.time_start).format(
- "D MMMM YYYY"
- )}
- |
-
- )
- }
- )}
-
-
-
-
- -
- pageRiwayatDonor > 1 &&
- setPageRiwayatDonor(curPage => curPage - 1)
- }
- >
-
- «
-
- Halaman sebelumnya riwayat donor
-
-
-
- {paginationRiwayatDonor.map(val => (
- - setPageRiwayatDonor(val)}
- key={val}
- >
-
- {val}
-
- {`Halaman ${val} riwayat donor`}
-
-
-
- ))}
- -
- pageRiwayatDonor < pageCountRiwayatDonor &&
- setPageRiwayatDonor(curPage => curPage + 1)
- }
- >
-
- »
-
- Halaman selanjutnya riwayat donor
-
+ <>
+
Riwayat Donor Darah
+
+
+
+ Lokasi |
+ Tanggal |
+
+
+
+ {dataRiwayatDonor.data.results.map(riwayatDonor => {
+ return (
+
+
+ {riwayatDonor.location}
+ |
+
+ {moment(riwayatDonor.time_start).format(
+ "D MMMM YYYY"
+ )}
+ |
+
+ )
+ })}
+
+
+
+ -
+ pageRiwayatDonor > 1 &&
+ setPageRiwayatDonor(curPage => curPage - 1)
+ }
+ >
+
+ «
+
+ Halaman sebelumnya riwayat donor
+
+
+
+ {paginationRiwayatDonor.map(val => (
+ - setPageRiwayatDonor(val)}
+ key={val}
+ >
+
+ {val}
+
+ {`Halaman ${val} riwayat donor`}
-
-
- >
- )}
-
+
+
+ ))}
+
+ pageRiwayatDonor < pageCountRiwayatDonor &&
+ setPageRiwayatDonor(curPage => curPage + 1)
+ }
+ >
+
+ »
+
+ Halaman selanjutnya riwayat donor
+
+
+
+
+ >
)}
-
-
-
+ )}
+
+
)
}
-export default withAuthenticated(Profile)
+
+export default withAuthenticatedOrRedirect(Profile, "/")
diff --git a/frontend/src/pages/profile.test.js b/frontend/src/pages/profile.test.js
index ffd001392608b4ab7685bd83cbf36da052e94a9f..7f0b40d5db93380508e2ba0c8bee31688c2f8f5e 100644
--- a/frontend/src/pages/profile.test.js
+++ b/frontend/src/pages/profile.test.js
@@ -1,11 +1,19 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"
import React from "react"
-import { getAgendaDonor, getRiwayatDonor, getUserProfile } from "../api"
+import {
+ getAgendaDonor,
+ getRiwayatAcaraDonor,
+ getRiwayatDonor,
+ getUserProfile,
+ getFormulirDaftarDonor,
+} from "../api"
import { renderAuthenticated } from "../utils/test-util"
+import { acaraDonorFactory } from "./acara-donor.factory"
import { jadwalDonorFactory } from "./jadwal-donor.factory"
import ProfilePage from "./profile"
getAgendaDonor.mockResolvedValue({ data: [] })
getUserProfile.mockResolvedValue({ data: { profile: {} } })
+getRiwayatAcaraDonor.mockResolvedValue({ data: { count: 0, results: [] } })
getRiwayatDonor.mockResolvedValue({ data: { count: 0, results: [] } })
describe(`Kartu Donor`, () => {
@@ -77,12 +85,12 @@ describe("Agenda Donor", () => {
})
renderAuthenticated(
)
await screen.findByText(/Beji/i)
- expect(screen.getByText(/Beji/i)).toBeInTheDocument()
- expect(screen.getByText(/Lokasi\s*:\s*D'Mall/i)).toBeInTheDocument()
- expect(screen.getByText(/Kecamatan\s*:\s*Beji/i)).toBeInTheDocument()
- expect(screen.getByText(/Tanggal\s*:\s*2 Maret 2020/i)).toBeInTheDocument()
- expect(screen.getByText(/Kuota\s*:\s*150/i)).toBeInTheDocument()
- expect(screen.getByText(/Kategori\s*:\s*Umum/i)).toBeInTheDocument()
+ expect(screen.getByText(/: Beji/i)).toBeInTheDocument()
+ expect(screen.getByText(/: D'Mall/i)).toBeInTheDocument()
+ expect(screen.getByText(/: Beji/i)).toBeInTheDocument()
+ expect(screen.getByText(/: 2 Maret 2020/i)).toBeInTheDocument()
+ expect(screen.getByText(/: 150/i)).toBeInTheDocument()
+ expect(screen.getByText(/: Umum/i)).toBeInTheDocument()
await waitFor(() =>
expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
)
@@ -97,13 +105,61 @@ describe("Agenda Donor", () => {
renderAuthenticated(
)
expect(await screen.findByText(/Coba lagi/i)).toBeInTheDocument()
fireEvent.click(screen.getByText(/Coba lagi/i))
- expect(
- await screen.findByText(/Kategori\s*:\s*Tertutup/i)
- ).toBeInTheDocument()
+ expect(await screen.findByText(/: Tertutup/i)).toBeInTheDocument()
await waitFor(() =>
expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
)
})
+
+ it("can print formulir", async () => {
+ getAgendaDonor.mockResolvedValueOnce({
+ data: [
+ {
+ id: 1,
+ kecamatan: "Beji",
+ location: "D'Mall",
+ time_start: "2020-03-02T10:00:00+07:00",
+ time_end: "2020-03-02T15:00:00+07:00",
+ quota: 150,
+ category: "public",
+ },
+ ],
+ })
+ const printablePdfUrl = "https://printable.pdf"
+ getFormulirDaftarDonor.mockResolvedValueOnce({
+ data: {
+ url: printablePdfUrl,
+ },
+ })
+ window.open = jest.fn()
+ renderAuthenticated(
)
+ fireEvent.click(await screen.findByText("Cetak Formulir"))
+ await waitFor(() =>
+ expect(window.open).toHaveBeenCalledWith(printablePdfUrl, "_blank")
+ )
+ })
+
+ it("can alert when print formulir fails", async () => {
+ getAgendaDonor.mockResolvedValueOnce({
+ data: [
+ {
+ id: 1,
+ kecamatan: "Beji",
+ location: "D'Mall",
+ time_start: "2020-03-02T10:00:00+07:00",
+ time_end: "2020-03-02T15:00:00+07:00",
+ quota: 150,
+ category: "public",
+ },
+ ],
+ })
+ const errorMessage = "Network error"
+ getFormulirDaftarDonor.mockRejectedValueOnce(new Error(errorMessage))
+ window.alert = jest.fn()
+ renderAuthenticated(
)
+ fireEvent.click(await screen.findByText("Cetak Formulir"))
+ await waitFor(() => expect(window.alert).toHaveBeenCalledWith(errorMessage))
+ })
})
describe("Riwayat Donor", () => {
@@ -212,3 +268,146 @@ describe("Riwayat Donor", () => {
)
})
})
+
+describe("Riwayat Acara Donor", () => {
+ it("shows descriptive message when there is no riwayat acara donor", async () => {
+ getRiwayatAcaraDonor.mockResolvedValueOnce({
+ data: {
+ count: 0,
+ results: [],
+ },
+ })
+ renderAuthenticated(
)
+ expect(
+ await screen.findByText(/Belum ada riwayat pengajuan acara donor darah./i)
+ ).toBeInTheDocument()
+ await waitFor(() =>
+ expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
+ )
+ })
+
+ it("shows riwayat acara donor on success", async () => {
+ getRiwayatAcaraDonor.mockResolvedValue({
+ data: {
+ count: 1,
+ results: [
+ {
+ id: 1,
+ status: false,
+ alamat_institusi: "Pacilkom",
+ alamat_lokasi_donor: "Sekre Pacil",
+ email_kantor: "pacil@cs.ui.ac.id",
+ email_koor: "pacilia@gmail.com",
+ kategori: "Terbuka",
+ kecamatan: "Beji",
+ keterangan: "",
+ nama_institusi: "Pacilkom",
+ nama_koor: "dr. Pacilia",
+ no_telp_kantor: "08165342342",
+ no_telp_koor: "08167021743",
+ perkiraan_jumlah_donor: 455,
+ waktu_berakhir: "2020-06-02T12:00:00+07:00",
+ waktu_mulai: "2020-06-02T10:00:00+07:00",
+ },
+ ],
+ },
+ })
+ renderAuthenticated(
)
+ await screen.findByText("Alamat")
+ expect(screen.getByText("Sekre Pacil"))
+ expect(screen.getByText("Ditolak"))
+ await waitFor(() =>
+ expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
+ )
+ })
+
+ it("shows correct status for acara donor", async () => {
+ const acara_donor1 = acaraDonorFactory({
+ status: null,
+ })
+ const acara_donor2 = acaraDonorFactory({
+ status: false,
+ })
+ const acara_donor3 = acaraDonorFactory({
+ status: true,
+ })
+ getRiwayatAcaraDonor.mockResolvedValueOnce({
+ data: {
+ count: 3,
+ results: [acara_donor1, acara_donor2, acara_donor3],
+ },
+ })
+ renderAuthenticated(
)
+ await screen.findByText("Alamat")
+ expect(screen.getByText("Diajukan")).toBeInTheDocument()
+ expect(screen.getByText("Ditolak")).toBeInTheDocument()
+ expect(screen.getByText("Diterima")).toBeInTheDocument()
+ })
+
+ it("has clickable pagination", async () => {
+ const acara_donor1 = acaraDonorFactory({
+ alamat_lokasi_donor: "Yuli Pacil",
+ })
+ const acara_donor4 = acaraDonorFactory({
+ alamat_lokasi_donor: "Belyos Pacil",
+ })
+ getRiwayatAcaraDonor
+ .mockResolvedValueOnce({
+ data: {
+ count: 4,
+ results: [acara_donor1, acaraDonorFactory(), acaraDonorFactory()],
+ },
+ })
+ .mockResolvedValueOnce({
+ data: {
+ count: 4,
+ results: [acara_donor4],
+ },
+ })
+ .mockResolvedValueOnce({
+ data: {
+ count: 4,
+ results: [acara_donor1, acaraDonorFactory(), acaraDonorFactory()],
+ },
+ })
+ .mockResolvedValueOnce({
+ data: {
+ count: 4,
+ results: [acara_donor4],
+ },
+ })
+ renderAuthenticated(
)
+ expect(await screen.findByText("Yuli Pacil")).toBeInTheDocument()
+ fireEvent.click(screen.getByText("Halaman 2 riwayat acara donor"))
+ expect(await screen.findByText("Belyos Pacil")).toBeInTheDocument()
+ fireEvent.click(screen.getByText("Halaman sebelumnya riwayat acara donor"))
+ expect(await screen.findByText("Yuli Pacil")).toBeInTheDocument()
+ fireEvent.click(screen.getByText("Halaman selanjutnya riwayat acara donor"))
+ expect(await screen.findByText("Belyos Pacil")).toBeInTheDocument()
+ await waitFor(() =>
+ expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
+ )
+ })
+
+ it("shows retry button on error and can be retried", async () => {
+ getRiwayatAcaraDonor
+ .mockRejectedValueOnce(new Error("Network error"))
+ .mockResolvedValueOnce({
+ data: {
+ count: 1,
+ results: [
+ acaraDonorFactory({
+ alamat_lokasi_donor: "Yuli Pacil",
+ }),
+ ],
+ },
+ })
+ renderAuthenticated(
)
+ expect(await screen.findByText(/Coba lagi/i)).toBeInTheDocument()
+ fireEvent.click(screen.getByText(/Coba lagi/i))
+ expect(await screen.findByText("Yuli Pacil")).toBeInTheDocument()
+ await waitFor(() =>
+ expect(screen.queryByText("Loading...")).not.toBeInTheDocument()
+ )
+ })
+})
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index d44c73034cb1af4b484d9bdc8e799f16da8f4aad..bd84db4bd9576df6043e684285471bfc4382a600 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -20,16 +20,25 @@ body {
background-color: var(--cream);
}
+.MuiOutlinedInput-input {
+ padding: 10px 14px !important;
+}
+.MuiInputBase-input:hover {
+ text-shadow: 0.7px 0 0 currentColor;
+}
+.MuiInputBase-input:focus {
+ text-shadow: 0.7px 0 0 currentColor;
+}
+
.form-control {
color: var(--blue);
background-color: var(--white);
border-color: var(--red) !important;
- box-shadow: 0 0 2px 1px var(--red) !important;
}
.form-control:hover {
color: var(--blue);
- text-shadow: 0 0 0.65px var(--blue), 0 0 0.65px var(--blue);
+ text-shadow: 0.7px 0 0 currentColor;
background-color: var(--white);
border-color: var(--dark-red) !important;
box-shadow: 0 0 2px 1px var(--dark-red) !important;
@@ -37,12 +46,16 @@ body {
.form-control:focus {
color: var(--blue);
- text-shadow: 0 0 0.65px var(--blue), 0 0 0.65px var(--blue);
+ text-shadow: 0.7px 0 0 currentColor;
background-color: var(--white);
border-color: var(--dark-red) !important;
box-shadow: 4px 4px var(--dark-red) !important;
}
+input {
+ color: var(--blue) !important;
+}
+
.text-red {
color: var(--red);
}
@@ -193,6 +206,17 @@ body {
box-shadow: var(--bottom-shadow);
}
+.tooltip {
+ font-weight: bold;
+}
+.arrow::before {
+ border-right-color: var(--red) !important;
+}
+.tooltip-inner {
+ color: var(--cream);
+ background-color: var(--red);
+}
+
@media (max-width: 1024px) {
.form-control {
font-size: 15px;
@@ -234,6 +258,21 @@ body {
content: "⚠ ";
}
+.thumbnail {
+ position: relative;
+}
+
+.caption {
+ position: absolute;
+ top: 75%;
+ left: 0;
+ width: 100%;
+}
+
#title {
text-align: center;
}
+
+.unstyled-link:hover {
+ text-decoration: none;
+}
diff --git a/frontend/src/styles/theme.js b/frontend/src/styles/theme.js
new file mode 100644
index 0000000000000000000000000000000000000000..8f233759e2134d9ceb99aa0c2a452269948e41fe
--- /dev/null
+++ b/frontend/src/styles/theme.js
@@ -0,0 +1,75 @@
+import { createMuiTheme } from "@material-ui/core/styles"
+
+const RED = "#a91320"
+const DARK_RED = "#8a1324"
+const BLUE = "#376c8b"
+const DARK_BLUE = "#285e9b"
+const CREAM = "#fff3df"
+const DARK_CREAM = "#f9e5cc"
+const ERROR_RED = "#ff0000"
+
+const theme = createMuiTheme({
+ palette: {
+ primary: {
+ main: RED,
+ dark: DARK_RED,
+ contrastText: CREAM,
+ },
+ secondary: {
+ main: BLUE,
+ dark: DARK_BLUE,
+ contrastText: CREAM,
+ },
+ error: {
+ main: ERROR_RED,
+ },
+ },
+ overrides: {
+ MuiIconButton: {
+ root: {
+ color: BLUE,
+ },
+ },
+ MuiOutlinedInput: {
+ root: {
+ position: "relative",
+ "& $notchedOutline": {
+ borderColor: RED,
+ },
+ "&:hover:not($disabled):not($focused):not($error) $notchedOutline": {
+ borderColor: DARK_RED,
+ boxShadow: "0 0 2px 1px #8a1324 !important",
+ // Reset on touch devices, it doesn't add specificity
+ "#media (hover: none)": {
+ borderColor: DARK_RED,
+ boxShadow: "0 0 2px 1px #8a1324 !important",
+ },
+ },
+ "&$focused $notchedOutline": {
+ borderColor: DARK_RED,
+ boxShadow: "4px 4px #8a1324 !important",
+ },
+ },
+ },
+ MuiPickersBasePicker: {
+ pickerView: {
+ backgroundColor: CREAM,
+ },
+ pickerViewLandscape: {
+ backgroundColor: CREAM,
+ },
+ },
+ MuiPickersCalendarHeader: {
+ iconButton: {
+ backgroundColor: DARK_CREAM,
+ },
+ },
+ MuiPickersClock: {
+ clock: {
+ backgroundColor: DARK_CREAM,
+ },
+ },
+ },
+})
+
+export default theme
diff --git a/frontend/src/utils/test-util.js b/frontend/src/utils/test-util.js
index cf602b4ea210bc21941e43ab708c0782a60c4748..7e6319ba22d3774d45919601d21eaeb9dca9a626 100644
--- a/frontend/src/utils/test-util.js
+++ b/frontend/src/utils/test-util.js
@@ -1,3 +1,8 @@
+import {
+ createHistory,
+ createMemorySource,
+ LocationProvider,
+} from "@reach/router"
import { render as renderTestingLibrary } from "@testing-library/react"
import React from "react"
import { AuthProvider } from "../hooks/authenticate"
@@ -10,3 +15,13 @@ export const renderAuthenticated = component =>
renderTestingLibrary(
{component}
)
+
+export const renderWithRouter = (
+ ui,
+ { route = "/", history = createHistory(createMemorySource(route)) } = {}
+) => {
+ return {
+ ...render(
{ui}),
+ history,
+ }
+}
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 8870c541da68da352a3cf479e7b3a7aa7c33420a..e73b20907ae4e43d4f68fdb0212fb0f308c718db 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -21,7 +21,7 @@ http {
server {
listen 8000 default_server;
listen [::]:8000 default_server;
- server_name localhost;
+ server_name ppl2020c6.cs.ui.ac.id;
charset utf-8;
root /var/www/html;
index index.html;
@@ -29,10 +29,16 @@ http {
location /api/static {
try_files $uri $uri/ $uri/index.html;
}
+
+ location /api/media {
+ }
location /api {
proxy_pass http://api;
proxy_set_header SCRIPT_NAME /api;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $server_name;
}
location / {
diff --git a/nginx/nginx.dev.conf b/nginx/nginx.dev.conf
index 4eee5985ad1ab59e95dddbf77f9aa6680bc24eca..38c415d0e9aae31010cfc405b6e4232e00fcafba 100644
--- a/nginx/nginx.dev.conf
+++ b/nginx/nginx.dev.conf
@@ -21,7 +21,7 @@ http {
server {
listen 8000 default_server;
listen [::]:8000 default_server;
- server_name localhost;
+ server_name ppl2020c6.cs.ui.ac.id;
charset utf-8;
root /var/www/html;
index index.html;
@@ -30,9 +30,15 @@ http {
try_files $uri $uri/ $uri/index.html;
}
+ location /staging/api/media {
+ }
+
location /staging/api {
proxy_pass http://api;
proxy_set_header SCRIPT_NAME /staging/api;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $server_name;
}
location /staging {