This commit is contained in:
zadit
2025-01-25 00:17:06 +07:00
parent 469d786d49
commit a1b7d0b844
13 changed files with 542 additions and 226 deletions

View File

@@ -35,7 +35,7 @@
flex-grow: 1; flex-grow: 1;
text-align: center; text-align: center;
padding: 10px; padding: 10px;
border-radius: 10px 0 0 0; /* border-radius: 10px 0 0 0; */
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
font-weight: 600; font-weight: 600;
} }
@@ -44,7 +44,7 @@
flex-grow: 1; flex-grow: 1;
text-align: center; text-align: center;
padding: 10px; padding: 10px;
border-radius: 0 10px 0 0; /* border-radius: 0 10px 0 0; */
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
font-weight: 600; font-weight: 600;
} }
@@ -56,11 +56,11 @@
.dateSelectorInactive { .dateSelectorInactive {
color: transparent; color: transparent;
border-color: transparent; /* border-color: transparent; */
} }
.chartWrapper { .chartWrapper {
border: 1px solid rgb(179, 177, 177); /* border: 1px solid rgb(179, 177, 177);
border-radius: 0 0 11px 11px; border-radius: 0 0 11px 11px; */
} }

View File

@@ -3,10 +3,13 @@
display: flex; display: flex;
border: 2px solid #ccc; border: 2px solid #ccc;
height: 50%; height: 50%;
max-height: 140px;
background-color: #f8f8f8; background-color: #f8f8f8;
border-radius: 8px; border-radius: 8px;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
align-items: center; align-items: center;
position: relative;
margin-bottom: 5px;
} }
/* Left side (with the rotated code and dotted line) */ /* Left side (with the rotated code and dotted line) */
@@ -22,7 +25,7 @@
.coupon-code { .coupon-code {
writing-mode: vertical-rl; writing-mode: vertical-rl;
font-size: 18px; font-size: 4vw;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin: 0; margin: 0;
@@ -42,7 +45,7 @@
flex-grow: 1; flex-grow: 1;
} }
.coupon-value { .coupon-value {
font-size: clamp(18px, 3vw, 24px); /* Minimum 18px, 6vw (responsive), Maximum 24px */ font-size: 4vw; /* Minimum 18px, 6vw (responsive), Maximum 24px */
font-weight: bold; font-weight: bold;
color: #2c3e50; color: #2c3e50;
text-align: left; text-align: left;
@@ -50,7 +53,34 @@
.coupon-period, .coupon-period,
.coupon-expiration { .coupon-expiration {
font-size: 14px; font-size: 3vw;
color: #7f8c8d; color: #7f8c8d;
} }
.RibbonBannerInverted {
pointer-events: none;
position: absolute;
top: -38px;
width: 200px;
height: 200px;
right: -35px;
transform: scale(-0.8,0.8);
}
.RibbonBannerInverted img {
object-fit: contain;
width: 100%;
height: auto;
}
.RibbonBannerInverted h1 {
margin: 0; /* Remove default margin */
font-size: 20px; /* Adjust font size as needed */
transform: rotate(-44.7deg)scale(-1,1); /* Rotate the text */
transform-origin: center; /* Rotate around its center */
white-space: nowrap; /* Prevent text wrapping */
position: absolute;
top: 68px;
left: -9px;
}

View File

@@ -3,22 +3,65 @@ import './Coupon.css'; // Import a CSS file for styling
const Coupon = ({ code, value, period, type, expiration }) => { const Coupon = ({ code, value, period, type, expiration }) => {
// Format the value based on type // Format the value based on type
const formattedValue = type === 'fixed' ? `Rp ${value}` : value != 0 ? `${value}%` : 'kupon berlangganan'; const formattedValue = type == 'fixed' ? `Rp ${value}` : value != 0 ? `${value}%` : 'kupon berlangganan';
// Function to convert expiration to Indonesian date format (dd MMMM yyyy)
const formatExpirationDate = (dateString) => {
const date = new Date(dateString);
// Options for Indonesian date format
const options = {
weekday: 'long', // 'Monday'
year: 'numeric', // '2025'
month: 'long', // 'Januari'
day: 'numeric' // '11'
};
// Format the date to Indonesian locale (ID)
return date.toLocaleDateString('id-ID', options);
};
// Function to calculate the difference in days between expiration and current date (adjusted to UTC)
const calculateDaysLeft = (expirationDate) => {
const currentDate = new Date();
const expiration = new Date(expirationDate);
// Convert both dates to UTC
const utcCurrentDate = new Date(currentDate.toISOString()); // Ensure it's in UTC
const utcExpirationDate = new Date(expiration.toISOString()); // Ensure it's in UTC
// Calculate the time difference in milliseconds
const timeDifference = utcExpirationDate - utcCurrentDate;
const daysLeft = Math.ceil(timeDifference / (1000 * 3600 * 24)); // Convert to days
return daysLeft;
};
const daysLeft = expiration ? calculateDaysLeft(expiration) : null;
return ( return (
<div className='coupon'> <div className='coupon'>
{daysLeft < 1 && (
<div className='RibbonBannerInverted'>
<img src={"https://i.imgur.com/yt6osgL.png"}></img>
<h1>Kupon berakhir</h1>
</div>
)}
<div className='coupon-left'> <div className='coupon-left'>
<div className='coupon-code'>{code == null ? '404' : code}</div> <div className='coupon-code'>{code == null ? '404' : code}</div>
<div className='dotted-line'></div> {/* <div className='dotted-line'></div> */}
</div> </div>
<div className='coupon-right'> <div className='coupon-right'>
<h2 className='coupon-value'>{code == null ? 'Kupon tidak ditemukan' : formattedValue}</h2> <h2 className='coupon-value'>{code == null ? 'Kupon tidak ditemukan' : formattedValue}</h2>
{type && <span className='coupon-type'>{type}</span>} {/* Display type if provided */} {type && <span className='coupon-type'>{type}</span>} {/* Display type if provided */}
<p className='coupon-period'> <p className='coupon-period'>
{code == null ? '-' : value == 0 ? `Masa berlangganan ${period} minggu` : `Masa kupon ${period} minggu`} {/* Fixed string concatenation */} {code == null ? '-' : value === 0 ? `Masa berlangganan ${period} minggu` : `Masa kupon ${period} minggu`}
</p> </p>
<p className='coupon-expiration'> <p className='coupon-expiration'>
{expiration == null ? (code == null ? '-' : 'Tanpa kadaluarsa') : `Berlaku sampai: ${expiration}`} {expiration == null ? (code == null ? '-' : 'Tanpa kadaluarsa') :
daysLeft <= 7
? `Berlaku hingga ${daysLeft} hari lagi`
: `Berlaku hingga: ${formatExpirationDate(expiration)}`}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -89,15 +89,15 @@ const DailyCharts = ({ transactionGraph, colors, type }) => {
key={indexx} key={indexx}
className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive
}`} }`}
style={{border: (index==0||index==1) && selectedIndex != index && selectedIndex != indexx || selectedIndex ==-1 && index == 0 || selectedIndex == index && index == indexx ? style={{position: 'relative' }}
'1px solid rgb(179, 177, 177)' : 'none' }}
onClick={() => onClick={() =>
type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1) type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1)
} }
// style={{ backgroundColor: index === indexx ? colors[index % colors.length] : 'transparent' }} // style={{ backgroundColor: index === indexx ? colors[index % colors.length] : 'transparent' }}
> >
<div style={{position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: index == indexx ? `1px solid ${colors[index % colors.length]}` : 'none'}}></div>
<div <div
style={{ color: index === indexx ? colors[index % colors.length] : 'transparent' }}> style={{ color: index === indexx ? 'black' : 'transparent' }}>
{indexx !== chartData.length - 1 ? ( {indexx !== chartData.length - 1 ? (
<> <>
{day}{" "} {day}{" "}

View File

@@ -61,7 +61,6 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, setModal, handleMove
<div className={styles.modalContent} onClick={handleContentClick}> <div className={styles.modalContent} onClick={handleContentClick}>
{modalContent === "edit_account" && <AccountUpdatePage user={user} />} {modalContent === "edit_account" && <AccountUpdatePage user={user} />}
{modalContent === "join" && <Join setModal={setModal} />}
{modalContent === "reset-password" && <ResetPassword />} {modalContent === "reset-password" && <ResetPassword />}
{modalContent === "req_notification" && <NotificationRequest setModal={setModal} />} {modalContent === "req_notification" && <NotificationRequest setModal={setModal} />}
{modalContent === "blocked_notification" && <NotificationBlocked />} {modalContent === "blocked_notification" && <NotificationBlocked />}
@@ -105,6 +104,10 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, setModal, handleMove
{modalContent === "create_coupon" && <CreateCoupon />} {modalContent === "create_coupon" && <CreateCoupon />}
{modalContent === "check_coupon" && <CheckCoupon />} {modalContent === "check_coupon" && <CheckCoupon />}
{modalContent === "create_user" && <CreateUserWithCoupon setModal={setModal}/>} {modalContent === "create_user" && <CreateUserWithCoupon setModal={setModal}/>}
{modalContent === "join" && <Join setModal={setModal} />}
{modalContent === "claim-coupon" && <Join setModal={setModal} />}
</div> </div>
</div> </div>
); );

View File

@@ -52,16 +52,18 @@ const PeriodCharts = ({ type, aggregatedCurrentReports, aggregatedPreviousReport
onClick={() => onClick={() =>
selectedIndex === -1 ? setSelectedIndex(0) : setSelectedIndex(-1) selectedIndex === -1 ? setSelectedIndex(0) : setSelectedIndex(-1)
} }
style={{border: '1px solid rgb(179, 177, 177)', color: colors[0]}} style={{ color: 'black',position: 'relative' }}
> >
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: `1px solid ${colors[0]}` }}></div>
<div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div> <div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div>
</div> </div>
<div <div
className={`${styles.dateSelector} ${styles.dateSelectorInactive className={`${styles.dateSelector} ${styles.dateSelectorInactive
}`} }`}
onClick={() =>
selectedIndex === 0 ? setSelectedIndex(-1) : setSelectedIndex(1) onClick={() =>
}> selectedIndex === 0 ? setSelectedIndex(-1) : setSelectedIndex(1)
}>
<div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div> <div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div>
</div> </div>
</div> </div>
@@ -70,7 +72,7 @@ const PeriodCharts = ({ type, aggregatedCurrentReports, aggregatedPreviousReport
options={{ options={{
tooltip: { enabled: false }, tooltip: { enabled: false },
chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } }, chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } },
xaxis: { xaxis: {
categories: cat, categories: cat,
axisBorder: { axisBorder: {
show: false, // Removes the x-axis line show: false, // Removes the x-axis line
@@ -78,7 +80,7 @@ const PeriodCharts = ({ type, aggregatedCurrentReports, aggregatedPreviousReport
axisTicks: { axisTicks: {
show: false, // Removes the ticks on the x-axis show: false, // Removes the ticks on the x-axis
}, },
}, },
yaxis: { max: globalMax, min: 0, labels: { style: { colors: "transparent" } } }, yaxis: { max: globalMax, min: 0, labels: { style: { colors: "transparent" } } },
grid: { show: false }, grid: { show: false },
fill: { opacity: 0.5 }, fill: { opacity: 0.5 },
@@ -105,17 +107,19 @@ const PeriodCharts = ({ type, aggregatedCurrentReports, aggregatedPreviousReport
<div <div
className={`${styles.dateSelector} ${styles.dateSelectorInactive className={`${styles.dateSelector} ${styles.dateSelectorInactive
}`} }`}
onClick={() => onClick={() =>
selectedIndex === 1 ? setSelectedIndex(-1) : setSelectedIndex(0) selectedIndex === 1 ? setSelectedIndex(-1) : setSelectedIndex(0)
}> }>
<div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div> <div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div>
</div> </div>
<div className={styles.dateSelector} <div className={styles.dateSelector}
onClick={() => onClick={() =>
selectedIndex === -1 ? setSelectedIndex(1) : setSelectedIndex(-1) selectedIndex === -1 ? setSelectedIndex(1) : setSelectedIndex(-1)
} }
style={{border: '1px solid rgb(179, 177, 177)', color: colors[1]}}> style={{ color: 'black',position: 'relative' }}
>
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: `1px solid ${colors[1]}` }}></div>
<div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div> <div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div>
</div> </div>
</div> </div>
@@ -125,13 +129,14 @@ const PeriodCharts = ({ type, aggregatedCurrentReports, aggregatedPreviousReport
tooltip: { enabled: false }, tooltip: { enabled: false },
chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } }, chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } },
xaxis: { xaxis: {
categories: cat, categories: cat,
axisBorder: { axisBorder: {
show: false, // Removes the x-axis line show: false, // Removes the x-axis line
}, },
axisTicks: { axisTicks: {
show: false, // Removes the ticks on the x-axis show: false, // Removes the ticks on the x-axis
}, }, },
},
yaxis: { max: globalMax, min: 0, labels: { style: { colors: "transparent" } } }, yaxis: { max: globalMax, min: 0, labels: { style: { colors: "transparent" } } },
grid: { show: false }, grid: { show: false },
fill: { opacity: 0.5 }, fill: { opacity: 0.5 },

View File

@@ -259,7 +259,7 @@ const SetPaymentQr = ({ shop }) => {
<div <div
style={{ style={{
height: 28, height: 28,
left: isconfigcafeidentityname ? 0:69, left: isconfigcafeidentityname ? 0 : 69,
right: 0, right: 0,
top: 5, top: 5,
position: 'absolute', position: 'absolute',
@@ -328,7 +328,6 @@ const SetPaymentQr = ({ shop }) => {
> >
{window.location.hostname}/ {window.location.hostname}/
</div> </div>
<input <input
ref={cafeIdentifyNameRef} ref={cafeIdentifyNameRef}
style={{ style={{
@@ -344,16 +343,24 @@ const SetPaymentQr = ({ shop }) => {
paddingLeft: isconfigcafeidentityname ? '10px' : '0', // Adjust padding when focused paddingLeft: isconfigcafeidentityname ? '10px' : '0', // Adjust padding when focused
borderLeft: isconfigcafeidentityname ? '1px solid #ccc' : '0', // Adjust border when focused borderLeft: isconfigcafeidentityname ? '1px solid #ccc' : '0', // Adjust border when focused
}} }}
onChange={(e)=>setCafeIdentifyNameUpdate(e.target.value)} onChange={(e) => {
// Convert to lowercase, replace spaces with underscores, and remove invalid characters
const updatedValue = e.target.value
.toLowerCase() // Convert input to lowercase
.replace(/ /g, '_') // Replace spaces with underscores
.replace(/[^a-z0-9_]/g, ''); // Remove characters that are not lowercase letters, numbers, or underscores
setCafeIdentifyNameUpdate(updatedValue);
}}
value={cafeIdentifyNameUpdate} value={cafeIdentifyNameUpdate}
onFocus={() => { onFocus={() => {
setIsConfigCafeIdentityName(true); // Set the state to true when input is focused setIsConfigCafeIdentityName(true); // Set the state to true when input is focused
}} }}
onBlur={() => { onBlur={() => {
setIsConfigCafeIdentityName(false); // Set the state to false when input loses focus setIsConfigCafeIdentityName(false); // Set the state to false when input loses focus
setCafeIdentifyNameUpdate(cafeIdentifyNameDefault) setCafeIdentifyNameUpdate(cafeIdentifyNameDefault); // Reset to default value on blur
}} // Handle blur event to reset the state }} // Handle blur event to reset the state
/> />
</div> </div>
</div> </div>
<div <div
@@ -600,10 +607,10 @@ const SetPaymentQr = ({ shop }) => {
</div> </div>
</div> </div>
{!isconfigcafeidentityname ? <div {!isconfigcafeidentityname ? <div
onClick={() => {setIsConfigCafeIdentityName(true); cafeIdentifyNameRef.current && cafeIdentifyNameRef.current.focus()}} // Open the config modal onClick={() => { setIsConfigCafeIdentityName(true); cafeIdentifyNameRef.current && cafeIdentifyNameRef.current.focus() }} // Open the config modal
style={{ style={{
backgroundColor: '#303034', backgroundColor: '#303034',
right: 0, right: 0,
@@ -618,8 +625,10 @@ const SetPaymentQr = ({ shop }) => {
> >
Ganti alamat kedai Ganti alamat kedai
</div> : ( </div> : (
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', <div style={{
marginBottom: '10px' }}> display: 'flex', justifyContent: 'space-between', width: '100%',
marginBottom: '10px'
}}>
<div <div
onClick={() => setIsConfigCafeIdentityName(false)} // Close the config modal onClick={() => setIsConfigCafeIdentityName(false)} // Close the config modal
style={{ style={{
@@ -641,7 +650,7 @@ const SetPaymentQr = ({ shop }) => {
setCafeIdentifyNameDefault(cafeIdentifyNameUpdate) setCafeIdentifyNameDefault(cafeIdentifyNameUpdate)
// Handle save functionality here // Handle save functionality here
setIsConfigCafeIdentityName(false); // Close after saving setIsConfigCafeIdentityName(false); // Close after saving
}} }}
style={{ style={{
backgroundColor: '#303034', backgroundColor: '#303034',

View File

@@ -0,0 +1,109 @@
// helpers/couponHelpers.js
import API_BASE_URL from '../config.js';
// Helper function to get the auth token from localStorage
export function getAuthToken() {
return localStorage.getItem('auth');
}
// Function to check the validity of the coupon code
export async function checkCoupon(couponCode) {
try {
const response = await fetch(`${API_BASE_URL}/coupon/check/${couponCode}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const data = await response.json();
return { status: 'Coupon is valid', coupon: data.coupon };
} else {
return { status: 'Coupon not found or expired', coupon: null };
}
} catch (error) {
console.error('Error checking coupon:', error);
return { status: 'Error checking coupon.', coupon: null };
}
}
// Function to create a user with the coupon code
export async function createUserWithCoupon(username, email, password, couponCode) {
try {
const response = await fetch(`${API_BASE_URL}/coupon/create-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
email,
password,
couponCode,
}),
});
if (response.ok) {
const data = await response.json();
return { success: true, token: data.token };
} else {
const errorData = await response.json();
return { success: false, message: errorData.message || 'Error creating user' };
}
} catch (error) {
console.error('Error creating user with coupon:', error);
return { success: false, message: 'Error creating user.' };
}
}
// Function to create a user with the coupon code
export async function logCouponForUser(couponCode) {
try {
const response = await fetch(`${API_BASE_URL}/coupon/log-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
couponCode,
}),
});
if (response.ok) {
const data = await response.json();
return { success: true };
} else {
const errorData = await response.json();
return { success: false, message: errorData.message || 'Error creating user' };
}
} catch (error) {
console.error('Error creating user with coupon:', error);
return { success: false, message: 'Error creating user.' };
}
}
// Function to check the validity of the coupon code
export async function getUserCoupons() {
try {
const response = await fetch(`${API_BASE_URL}/coupon/get/`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
});
if (response.ok) {
const data = await response.json();
return { status: 'Coupon is valid', coupons: data.coupons };
} else {
return { status: 'Coupon not found or expired', coupon: null };
}
} catch (error) {
console.error('Error checking coupon:', error);
return { status: 'Error checking coupon.', coupon: null };
}
}

View File

@@ -1,10 +1,7 @@
// LinktreePage.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styles from './LinktreePage.module.css'; // Import the module.css file import styles from './Join.module.css';
import API_BASE_URL from '../config.js'; import { checkCoupon, createUserWithCoupon } from '../helpers/couponHelpers'; // Import the helper functions
function getAuthToken() {
return localStorage.getItem('auth');
}
const LinktreePage = ({ setModal }) => { const LinktreePage = ({ setModal }) => {
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
@@ -25,39 +22,22 @@ const LinktreePage = ({ setModal }) => {
// Detect query params on component mount // Detect query params on component mount
useEffect(() => { useEffect(() => {
const code = queryParams.get('couponCode'); const code = queryParams.get('couponCode');
console.log(code)
if (code) { if (code) {
setCouponStatus(200); setCouponStatus('Coupon is valid');
setCouponCode(code); setCouponCode(code);
setIsUsingCoupon(true); // Automatically switch to the coupon input state setIsUsingCoupon(true); // Automatically switch to the coupon input state
} }
}, [queryParams]); }, [queryParams]);
// Handle coupon validation
const handleCheckCoupon = async (e) => { const handleCheckCoupon = async (e) => {
e.preventDefault(); e.preventDefault();
try { const { status, coupon } = await checkCoupon(couponCode);
const response = await fetch(`${API_BASE_URL}/coupon/check/${couponCode}`, { setCouponStatus(status);
method: 'GET', setCouponDetails(coupon);
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
});
if (response.ok) {
const data = await response.json();
setCouponStatus('Coupon is valid');
setCouponDetails(data.coupon);
} else {
setCouponStatus('Coupon not found or expired');
setCouponDetails(null);
}
} catch (error) {
setCouponStatus('Error checking coupon.');
setCouponDetails(null);
}
}; };
// Handle user creation with coupon
const handleCreateUserWithCoupon = async (e) => { const handleCreateUserWithCoupon = async (e) => {
e.preventDefault(); e.preventDefault();
@@ -66,55 +46,38 @@ const LinktreePage = ({ setModal }) => {
return; return;
} }
try { setLoading(true);
const response = await fetch(`${API_BASE_URL}/coupon/create-user`, { const { success, token, message } = await createUserWithCoupon(username, email, password, couponCode);
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({
username,
email,
password,
couponCode,
}),
});
if (response.ok) { if (success) {
const data = await response.json(); setCouponStatus('User created successfully with coupon');
setCouponStatus('User created successfully with coupon'); localStorage.setItem('auth', token);
setCouponDetails(null);
localStorage.setItem('auth', data.token); // Clean the URL by removing query parameters and hash
const cleanUrl = window.location.origin + window.location.pathname;
window.history.replaceState(null, '', cleanUrl);
// Clean the URL by removing query parameters and hash // Reload the page with the cleaned URL (no query params or hash)
const cleanUrl = window.location.origin + window.location.pathname; window.location.reload();
} else {
// Replace the current URL with the cleaned one setCouponStatus(message || 'Error creating user');
window.history.replaceState(null, '', cleanUrl); setModal('join', { couponCode });
// Reload the page with the cleaned URL (no query params or hash)
window.location.reload();
} else {
const errorData = await response.json();
setCouponStatus(errorData.message || 'Error creating user');
setModal('join', { couponCode })
}
} catch (error) {
setCouponStatus('Error creating user.');
setModal('join', { couponCode })
} }
setLoading(false);
}; };
return ( return (
<div className={styles.linktreePage}> <div className={styles.linktreePage}>
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
<div className={styles.mainHeading}>Gunakan Kupon</div> <div className={styles.mainHeading}>Gunakan Kupon</div>
<div className={styles.subHeadingTransparent}>
Daftarkan kedaimu sekarang dan mulai gunakan semua fitur unggulan kami.
</div>
<div className={styles.LoginForm}> <div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}> <div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
<label htmlFor="username" className={styles.usernameLabel}>---- masuk -------------------------------</label> <label htmlFor="username" className={styles.usernameLabel}>---- Daftar -------------------------------</label>
<input <input
type="text" type="text"
placeholder="Username" placeholder="Username"
@@ -130,7 +93,7 @@ const LinktreePage = ({ setModal }) => {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className={!error ? styles.usernameInput : styles.usernameInputError} className={!error ? styles.usernameInput : styles.usernameInputError}
/> />
<button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}> <button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true); }} className={styles.claimButton}>
<span></span> <span></span>
</button> </button>
</div> </div>
@@ -139,7 +102,6 @@ const LinktreePage = ({ setModal }) => {
<span> <span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- kembali </label> <label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- kembali </label>
<label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----------------- &nbsp; </label> <label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----------------- &nbsp; </label>
</span> </span>
<input <input

View File

@@ -6,11 +6,13 @@ import { getOwnedCafes, createCafe, updateCafe } from "../helpers/cafeHelpers";
import { getMyTransactions } from "../helpers/transactionHelpers"; import { getMyTransactions } from "../helpers/transactionHelpers";
import { unsubscribeUser } from "../helpers/subscribeHelpers.js"; import { unsubscribeUser } from "../helpers/subscribeHelpers.js";
import { getLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers"; import { getLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers";
import { getUserCoupons } from "../helpers/couponHelpers";
import { ThreeDots } from "react-loader-spinner"; import { ThreeDots } from "react-loader-spinner";
import Header from '../components/Header'; import Header from '../components/Header';
import CircularDiagram from "./CircularDiagram"; import CircularDiagram from "./CircularDiagram";
import API_BASE_URL from '../config'; import API_BASE_URL from '../config';
import DailyCharts from '../components/DailyCharts'; import DailyCharts from '../components/DailyCharts';
import Coupon from '../components/Coupon';
const LinktreePage = ({ user, setModal }) => { const LinktreePage = ({ user, setModal }) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -29,6 +31,7 @@ const LinktreePage = ({ user, setModal }) => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedItemId, setSelectedItemId] = useState(0); const [selectedItemId, setSelectedItemId] = useState(0);
const [selectedSubItemId, setSelectedSubItemId] = useState(0); const [selectedSubItemId, setSelectedSubItemId] = useState(0);
const [coupons, setCoupons] = useState(null);
useEffect(() => { useEffect(() => {
@@ -55,11 +58,22 @@ const LinktreePage = ({ user, setModal }) => {
// Remove cafeId from the query parameter // Remove cafeId from the query parameter
searchParams.delete('cafeId'); searchParams.delete('cafeId');
} }
// Update the URL with the modified query params // Update the URL with the modified query params
window.history.replaceState(null, '', `${url.pathname}?${searchParams.toString()}`); window.history.replaceState(null, '', `${url.pathname}?${searchParams.toString()}`);
}, [selectedItemId]); }, [selectedItemId]);
// Detect query params on component mount
useEffect(() => {
handleGetkCoupons();
}, []);
// Handle manual coupon code check
const handleGetkCoupons = async () => {
const result = await getUserCoupons();
setCoupons(result.coupons);
console.log(result)
};
// Handle user transactions // Handle user transactions
const handleMyTransactions = async () => { const handleMyTransactions = async () => {
@@ -169,15 +183,15 @@ const LinktreePage = ({ user, setModal }) => {
// Strong contrasting colors for visibility on white background // Strong contrasting colors for visibility on white background
"#333333", // Dark Gray (great contrast for legibility) "#333333", // Dark Gray (great contrast for legibility)
"#555555", // Medium Gray (slightly lighter, still legible) "#555555", // Medium Gray (slightly lighter, still legible)
// Subtle accent colors (not too bright, but distinct) // Subtle accent colors (not too bright, but distinct)
"#4B9F8D", // Muted Teal (offers a soft contrast) "#4B9F8D", // Muted Teal (offers a soft contrast)
"#7F7F7F", // Slate Gray (elegant and balanced) "#7F7F7F", // Slate Gray (elegant and balanced)
// Softer neutral colors (for less emphasis) // Softer neutral colors (for less emphasis)
"#B0B0B0", // Light Gray (gentle tone for subtle slices) "#B0B0B0", // Light Gray (gentle tone for subtle slices)
"#D3D3D3", // Silver Gray (light, but still visible on white background) "#D3D3D3", // Silver Gray (light, but still visible on white background)
// A touch of color for balance (but still muted) // A touch of color for balance (but still muted)
"#9C6E5C", // Muted Brown (earthy, grounded tone) "#9C6E5C", // Muted Brown (earthy, grounded tone)
"#A1A1A1", // Silver (neutral highlight) "#A1A1A1", // Silver (neutral highlight)
@@ -202,19 +216,19 @@ const LinktreePage = ({ user, setModal }) => {
if (user.roleId == 1 || user.roleId == 0 && selectedItemId == 0) if (user.roleId == 1 || user.roleId == 0 && selectedItemId == 0)
segments = (selectedItemId != 0 && selectedItemId != -1 && selectedSubItemId == 0 ? allSelectedSubItems : selectedItemId != 0 && selectedItemId != -1 ? filteredItems.report?.items || [] : items?.items || []).map((item, index) => ({ segments = (selectedItemId != 0 && selectedItemId != -1 && selectedSubItemId == 0 ? allSelectedSubItems : selectedItemId != 0 && selectedItemId != -1 ? filteredItems.report?.items || [] : items?.items || []).map((item, index) => ({
percentage: item.percentage || (items?.totalIncome / item?.totalIncome || items?.totalIncome / item?.report?.totalIncome) * 100, percentage: item.percentage || (items?.totalIncome / item?.totalIncome || items?.totalIncome / item?.report?.totalIncome) * 100,
value: item.username || item.itemName || item.name, value: item.username || item.itemName || item.name,
color: (colors && colors[index]) || "#cccccc", // Safe check for colors array color: (colors && colors[index]) || "#cccccc", // Safe check for colors array
})) || []; // Ensure segments is an empty array if no items are availabled })) || []; // Ensure segments is an empty array if no items are availabled
// Function to combine items of all cafes for the selected tenant // Function to combine items of all cafes for the selected tenant
console.log(selectedItems) console.log(selectedItems)
console.log(segments) console.log(segments)
// Check if items and items.items are defined before proceeding // Check if items and items.items are defined before proceeding
const allMaterials = (items?.items || []).flatMap(item => item.report?.materialsPurchased || []); const allMaterials = (items?.items || []).flatMap(item => item.report?.materialsPurchased || []);
// Sort the merged array by date if it's not empty // Sort the merged array by date if it's not empty
const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(b.date)); const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(b.date));
@@ -681,7 +695,7 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
<div className={styles.dashboardBody}> <div className={styles.dashboardBody}>
<button className={styles.goCafeButton} style={{ visibility: (selectedItems?.cafeId || selectedSubItems.find(cafe => cafe.cafeId == selectedSubItemId)?.cafeId) == null ? 'hidden' : 'visible' }} onClick={() => window.location.href = window.location.origin + '/' + (selectedItems?.cafeIdentifyName || selectedSubItems.find(cafe => cafe.cafeId == selectedSubItemId)?.cafeIdentifyName)}>Kunjungi kedai</button> <button className={styles.goCafeButton} style={{ visibility: (selectedItems?.cafeId || selectedSubItems.find(cafe => cafe.cafeId == selectedSubItemId)?.cafeId) == null ? 'hidden' : 'visible' }} onClick={() => window.location.href = window.location.origin + '/' + (selectedItems?.cafeIdentifyName || selectedSubItems.find(cafe => cafe.cafeId == selectedSubItemId)?.cafeIdentifyName)}>Kunjungi kedai</button>
<h3 style={{color: 'black'}}>terlaku</h3> <h3 style={{ color: 'black' }}>terlaku</h3>
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -711,7 +725,7 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
> >
</div> </div>
<h5 style={{ margin: 0, textAlign: "left" , color: 'black'}}>{item.percentage == 'Infinity' || isNaN(item.percentage) ? 0 : item.percentage}% &nbsp; {item.value} </h5> <h5 style={{ margin: 0, textAlign: "left", color: 'black' }}>{item.percentage == 'Infinity' || isNaN(item.percentage) ? 0 : item.percentage}% &nbsp; {item.value} </h5>
</div> </div>
))} ))}
{segments.length < 1 && {segments.length < 1 &&
@@ -748,6 +762,15 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
</div> </div>
<h3>penambahan stok</h3> <h3>penambahan stok</h3>
<DailyCharts Data={selectedItems?.report?.materialsPurchased || sortedMaterials} /> <DailyCharts Data={selectedItems?.report?.materialsPurchased || sortedMaterials} />
{coupons && coupons.map((coupon) => {
return <Coupon
code={coupon?.code || null}
value={coupon?.discountValue}
period={coupon?.discountPeriods}
expiration={coupon?.discountEndDate}
/>
})}
<button onClick={()=>setModal('claim-coupon')}></button>
<div style={{ height: '24vh' }}></div> <div style={{ height: '24vh' }}></div>
</div> </div>
@@ -844,38 +867,38 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
<div className={styles.dashboardLine}></div> <div className={styles.dashboardLine}></div>
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
{API_BASE_URL == 'https://test.api.kedaimaster.com' ? {API_BASE_URL == 'https://test.api.kedaimaster.com' ?
<div className={styles.mainHeading}> <div className={styles.mainHeading}>
KEDAIMASTER CREDITS KEDAIMASTER CREDITS
<div className={styles.swipeContainer}> <div className={styles.swipeContainer}>
<div className={styles.swipeCreditsContent}> <div className={styles.swipeCreditsContent}>
{['AI - MUHAMMAD AINUL FIKRI', {['AI - MUHAMMAD AINUL FIKRI',
'BACKEND - ZADIT TAQWA W.', 'BACKEND - ZADIT TAQWA W.',
'FRONTEND - M. PASHA A. P.' , 'FRONTEND - M. PASHA A. P.',
'FRONTEND - NAUFAL DANIYAL P.', 'FRONTEND - NAUFAL DANIYAL P.',
'FRONTEND - ZADIT TAQWA W.', 'FRONTEND - ZADIT TAQWA W.',
'UI/UX - KEVIN DWI WIJAYA', 'UI/UX - KEVIN DWI WIJAYA',
'UI/UX - LUNA CHELISA A.', 'UI/UX - LUNA CHELISA A.',
'UI/UX - MAULINA AYU E.', 'UI/UX - MAULINA AYU E.',
'UI/UX - NUR ARINDA P.', 'UI/UX - NUR ARINDA P.',
'UI/UX - NAURA IZZATI B.',].map((item, index) => ( 'UI/UX - NAURA IZZATI B.',].map((item, index) => (
<div key={index} className={styles.swipeItem}>{item}</div> <div key={index} className={styles.swipeItem}>{item}</div>
))} ))}
</div>
</div> </div>
gratis 3 bulan pertama
</div> </div>
gratis 3 bulan pertama :
</div> <div className={styles.mainHeading}>
: COBA KEDAIMASTER
<div className={styles.mainHeading}> <div className={styles.swipeContainer}>
COBA KEDAIMASTER <div className={styles.swipeContent}>
<div className={styles.swipeContainer}> {['pemesanan langsung dari meja', 'pengelolaan pesanan dan keuangan', 'tentukan suasana musik', 'pengelolaan stok dan manajemen', 'jangan pernah ragukan pelanggan'].map((item, index) => (
<div className={styles.swipeContent}> <div key={index} className={styles.swipeItem}>{item}</div>
{['pemesanan langsung dari meja', 'pengelolaan pesanan dan keuangan', 'tentukan suasana musik', 'pengelolaan stok dan manajemen', 'jangan pernah ragukan pelanggan'].map((item, index) => ( ))}
<div key={index} className={styles.swipeItem}>{item}</div> </div>
))}
</div> </div>
Gratis 3 bulan pertama
</div> </div>
Gratis 3 bulan pertama
</div>
} }
<div className={styles.subHeading}> <div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi. Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
@@ -883,8 +906,8 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
{getLocalStorage('auth') == null && ( {getLocalStorage('auth') == null && (
<div className={styles.LoginForm}> <div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword? styles.reverseForm : ''}`}> <div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
<label htmlFor="username" className={styles.usernameLabel}>---- masuk -------------------------------</label> <label htmlFor="username" className={styles.usernameLabel}>---- Masuk -------------------------------</label>
<input <input
id="username" id="username"
placeholder="username" placeholder="username"
@@ -893,14 +916,14 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
<button onClick={() => {setInputtingPassword(true); setWasInputtingPassword(true)}} className={styles.claimButton}> <button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}>
<span></span> <span></span>
</button> </button>
</div> </div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword? styles.reverseForm : ''}`}> <div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : styles.idleForm}`}>
<span> <span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- kembali </label> <label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- Kembali </label>
<label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----- &nbsp; </label> <label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----- &nbsp; </label>
<label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}> <label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}>
lupa password? - lupa password? -
@@ -947,7 +970,7 @@ const sortedMaterials = allMaterials.sort((a, b) => new Date(a.date) - new Date(
onError={(e) => e.target.src = '/fallback-image.png'} onError={(e) => e.target.src = '/fallback-image.png'}
/> />
</div> </div>
<a style={{left: 0, right: 0, bottom: 0, textAlign: 'center', color: '#254F1A', fontSize:'13px', position: 'fixed'}}>©2025 KEDIRITECHNOPARK.COM</a> <a style={{ left: 0, right: 0, bottom: 0, textAlign: 'center', color: '#254F1A', fontSize: '13px', position: 'fixed' }}>©2025 KEDIRITECHNOPARK.COM</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,13 @@
// LinktreePage.js
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styles from './Join.module.css'; // Import the module.css file import styles from './Join.module.css'; // Import the module.css file
import API_BASE_URL from '../config.js'; import { checkCoupon, logCouponForUser } from '../helpers/couponHelpers'; // Import the new helper
import Coupon from '../components/Coupon'; import Coupon from '../components/Coupon';
function getAuthToken() {
return localStorage.getItem('auth');
}
const LinktreePage = ({ data, setModal }) => { const LinktreePage = ({ data, setModal }) => {
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
const [isOnlyClaimCoupon, setIsOnlyClaimCoupon] = useState(false);
const [isUsingCoupon, setIsUsingCoupon] = useState(false); const [isUsingCoupon, setIsUsingCoupon] = useState(false);
const [couponCode, setCouponCode] = useState(''); const [couponCode, setCouponCode] = useState('');
const [couponStatus, setCouponStatus] = useState(0); const [couponStatus, setCouponStatus] = useState(0);
@@ -16,9 +15,15 @@ const LinktreePage = ({ data, setModal }) => {
// Detect query params on component mount // Detect query params on component mount
useEffect(() => { useEffect(() => {
if(couponCode != '') return; if (couponCode !== '') return;
const modal = queryParams.get('modal');
const code = queryParams.get('couponCode'); const code = queryParams.get('couponCode');
console.log(code) console.log(code)
if (modal == 'claim-coupon') {
setIsOnlyClaimCoupon(true)
setIsUsingCoupon(true); // Automatically switch to the coupon input state
}
if (code) { if (code) {
setCouponCode(code); setCouponCode(code);
setIsUsingCoupon(true); // Automatically switch to the coupon input state setIsUsingCoupon(true); // Automatically switch to the coupon input state
@@ -28,27 +33,14 @@ const LinktreePage = ({ data, setModal }) => {
// Handle manual coupon code check // Handle manual coupon code check
const handleCheckCoupon = async (code = couponCode) => { const handleCheckCoupon = async (code = couponCode) => {
try { const result = await checkCoupon(code); // Call the helper
const response = await fetch(`${API_BASE_URL}/coupon/check/${code}`, { setCouponStatus(result.coupon ? 200 : 404);
method: 'GET', setCouponDetails(result.coupon);
headers: { };
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`,
},
});
if (response.ok) { // Handle manual coupon code check
const data = await response.json(); const handleLogCouponForUser = async (code = couponCode) => {
setCouponStatus(200); const result = await logCouponForUser(code); // Call the helper
setCouponDetails(data.coupon);
} else {
setCouponStatus(404);
setCouponDetails(null);
}
} catch (error) {
setCouponStatus(404);
setCouponDetails(null);
}
}; };
// Listen for query parameter changes (using the `location` object) // Listen for query parameter changes (using the `location` object)
@@ -74,7 +66,7 @@ const LinktreePage = ({ data, setModal }) => {
{!isUsingCoupon ? ( {!isUsingCoupon ? (
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
<div className={styles.mainHeading}>Nikmati Kemudahan Mengelola Kafe</div> <div className={styles.mainHeading}>Nikmati Kemudahan Mengelola Kafe</div>
<div className={styles.subHeading}> <div className={styles.subHeadingTransparent}>
Daftarkan kedaimu sekarang dan mulai gunakan semua fitur unggulan kami. Daftarkan kedaimu sekarang dan mulai gunakan semua fitur unggulan kami.
</div> </div>
<form className={styles.linktreeForm}> <form className={styles.linktreeForm}>
@@ -108,16 +100,13 @@ const LinktreePage = ({ data, setModal }) => {
Gunakan kupon Gunakan kupon
</a> </a>
</div> </div>
<div className={styles.footerImage}>
<img src="./laporan.png" alt="Linktree visual" />
</div>
</div> </div>
</div> </div>
) : ( ) : (
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
<div className={styles.mainHeading}>Daftar Menggunakan Kupon</div> <div className={styles.mainHeading}>{isOnlyClaimCoupon ? 'Aktifkan Kupon' : 'Daftar Menggunakan Kupon'}</div>
<div className={styles.subHeading}> <div className={styles.subHeading}>
Kupon tidak hanya dapat digunakan untuk pembuatan akun penyewa, tetapi juga dapat digunakan untuk memperpanjang masa berlangganan. Kupon dapat digunakan untuk pembuatan akun penyewa maupun untuk memperpanjang masa berlangganan.
</div> </div>
{couponStatus === 0 ? ( {couponStatus === 0 ? (
<form className={styles.linktreeForm} onSubmit={(e) => e.preventDefault()}> <form className={styles.linktreeForm} onSubmit={(e) => e.preventDefault()}>
@@ -144,21 +133,29 @@ const LinktreePage = ({ data, setModal }) => {
period={couponDetails?.discountPeriods} period={couponDetails?.discountPeriods}
expiration={couponDetails?.expirationDate} expiration={couponDetails?.expirationDate}
/> />
{couponStatus == 200 && {couponStatus === 200 &&
<form className={styles.linktreeForm}> <div className={styles.linktreeForm}>
<label htmlFor="username" className={styles.usernameLabel}> <label htmlFor="username" className={styles.usernameLabel}>
-------------------------------------------- --------------------------------------------
</label> </label>
<button <button
type="submit" className={styles.claimButton}
className={styles.claimButton} style={{ width: '266px' }}
style={{ width: '266px' }} onClick={() => {
onClick={() => setModal('create_user', { codeStatus: 200, couponCode })} if (!isOnlyClaimCoupon) {
> // If it's only claiming a coupon, trigger claim logic
<span>Buat akun dengan kupon ini</span> setModal('create_user', { codeStatus: 200, couponCode });
</button> } else {
</form> // Otherwise, handle the coupon for user creation
} handleLogCouponForUser();
}
}}
>
<span>{isOnlyClaimCoupon ? 'Aktifkan untuk akun ini' : 'Buat akun dengan kupon ini'}</span>
</button>
</div>
}
</> </>
)} )}
<div className={styles.footer}> <div className={styles.footer}>
@@ -171,31 +168,23 @@ const LinktreePage = ({ data, setModal }) => {
> >
Pelajari lebih lanjut Pelajari lebih lanjut
</a> </a>
<a {(!isOnlyClaimCoupon || couponStatus != 0) &&
onClick={() => { <a
// Get the current URL query parameters onClick={() => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete('couponCode');
// Remove the couponCode query parameter url.searchParams.delete('codeStatus');
url.searchParams.delete('couponCode'); window.history.pushState({}, '', url.toString());
url.searchParams.delete('codeStatus'); setIsUsingCoupon(couponStatus === 0 ? false : true);
setCouponCode('');
// Update the browser's URL, but keep 'modal=join' intact setCouponDetails(null);
window.history.pushState({}, '', url.toString()); setCouponStatus(0);
}}
// Reset the states and force the component to reset className={styles.footerLink}
setIsUsingCoupon(couponStatus == 0 ? false : true); >
setCouponCode(''); Kembali
setCouponDetails(null); </a>
setCouponStatus(0); }
}}
className={styles.footerLink}
>
Kembali
</a>
</div>
<div className={styles.footerImage}>
<img src="./laporan.png" alt="Linktree visual" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
background-color: rgb(232 204 88); background-color: rgb(232 204 88);
overflow: hidden;
} }
@@ -72,6 +73,15 @@
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.subHeadingTransparent {
font-weight: 400;
line-height: 1.5rem;
font-size: 14px;
font-family: 'poppins';
color: transparent;
margin-bottom: -2.5rem;
}
/* Form */ /* Form */
.linktreeForm { .linktreeForm {
display: flex; display: flex;
@@ -127,6 +137,7 @@
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 14px;
} }
.footerLink { .footerLink {
@@ -159,3 +170,129 @@
margin-top: -50px; margin-top: -50px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.LoginForm {
display: inline-flex;
position: relative;
height: 237px;
}
/* Form */
.FormUsername {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 0vw;
}
.FormUsername.animateForm {
animation: FormUsernameProgress 0.5s forwards;
/* Apply the animation when inputtingPassword is true */
}
.FormUsername.reverseForm {
animation: FormUsernameReverse 0.5s forwards;
/* Reverse animation when inputtingPassword is false */
}
@keyframes FormUsernameProgress {
0% {
left: 0vw;
}
100% {
left: -100vw;
}
}
@keyframes FormUsernameReverse {
0% {
left: -100vw;
}
100% {
left: 0vw;
}
}
.FormPassword {
display: flex;
flex-direction: column;
align-items: flex-start;
position: absolute;
left: 100vw;
}
.FormPassword.animateForm {
animation: FormPasswordProgress 0.5s forwards;
/* Apply the animation when inputtingPassword is true */
}
.FormPassword.reverseForm {
animation: FormPasswordReverse 0.5s forwards;
/* Reverse animation when inputtingPassword is false */
}
@keyframes FormPasswordProgress {
0% {
left: 100vw;
}
100% {
left: 0vw;
}
}
@keyframes FormPasswordReverse {
0% {
left: 0vw;
}
99.9% {
left: 100vw;
visibility: hidden;
}
100% {
left: 0vw;
visibility: hidden;
}
}
.usernameLabel {
font-size: 0.875rem;
color: #444;
margin-bottom: 5px;
position: relative;
}
.usernameInputError {
width: 250px;
height: 55px;
padding-left: 10px;
font-size: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
border: 2px solid red;
/* Red border when error is true */
margin-top: 5px;
margin-bottom: 15px;
/* Apply keyframe animation for border color transition */
animation: borderTransition 2s ease-in-out forwards;
}
/* Keyframe animation for border color transition */
@keyframes borderTransition {
0% {
border-color: red;
/* Initial red border */
}
100% {
border-color: transparent;
/* Transition to transparent */
}
}

View File

@@ -208,6 +208,11 @@
left: 100vw; left: 100vw;
} }
.FormPassword.idleForm {
left: 0vw;
visibility: hidden;
}
.FormPassword.animateForm { .FormPassword.animateForm {
animation: FormPasswordProgress 0.5s forwards; animation: FormPasswordProgress 0.5s forwards;
/* Apply the animation when inputtingPassword is true */ /* Apply the animation when inputtingPassword is true */
@@ -338,9 +343,10 @@
padding: 12px 30px; padding: 12px 30px;
border-radius: 30px; border-radius: 30px;
text-align: center; text-align: center;
font-size: 0.875rem; font-size: 0.8rem;
margin-top: 1.5rem; margin-top: 1.5rem;
cursor: pointer; cursor: pointer;
max-width: 200px;
} }
.footerImage { .footerImage {