Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/CourtBooking/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store

# Ignore TanStack Router temp
.tanstack/tmp/
# Nitro
.output/
.nitro/

# TanStack Start
.tanstack/


# Ignore Vite cache
.vite/
145 changes: 145 additions & 0 deletions packages/CourtBooking/bun.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/CourtBooking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@react-oauth/google": "^0.12.2",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-router": "^1.127.3",
"@tanstack/react-router-devtools": "^1.127.3",
"@tanstack/react-start": "^1.127.7",
"firebase": "^11.10.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-spinners": "^0.17.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.11",
"vite": "^7.0.4"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const API_URL =
"https://courtcaller.azurewebsites.net/api/Users?pageNumber=1&pageSize=100";

export const validateEmail = async (email) => {
const emailRegex = /[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}/gim;

if (!emailRegex.test(email)) {
return { isValid: false, message: "Wrong Email format! (abc@gmail.com)" };
}
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("Failed to fetch user detail");
}

const users = await response.json();
const resetEmail = users.data.map((user) => user.email.toLowerCase());

if (!resetEmail.includes(email.toLowerCase())) {
return {
isValid: false,
message: "THERE IS NO ACCOUNT WITH THIS EMAIL!",
};
}

return { isValid: true, message: "" };
} catch (error) {
console.error("Error fetching email:", error);
return { isValid: false, message: "Error validating email" };
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const flexValidation = (slots) => {
const slotNumber = Number(slots);

if (isNaN(slotNumber)) {
return { isValid: false, message: 'You must input a number' };
}

if (slotNumber > 10) {
return { isValid: false, message: 'You can only book a maximum of 10 slots each time' };
}

return { isValid: true, message: '' };
};

export const fixMonthValidation = (months) => {
if (months > 3) {
return { isValid: false, message: 'You can only book a maximum of 3 months each time' };
}

return { isValid: true, message: '' };
};

export const fixStartTimeValidation = (startTime) => {
const timeFormat = /^\d{2}:00:00$/;

if (!timeFormat.test(startTime)) {
return { isValid: false, message: 'Following the format hh:00:00' };
}

return { isValid: true, message: '' };
};

export const fixEndTimeValidation = (startTime, endTime) => {
const timeFormat = /^\d{2}:00:00$/;

if (!timeFormat.test(endTime)) {
return { isValid: false, message: 'Following the format hh:00:00' };
}

const startHour = parseInt(startTime.split(':')[0], 10);
const endHour = parseInt(endTime.split(':')[0], 10);

if (endHour - startHour !== 1) {
return { isValid: false, message: 'End Time is 1 hour later than Start Time' };
}

return { isValid: true, message: '' };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const API_URL =
"https://courtcaller.azurewebsites.net/api/Users?pageNumber=1&pageSize=100";

export const validateFullName = (fullName) => {
if (fullName.length >= 6) return { isValid: true, message: "" };
return { isValid: false, message: "More than 6 characters!" };
};

export const validateUserName = async (userName) => {
if (userName.length < 6)
return { isValid: false, message: "More than 6 character!" };

try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("Failed to fetch registered emails");
}

const users = await response.json();
const duplicateUserName = users.data.map((user) =>
user.userName.toLowerCase()
);

if (duplicateUserName.includes(userName.toLowerCase())) {
return {
isValid: false,
message: "User Name is already exist!",
};
}
return { isValid: true, message: "" };
} catch (error) {
console.error("Error fetching userName:", error);
return { isValid: false, message: "Error validating userName" };
}
};

export const validateAddress = (address) => {
if (address.length >= 15) return { isValid: true, message: "" };
return { isValid: false, message: "More than 15 characters!" };
};

export const validateYob = (yob) => {
const currentYear = new Date().getFullYear();

if (yob > currentYear) {
return {
isValid: false,
message: "Year of birth must be less than current year!",
};
}

if (yob < currentYear - 100) {
return { isValid: false, message: "Invalid year of birth!" };
}

return { isValid: true, message: "" };
};

export const validateEmail = async (email) => {
const emailRegex = /[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}/gim;

if (!emailRegex.test(email)) {
return { isValid: false, message: "Wrong Email format! (abc@gmail.com)" };
}

// Fetch registered emails from the API
try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("Failed to fetch registered emails");
}

const users = await response.json();
const registeredEmails = users.data.map((user) => user.email.toLowerCase());

if (registeredEmails.includes(email.toLowerCase())) {
return { isValid: false, message: "Email is already existed" };
}

return { isValid: true, message: "" };
} catch (error) {
console.error("Error fetching emails:", error);
return {
isValid: false,
message: "Error validating email. Please try again later.",
};
}
};

export const validatePassword = (password) => {
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\W).{6,}$/;

