From ba5c440a2a6131c2aa623575ba715a627a4eaf57 Mon Sep 17 00:00:00 2001 From: zadit <75159257+insvrgent@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:36:24 +0700 Subject: [PATCH] ok --- src/components/Header.js | 2 +- src/components/ItemLister.js | 23 ++- src/components/ItemLister.module.css | 56 +++++- src/components/Modal.js | 2 +- src/components/PaymentOptions.js | 281 +++++++++++++++----------- src/pages/MaterialList.js | 291 ++++++++------------------- src/pages/MaterialList.module.css | 144 +++++++++++++ 7 files changed, 465 insertions(+), 334 deletions(-) create mode 100644 src/pages/MaterialList.module.css diff --git a/src/components/Header.js b/src/components/Header.js index 43f29f6..39ce762 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -386,7 +386,7 @@ const Header = ({ Desain kafe setModal("edit_tables")}> - Daftar meja + QR kedai dan meja setModal("payment_option")}> Metode pembayaran diff --git a/src/components/ItemLister.js b/src/components/ItemLister.js index c85a78d..8327873 100644 --- a/src/components/ItemLister.js +++ b/src/components/ItemLister.js @@ -67,6 +67,8 @@ const ItemLister = ({ const [itemsToCreate, setItemsToCreate] = useState([]); const [itemsToUpdate, setItemsToUpdate] = useState([]); + const [isFirstStep, setIsFirstStep] = useState(true); + const handlePlusClick = (itemId) => { const updatedItems = items.map((item) => { if (item.itemId === itemId) { @@ -483,6 +485,7 @@ const ItemLister = ({ setItemsToUpdate([]); setIsEditing(false); if (handleUnEdit) handleUnEdit(); + setIsFirstStep(true); }; return ( @@ -495,6 +498,8 @@ const ItemLister = ({ }`} style={{ paddingBottom: isEdit ? "28vh" : "" }} > + + { (isEdit && isFirstStep || !isEdit) &&
{isEdit && } )}
- {isEdit && ( +} + {isEdit && isFirstStep && ( + <>
+ + )} - + { (isEdit && !isFirstStep || !isEdit) && + <> + {isEdit &&
setIsFirstStep(true)}style={{color: 'black', fontSize: '50px', width: '30px'}}>←
+

Daftar item

}
{user && ( user.userId == shopOwnerId || user.cafeId == shopId) && @@ -943,15 +955,18 @@ const ItemLister = ({ user.userId == shopOwnerId && isEdit && ( <> - + */} )} +
+ + } {isEdit && (
diff --git a/src/components/ItemLister.module.css b/src/components/ItemLister.module.css index 841975a..63bc1a7 100644 --- a/src/components/ItemLister.module.css +++ b/src/components/ItemLister.module.css @@ -22,11 +22,63 @@ grid-template-columns: repeat(4, 1fr); gap: 10px; /* padding: 10px; */ - max-height: calc(3 * (25vw - 20px) + 20px); + /* max-height: calc(3 * (25vw - 20px) + 20px); */ overflow-y: auto; height: calc(49vw - 20px); } +@media (min-height: 0px) { + .grid-container { + height: 27vh; + } +} +@media (min-height: 630px) { + .grid-container { + height: 27vh; + } +} +@media (min-height: 636px) { + .grid-container { + height: 29vh; + } +} +@media (min-height: 650px) { + .grid-container { + height: 34vh; + } +} +@media (min-height: 705px) { + .grid-container { + height: 37vh; + } +} +@media (min-height: 735px) { + .grid-container { + height: 38vh; + } +} +@media (min-height: 759px) { + .grid-container { + height: 40vh; + } +} +@media (min-height: 819px) { + .grid-container { + height: 44vh; + } +} + +@media (min-height: 830px) { + .grid-container { + height: 47vh; + } +} +@media (min-height: 892px) { + .grid-container { + height: 49vh; + } +} + .title-container { display: flex; align-items: center; @@ -124,7 +176,6 @@ font-style: normal; font-size: 1.5em; padding: 10px 0; - margin-bottom: 17px; } .OptionContainer { display: flex; @@ -136,7 +187,6 @@ font-style: normal; font-size: 0.9em; padding: 10px 0; - margin-bottom: 17px; } .PayButton { font-family: "Plus Jakarta Sans", sans-serif; diff --git a/src/components/Modal.js b/src/components/Modal.js index 5df7ac0..4212ddd 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -85,7 +85,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, setModal, handleMove {modalContent === "transaction_end" && } {modalContent === "transaction_failed" && } {modalContent === "payment_option" && ( - + )} {modalContent === "add_material" && ( diff --git a/src/components/PaymentOptions.js b/src/components/PaymentOptions.js index d1f766a..0c7c21c 100644 --- a/src/components/PaymentOptions.js +++ b/src/components/PaymentOptions.js @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import jsQR from "jsqr"; import { getImageUrl } from "../helpers/itemHelper"; +import { getTables, updateTable, createTable } from "../helpers/tableHelper"; import { getCafe, saveCafeDetails, @@ -8,158 +9,195 @@ import { } from "../helpers/cafeHelpers"; import Switch from "react-switch"; // Import the Switch component -const SetPaymentQr = ({ shopId }) => { - const [qrPosition, setQrPosition] = useState([50, 50]); - const [qrSize, setQrSize] = useState(50); - const [qrPayment, setQrPayment] = useState(); - const [qrCodeDetected, setQrCodeDetected] = useState(false); - const [isNeedConfirmationState, setIsNeedConfirmationState] = useState(0); - const qrPaymentInputRef = useRef(null); - const qrCodeContainerRef = useRef(null); - const [cafe, setCafe] = useState({}); +const SetPaymentQr = ({ shop }) => { + const [initialPos, setInitialPos] = useState({ + left: shop.xposition, + top: shop.yposition, + }); + const [initialSize, setInitialSize] = useState(shop.scale); + const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground)); + const qrBackgroundInputRef = useRef(null); + const shopUrl = window.location.hostname + "/" + shop.cafeId; + + const generateQRCodeUrl = (tableCode) => { + if (tableCode != null) { + return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( + shopUrl + "/" + tableCode + )}`; + } else { + return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( + shopUrl + )}`; + } + }; + + const [isConfig, setIsConfig] = useState(false); + const [isViewTables, setIsViewTables] = useState(false); + + const [tables, setTables] = useState([]); useEffect(() => { - const fetchCafe = async () => { + const fetchData = async () => { try { - const response = await getCafe(shopId); - setCafe(response); - setQrPayment(getImageUrl(response.qrPayment)); - console.log(response.needsConfirmation); - - setIsNeedConfirmationState(response.needsConfirmation === true ? 1 : 0); // Set state based on fetched data - setQrPosition([response.xposition, response.yposition]); - setQrSize(response.scale); + console.log(shop); + const fetchedTables = await getTables(shop.cafeId); + setTables(fetchedTables); } catch (error) { - console.error("Error fetching cafe:", error); + console.error("Error fetching tables:", error); } }; - fetchCafe(); - }, [shopId]); - // Detect QR code when qrPayment updates - useEffect(() => { - if (qrPayment) { - detectQRCodeFromContainer(); - } - }, [qrPayment]); + fetchData(); + }, [shop.cafeId]); + + const handleSave = () => { + const qrBackgroundFile = qrBackgroundInputRef.current.files[0]; // Get the selected file for qrBackground + + // Prepare the details object + const details = { + qrSize: initialSize, + qrPosition: initialPos, + qrBackgroundFile, // Include qrBackground file + }; + + // Call saveCafeDetails function with the updated details object + saveCafeDetails(shop.cafeId, details) + .then((response) => { + console.log("Cafe details saved:", response); + }) + .catch((error) => { + console.error("Error saving cafe details:", error); + }); + }; + + + const handlePositionChange = (e) => { + const { name, value } = e.target; + setInitialPos((prevPosition) => ({ + ...prevPosition, + [name]: parseFloat(value).toFixed(2), + })); + }; + + const handleSizeChange = (e) => { + setInitialSize(parseFloat(e.target.value).toFixed(2)); + }; + - // Handle file input change const handleFileChange = (e) => { const file = e.target.files[0]; if (file) { - const newqrPayment = URL.createObjectURL(file); - setQrPayment(newqrPayment); + const newBgImage = URL.createObjectURL(file); // Create a temporary URL for display + setBgImageUrl(newBgImage); } }; - // Detect QR code from the container - const detectQRCodeFromContainer = () => { - const container = qrCodeContainerRef.current; - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - const img = new Image(); - img.crossOrigin = "Anonymous"; - img.onload = () => { - canvas.width = container.offsetWidth; - canvas.height = container.offsetHeight; - context.drawImage(img, 0, 0, canvas.width, canvas.height); - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - const qrCode = jsQR(imageData.data, canvas.width, canvas.height); - setQrCodeDetected(!!qrCode); - if (qrCode) { - console.log("QR Code detected:", qrCode.data); - } - }; - img.src = qrPayment; - }; - - // Save cafe details - const handleSave = async () => { - const qrPaymentFile = qrPaymentInputRef.current.files[0]; - const details = { - qrPosition, - qrSize, - qrPaymentFile, - }; - - try { - const response = await saveCafeDetails(cafe.cafeId, details); - console.log("Cafe details saved:", response); - } catch (error) { - console.error("Error saving cafe details:", error); - } - - try { - const response = await setConfirmationStatus(cafe.cafeId, isNeedConfirmationState === 1); - setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving - } catch (error) { - console.error(error); - setIsNeedConfirmationState(cafe.needsConfirmation ? 1 : 0); // Fallback to initial value - } - }; - - // Handle Switch toggle - const handleChange = (checked) => { - setIsNeedConfirmationState(checked ? 1 : 0); // Toggle state based on the switch - }; - - const handleError = () => { - setQrPayment('https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU'); // Set your fallback image here - }; - return (

Konfigurasi pembayaran

+
qrPaymentInputRef.current.click()} style={{ + background: `center center / contain no-repeat url(${bgImageUrl})`, + backgroundSize: "contain", + border: "1px solid #ddd", + ...styles.qrCodeContainer, - backgroundImage: `url(${qrPayment})`, + backgroundImage: `url(${bgImageUrl})`, backgroundPosition: "center", backgroundRepeat: "no-repeat", backgroundSize: "contain", }} > QR Payment + src={generateQRCodeUrl(shopUrl)} + alt="QR Code" + style={{ + position: "absolute", + width: `${initialSize}%`, + left: `${initialPos.left}%`, + top: `${initialPos.top}%`, + transform: "translate(-50%, -50%)", + }} />
-

Klik untuk unggah QRIS

+

Klik untuk ganti background

- {qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ?

QR terdeteksi

:

Tidak ada qr terdeteksi

} - {qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ?
qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti
:
qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah
} +

setIsConfig(!isConfig)}> {isConfig ? '˅' : '˃'} Konfigurasi QR

+
qrBackgroundInputRef.current.click()} style={styles.uploadButton}>Ganti
-

Pengecekan ketersediaan ganda

-

- Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar. + {isConfig && <> +

+ +
+
+ +
+
+ +
+ } +

setIsViewTables(!isViewTables)}> + {isViewTables ? '˅' : '˃'} Daftar meja

-
@@ -174,6 +212,7 @@ const SetPaymentQr = ({ shopId }) => { // Styles const styles = { container: { + width: '100%', backgroundColor: "white", padding: "20px", borderRadius: "8px", @@ -230,13 +269,25 @@ const styles = { transition: "background-color 0.3s", }, switchContainer: { - marginTop: "20px", textAlign: "left", }, description: { margin: "10px 0", - fontSize: "14px", - color: "#666", + }, + sliderContainer: { + marginBottom: "20px", + }, + label: { + display: "block", + marginBottom: "10px", + }, + sliderWrapper: { + display: "flex", + alignItems: "center", + }, + input: { + flex: "1", + margin: "0 10px", }, }; diff --git a/src/pages/MaterialList.js b/src/pages/MaterialList.js index 87b4933..eb98f7e 100644 --- a/src/pages/MaterialList.js +++ b/src/pages/MaterialList.js @@ -19,8 +19,10 @@ import { import Switch from "react-switch"; // Import the Switch component import Carousel from '../components/Carousel' +import styles from './MaterialList.module.css'; // Import the CSS Module const SetPaymentQr = ({ cafeId }) => { + // All your state and logic goes here (unchanged) const [materials, setMaterials] = useState([]); const [mutations, setMutations] = useState([]); const [newMaterialName, setNewMaterialName] = useState(""); @@ -34,6 +36,7 @@ const SetPaymentQr = ({ cafeId }) => { const [latestMutation, setLatestMutation] = useState([]); const [currentQuantity, setCurrentQuantity] = useState(-1); const [currentPrice, setCurrentPrice] = useState(0); + const [newPrice, setNewPrice] = useState(0); const [quantityChange, setQuantityChange] = useState(0); const [sortOrder, setSortOrder] = useState("desc"); const [isEditCurrentPrice, setIsEditCurrentPrice] = useState(false); @@ -53,7 +56,7 @@ const SetPaymentQr = ({ cafeId }) => { const handleChange = (e) => { const formattedValue = formatCurrency(e.target.value); - setCurrentPrice(formattedValue); + setNewPrice(formattedValue); }; useEffect(() => { @@ -87,11 +90,7 @@ const SetPaymentQr = ({ cafeId }) => { fetchMutations(); }, [cafeId]); - const handleSortChange = (e) => { - setSortOrder(e.target.value); - }; - - const filteredMutations = mutations.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId) || []; + const filteredMutations = mutations.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId) || []; const sortedMutations = filteredMutations .filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId) @@ -156,32 +155,11 @@ const SetPaymentQr = ({ cafeId }) => { } }; - const handlePrevious = () => { - if (selectedMaterialIndex) { - setQuantityChange(0); - const currentIndex = materials.findIndex( - (material) => material.materialId === selectedMaterialIndex - ); - if (currentIndex > 0) { - setSelectedMaterialIndex(materials[currentIndex - 1].materialId); - } - } - }; - - const handleNext = () => { - if (selectedMaterialIndex) { - setQuantityChange(0); - const currentIndex = materials.findIndex( - (material) => material.materialId === selectedMaterialIndex - ); - if (currentIndex < materials.length - 1) { - setSelectedMaterialIndex(materials[currentIndex + 1].materialId); - } - } - }; - const handleQuantityChange = (change) => { setQuantityChange((prev) => prev + change); + if (quantityChange + change < 1) setNewPrice(currentPrice); + + setIsEditCurrentPrice(false); }; useEffect(() => { @@ -202,24 +180,27 @@ const SetPaymentQr = ({ cafeId }) => { setLatestMutation(latestMutation); setCurrentQuantity(latestMutation.newStock); setCurrentPrice(formatCurrency(latestMutation.priceAtp)); + setNewPrice(formatCurrency(latestMutation.priceAtp)); } else { setCurrentQuantity(0); // Default value if no mutations exist setLatestMutation({ newStock: 0 }); setCurrentPrice(0); + setNewPrice(0); } } + setIsViewingHistory(false); }, [materials, mutations, selectedMaterialIndex]); const handleUpdateStock = async () => { setLoading(true); try { - const newPrice = convertToInteger(currentPrice) + const newprice = convertToInteger(newPrice) const newStock = currentQuantity + quantityChange; const formData = new FormData(); formData.append("newStock", newStock); - formData.append("priceAtp", newPrice); + formData.append("priceAtp", newprice); formData.append("reason", "Stock update"); await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData); @@ -244,103 +225,106 @@ const SetPaymentQr = ({ cafeId }) => { return date.toLocaleString(); }; - return ( -
- {loading ? +
+ {loading ? ( + <> + ) : ( <> - - : - <> -

Bahan baku

+

Bahan baku

setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} /> - {selectedMaterialIndex != -1 ? + {selectedMaterialIndex !== -1 ? ( <> -
+

Stok sekarang {currentQuantity}

-
-

{currentQuantity + quantityChange}

-
-
+

harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang

-
- +
-
quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} style={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
+
quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}> + {isEditCurrentPrice ? 'Terapkan' : 'Ganti'} +
-
-
-
setIsViewingHistory(!isViewingHistory)} style={styles.historyTab}> -

{isViewingHistory ? '˅' : '˃'} Riwayat stok

-
- {selectedMaterialIndex != -1 && isViewingHistory && !loading && ( -
- {sortedMutations.length > 0 ? ( - sortedMutations.map((mutation) => ( -
-

- {formatDate(mutation.createdAt)} -

-

Details: {mutation.reason}

-

stok {mutation.newStock}

-
- )) - ) : ( -

No mutations available.

- )} +
+

setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok

+ {selectedMaterialIndex !== -1 && isViewingHistory && !loading && ( + <> +
setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}> + Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"}
<>
- )} -
+
+
+ {sortedMutations.length > 0 ? ( + sortedMutations.map((mutation) => ( +
+
+ + + + + + + + + +
+
+

{formatDate(mutation.createdAt)}

+

Total stok: {mutation.newStock} || +{mutation.newStock - mutation.oldStock} || {formatCurrency((mutation.newStock - mutation.oldStock) * mutation.priceAtp)}

+
+
+ )) + ) : ( +

Tidak ada laporan perubahan stok.

+ )} +
+
+ + )}
- : + + ) : ( <> -
+
-
+
Fitur ini mempermudah mengelola biaya dan memantau pengeluaran bahan.
-
+

Buat bahan baru

-
- +
setNewMaterialName(event.target.value)} placeholder="Masukkan nama barang" + style={{width: '100%', height: '31px'}} />
@@ -348,7 +332,8 @@ const SetPaymentQr = ({ cafeId }) => { id="materialUnit" value={newMaterialUnit} onChange={(e) => setNewMaterialUnit(e.target.value)} - style={styles.unit} + className={styles.unit} + style={{height: '37px'}} > @@ -362,131 +347,17 @@ const SetPaymentQr = ({ cafeId }) => { -
-
- } + )} - } + )}
); }; -// Styles -const styles = { - container: { - width: '100%', - minHeight: '50vh', - backgroundColor: "white", - padding: "20px", - borderRadius: "8px", - boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", - textAlign: "center", // Center text and children - }, - title: { - marginBottom: "20px", - fontWeight: "bold", - }, - qrCodeContainer: { - backgroundColor: '#999999', - borderRadius: '20px', - position: "relative", - width: "100%", - height: "200px", - backgroundSize: "contain", - overflow: "hidden", - margin: "0 auto", // Center the QR code container - }, - uploadMessage: { - fontWeight: 600, - textAlign: "left", - }, - changeButtonEnabled: { - paddingRight: '10px', - backgroundColor: 'green', - borderRadius: '30px', - color: 'white', - fontWeight: 700, - height: '36px', - lineHeight: '36px', - paddingLeft: '10px', - paddingHeight: '10px', - }, - changeButtonDisabled: { - paddingRight: '10px', - backgroundColor: '#a1a1a1', - borderRadius: '30px', - color: 'white', - fontWeight: 700, - height: '36px', - lineHeight: '36px', - paddingLeft: '10px', - paddingHeight: '10px', - }, - resultMessage: { - marginTop: "-13px", - textAlign: "left", - display: 'flex', - justifyContent: 'space-between' - }, - stokContainer: { - display: 'flex', - justifyContent: 'space-evenly', - alignItems: 'center', - marginTop: '18px', - marginBottom: "6px", - textAlign: "left", - }, - buttonContainer: { - marginTop: "11px", - textAlign: "left", - }, - stockButton: { - padding: "10px 20px", - fontSize: "3.5vw", - backgroundColor: "#28a745", - color: "#fff", - border: "none", - borderRadius: "30px", - cursor: "pointer", - transition: "background-color 0.3s", - }, - saveButton: { - width: '100%', - padding: "10px 20px", - fontSize: "3.5vw", - backgroundColor: "#28a745", - color: "#fff", - border: "none", - borderRadius: "30px", - cursor: "pointer", - transition: "background-color 0.3s", - }, - switchContainer: { - marginTop: "20px", - textAlign: "left", - }, - historyTab: { - textAlign: "left", - }, - historyContainer: { - textAlign: "left", - maxHeight: '200px', - overflowY: 'auto' - }, - description: { - margin: "10px 0", - fontSize: "14px", - color: "#666", - }, - unit: { - marginTop: '11px', - width: '100%', - height: '31px', - }, -}; - export default SetPaymentQr; diff --git a/src/pages/MaterialList.module.css b/src/pages/MaterialList.module.css new file mode 100644 index 0000000..87f5196 --- /dev/null +++ b/src/pages/MaterialList.module.css @@ -0,0 +1,144 @@ +/* SetPaymentQr.module.css */ + +.container { + width: 100%; + min-height: 47vh; + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + text-align: center; + } + + .title { + margin-bottom: 20px; + font-weight: bold; + } + + .uploadMessage { + font-weight: 600; + text-align: left; + } + + .changeButtonEnabled { + padding-right: 10px; + background-color: green; + border-radius: 30px; + color: white; + font-weight: 700; + height: 36px; + line-height: 36px; + padding-left: 10px; + padding-height: 10px; + } + + .changeButtonDisabled { + padding-right: 10px; + background-color: #a1a1a1; + border-radius: 30px; + color: white; + font-weight: 700; + height: 36px; + line-height: 36px; + padding-left: 10px; + padding-height: 10px; + } + + .resultMessage { + margin-top: -13px; + text-align: left; + display: flex; + justify-content: space-between; + } + + .stokContainer { + display: flex; + justify-content: space-evenly; + align-items: center; + margin-top: -20px; + margin-bottom: -15px; + text-align: left; + } + + .buttonContainer { + margin-top: 11px; + text-align: left; + } + + .stockButton { + padding: 10px 20px; + font-size: 3.5vw; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 30px; + cursor: pointer; + transition: background-color 0.3s; + } + + .saveButton { + width: 100%; + padding: 10px 20px; + font-size: 3.5vw; + background-color: #28a745; + color: #fff; + border: none; + border-radius: 30px; + cursor: pointer; + transition: background-color 0.3s; + } + + .switchContainer { + margin-top: 20px; + text-align: left; + } + + .historyTab { + text-align: left; + } + + .historyContainer { + text-align: left; + max-height: 15vh; + overflow-y: auto; + } + + .description { + display: flex; + margin: 10px 0; + font-size: 14px; + color: #666; + } + + .unit { + margin-top: 11px; + width: 100%; + height: 31px; + } + + .sorter { + border: 1px solid #c3c3c3; + padding: 5px; + border-radius: 10px; + display: flex; + justify-content: space-between; + margin-top: -10px; + margin-bottom: 10px; + } + + .mutationCard { + display: flex; + margin-bottom: 7px; + margin-top: 7px; + } + + .mutationTitle { + margin-left: 5px; + } + .mutationTitle h4 { + margin: 0; + } + + .mutationTitle p { + margin: 0; + } \ No newline at end of file