if (!passwordRegex.test(password)) {
return {
isValid: false,
message: 'At least 6 characters, include "A, a, @,.."',
};
}

return { isValid: true, message: "" };
};

export const validateConfirmPassword = (password, confirmPassword) => {
if (password === confirmPassword) return { isValid: true, message: "" };
return { isValid: false, message: "Does not match with Password!" };
};

export const validatePhone = (phone) => {
const phoneRegex = /^\d{10,11}$/;
if (phoneRegex.test(phone)) {
return { isValid: true, message: "" };
}
return {
isValid: false,
message: "Phone must be a number with 10 to 11 digits",
};
};

export const validateTime = (time) => {
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
if (timeRegex.test(time)) return { isValid: true, message: "" };
return { isValid: false, message: "Invalid time format! (hh:mm:ss)" };
};

export const validateRequired = (value) => {
if (value.trim() !== "") return { isValid: true, message: "" };
return { isValid: false, message: "This field is required" };
};

export const validateNumber = (value) => {
if (!isNaN(value) && value.trim() !== "")
return { isValid: true, message: "" };
return { isValid: false, message: "Must be a number" };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const reviewTextValidation = (reviewText) => {
if(reviewText.length < 8){
return {isValid: false, message: 'You need to full fill this box. At least 8 characters!'}
}
return{isValid: true, message: ''}
}

export const valueValidation = (value) => {
if(value < 1){
return {isValid: false, message: 'Choose number of stars !'}
}
return{isValid: true, message: ''}
}
83 changes: 83 additions & 0 deletions packages/CourtBooking/src/components/shared/map/DisplayMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
import "leaflet-control-geocoder/dist/Control.Geocoder.css";
import "leaflet-control-geocoder/dist/Control.Geocoder.js";
import RoutingMachine from './RoutingMachine';
import { getGeocodeFromAddress } from './GeocoderLocation';

function DisplayMap({ address }) {
// Initialize userPosition and destination as null
const [userPosition, setUserPosition] = useState(null);
const [destination, setDestination] = useState(null);

useEffect(() => {
if (address) {
getGeocodeFromAddress(address)
.then(result => setDestination(result))
.catch(error => console.error(error));
}
}, [address]);

useEffect(() => {
navigator.geolocation.getCurrentPosition(
(position) => {
setUserPosition([position.coords.latitude, position.coords.longitude]);
},
() => {
console.error("User location permission denied");
}
);
}, []);

// Default center for the map to prevent white screen when positions are null
const defaultCenter = [10.875376656860935, 106.80076631184579];

return (
<MapContainer center={destination || defaultCenter} zoom={13} scrollWheelZoom={false}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{userPosition && (
<Marker position={userPosition} icon={originIcon}>
<Popup>Your Location</Popup>
</Marker>
)}
{destination && (
<Marker position={destination} icon={destinationIcon}>
<Popup>Branch Location</Popup>
</Marker>
)}

{destination && <UpdateMapView position={destination || defaultCenter} />}
{/* Conditional rendering of RoutingMachine */}
{userPosition && destination && <RoutingMachine userPosition={userPosition} branchPosition={destination} />}
</MapContainer>
);
}

function UpdateMapView({ position }) {
const map = useMap();
if (position) {
map.flyTo(position, 14);
}
return null;
}

const destinationIcon = L.icon({
iconUrl: 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-512.png',
iconSize: [35, 35],
iconAnchor: [17, 35],
popupAnchor: [0, -35]
});

const originIcon = L.icon({
iconUrl: 'https://cdn3.iconfinder.com/data/icons/map-14/144/Map-10-512.png',
iconSize: [60, 60],
iconAnchor: [17, 35],
popupAnchor: [0, -35]
});


export default DisplayMap;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import L from 'leaflet';
import 'leaflet-control-geocoder'; // Ensure you have this or a similar plugin

const getGeocodeFromAddress = async (address) => {
return new Promise((resolve, reject) => {
if (!address) {
reject("Address is required");
}

const geocoder = L.Control.Geocoder.nominatim();
geocoder.geocode(address, (results) => {
if (results.length > 0 && results[0].center) {
const { center } = results[0];
if (center && 'lat' in center && 'lng' in center) {
resolve(center); // Resolve with the geocode
} else {
reject("Invalid center object");
}
} else {
reject("Address not found");
}
});
});
};

export { getGeocodeFromAddress };
11 changes: 11 additions & 0 deletions packages/CourtBooking/src/components/shared/map/Geolocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const getUserLocation = () => {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(resolve, reject);
} else {
reject(new Error('Geolocation is not supported by this browser.'));
}
});
};

export default getUserLocation;
Loading
Loading