generated from muhtadeetaron/nextjs-template
fix(ts): refactor codebase for capacitor setup
This commit is contained in:
@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
const BackgroundWrapper = ({ children }) => {
|
||||
interface BackgroundWrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const BackgroundWrapper = ({ children }: BackgroundWrapperProps) => {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-cover bg-center bg-no-repeat relative"
|
||||
@ -10,9 +14,7 @@ const BackgroundWrapper = ({ children }) => {
|
||||
>
|
||||
<div
|
||||
className="min-h-screen"
|
||||
style={{
|
||||
backgroundColor: "rgba(0, 0, 0, 0)", // Optional overlay - adjust opacity as needed
|
||||
}}
|
||||
style={{ backgroundColor: "rgba(0, 0, 0, 0)" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
interface DestructibleAlertProps {
|
||||
text: string;
|
||||
extraStyles?: string;
|
||||
}
|
||||
|
||||
const DestructibleAlert = ({
|
||||
text,
|
||||
extraStyles = "",
|
||||
}: {
|
||||
text: string;
|
||||
extraStyles?: string;
|
||||
}) => {
|
||||
}: DestructibleAlertProps) => {
|
||||
return (
|
||||
<div
|
||||
className={`border bg-red-200 border-blue-200 rounded-3xl py-6 ${extraStyles}`}
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, InputHTMLAttributes } from "react";
|
||||
|
||||
interface FormFieldProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
handleChangeText: (value: string) => void;
|
||||
}
|
||||
|
||||
const FormField = ({
|
||||
title,
|
||||
@ -6,68 +13,38 @@ const FormField = ({
|
||||
value,
|
||||
handleChangeText,
|
||||
...props
|
||||
}) => {
|
||||
}: FormFieldProps) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const isPasswordField = title.toLowerCase().includes("password");
|
||||
|
||||
const isPasswordField = title === "Password" || title === "Confirm Password";
|
||||
const inputId = `input-${title.replace(/\s+/g, "-").toLowerCase()}`;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<label
|
||||
className="block mb-2"
|
||||
style={{
|
||||
color: "#666666",
|
||||
fontFamily: "Montserrat, sans-serif",
|
||||
fontWeight: "500",
|
||||
fontSize: 18,
|
||||
marginBottom: 8,
|
||||
letterSpacing: "-0.5px",
|
||||
}}
|
||||
htmlFor={inputId}
|
||||
className="block mb-2 text-[#666666] text-[18px] font-medium font-montserrat tracking-[-0.5px]"
|
||||
>
|
||||
{title}
|
||||
</label>
|
||||
|
||||
<div
|
||||
className="h-16 px-4 bg-blue-200 rounded-3xl flex items-center justify-between"
|
||||
style={{
|
||||
height: 64,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
backgroundColor: "#D2DFF0",
|
||||
borderRadius: 20,
|
||||
}}
|
||||
>
|
||||
<div className="h-16 px-4 bg-[#D2DFF0] rounded-3xl flex items-center justify-between">
|
||||
<input
|
||||
id={inputId}
|
||||
type={isPasswordField && !showPassword ? "password" : "text"}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => handleChangeText(e.target.value)}
|
||||
className="flex-1 bg-transparent outline-none border-none text-blue-950"
|
||||
style={{
|
||||
color: "#0D47A1",
|
||||
fontSize: 16,
|
||||
fontFamily: "inherit",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
}}
|
||||
className="flex-1 bg-transparent outline-none border-none text-blue-950 text-[16px] font-inherit"
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{isPasswordField && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="ml-2 text-gray-600 hover:text-gray-800 focus:outline-none"
|
||||
style={{
|
||||
fontFamily: "Montserrat, sans-serif",
|
||||
fontWeight: "500",
|
||||
fontSize: 16,
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
}}
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
className="ml-2 text-gray-600 hover:text-gray-800 focus:outline-none font-montserrat font-medium text-[16px] bg-none border-none"
|
||||
>
|
||||
{showPassword ? "Hide" : "Show"}
|
||||
</button>
|
||||
|
||||
@ -13,7 +13,7 @@ interface HeaderProps {
|
||||
displayUser?: boolean;
|
||||
displaySubject?: string;
|
||||
displayTabTitle?: string;
|
||||
examDuration?: string;
|
||||
examDuration?: string | null;
|
||||
}
|
||||
|
||||
const Header = ({
|
||||
@ -28,7 +28,7 @@ const Header = ({
|
||||
const [totalSeconds, setTotalSeconds] = useState(
|
||||
examDuration ? parseInt(examDuration) * 60 : 0
|
||||
);
|
||||
const { timeRemaining, stopTimer } = useTimer();
|
||||
const { stopTimer } = useTimer();
|
||||
const [userData, setUserData] = useState<UserData>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -6,7 +6,7 @@ import { Badge } from "./ui/badge";
|
||||
interface ResultItemProps {
|
||||
mode: "result";
|
||||
question: Question;
|
||||
selectedAnswer: string | undefined;
|
||||
selectedAnswer: { answer: string } | undefined;
|
||||
}
|
||||
|
||||
interface ExamItemProps {
|
||||
@ -20,10 +20,22 @@ type QuestionItemProps = ResultItemProps | ExamItemProps;
|
||||
|
||||
const QuestionItem = (props: QuestionItemProps) => {
|
||||
const [bookmark, setBookmark] = useState(false);
|
||||
const { question, selectedAnswer } = props;
|
||||
|
||||
const { question } = props;
|
||||
|
||||
const isExam = props.mode === "exam";
|
||||
|
||||
// Extract correct type-safe selectedAnswer
|
||||
const selectedAnswer = isExam
|
||||
? props.selectedAnswer
|
||||
: props.selectedAnswer?.answer;
|
||||
|
||||
const handleOptionSelect = (key: string) => {
|
||||
if (isExam && props.handleSelect) {
|
||||
props.handleSelect(parseInt(question.id), key);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border-[0.5px] border-[#8abdff]/60 rounded-2xl p-4 flex flex-col">
|
||||
<h3 className="text-xl font-semibold ">
|
||||
@ -45,19 +57,14 @@ const QuestionItem = (props: QuestionItemProps) => {
|
||||
|
||||
{isExam ? (
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
{Object.entries(question.options).map(([key, value]) => {
|
||||
{Object.entries(question.options ?? {}).map(([key, value]) => {
|
||||
const isSelected = selectedAnswer === key;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
className="flex items-center gap-3"
|
||||
onClick={
|
||||
isExam
|
||||
? () => props.handleSelect(question.id, key)
|
||||
: undefined
|
||||
}
|
||||
disabled={!isExam}
|
||||
onClick={() => handleOptionSelect(key)}
|
||||
>
|
||||
<span
|
||||
className={`flex items-center rounded-full border px-1.5 ${
|
||||
@ -80,7 +87,7 @@ const QuestionItem = (props: QuestionItemProps) => {
|
||||
<Badge className="bg-yellow-500" variant="destructive">
|
||||
Skipped
|
||||
</Badge>
|
||||
) : selectedAnswer.answer === question.correctAnswer ? (
|
||||
) : selectedAnswer === question.correctAnswer ? (
|
||||
<Badge className="bg-green-500 text-white" variant="default">
|
||||
Correct
|
||||
</Badge>
|
||||
@ -90,23 +97,20 @@ const QuestionItem = (props: QuestionItemProps) => {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
{Object.entries(question.options).map(([key, value]) => {
|
||||
{Object.entries(question.options ?? {}).map(([key, value]) => {
|
||||
const isCorrect = key === question.correctAnswer;
|
||||
const isSelected = key === selectedAnswer?.answer;
|
||||
const isSelected = key === selectedAnswer;
|
||||
|
||||
let optionStyle =
|
||||
"px-2 py-1 flex items-center rounded-full border font-medium text-sm";
|
||||
|
||||
if (isCorrect) {
|
||||
optionStyle += " bg-green-600 text-white border-green-600";
|
||||
}
|
||||
|
||||
if (isSelected && !isCorrect) {
|
||||
} else if (isSelected && !isCorrect) {
|
||||
optionStyle += " bg-red-600 text-white border-red-600";
|
||||
}
|
||||
|
||||
if (!isCorrect && !isSelected) {
|
||||
} else {
|
||||
optionStyle += " border-gray-300 text-gray-700";
|
||||
}
|
||||
|
||||
@ -118,7 +122,9 @@ const QuestionItem = (props: QuestionItemProps) => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="h-[0.5px] border-[0.5px] border-dashed border-black/20"></div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
|
||||
<p className="text-lg">{question.solution}</p>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import React, {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
UIEvent,
|
||||
} from "react";
|
||||
import styles from "../css/SlidingGallery.module.css";
|
||||
import { GalleryViews } from "@/types/gallery";
|
||||
|
||||
interface SlidingGalleryProps {
|
||||
views?: {
|
||||
id: string;
|
||||
content: React.ReactNode;
|
||||
}[];
|
||||
views: GalleryViews[] | undefined;
|
||||
className?: string;
|
||||
showPagination?: boolean;
|
||||
autoScroll?: boolean;
|
||||
@ -17,7 +19,7 @@ interface SlidingGalleryProps {
|
||||
}
|
||||
|
||||
const SlidingGallery = ({
|
||||
views = [],
|
||||
views,
|
||||
className = "",
|
||||
showPagination = true,
|
||||
autoScroll = false,
|
||||
@ -25,15 +27,47 @@ const SlidingGallery = ({
|
||||
onSlideChange = () => {},
|
||||
height = "100vh",
|
||||
}: SlidingGalleryProps) => {
|
||||
const [activeIdx, setActiveIdx] = useState(0);
|
||||
const [activeIdx, setActiveIdx] = useState<number>(0);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const scrollRef = useRef(null);
|
||||
const galleryRef = useRef(null);
|
||||
const autoScrollRef = useRef(null);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const galleryRef = useRef<HTMLDivElement | null>(null);
|
||||
const autoScrollRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||
handleUserInteraction();
|
||||
const scrollLeft = event.currentTarget.scrollLeft;
|
||||
const slideWidth = dimensions.width;
|
||||
const index = Math.round(scrollLeft / slideWidth);
|
||||
|
||||
if (index !== activeIdx) {
|
||||
setActiveIdx(index);
|
||||
onSlideChange(index);
|
||||
}
|
||||
};
|
||||
|
||||
const goToSlide = useCallback(
|
||||
(index: number) => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTo({
|
||||
left: index * dimensions.width,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
},
|
||||
[dimensions.width]
|
||||
);
|
||||
|
||||
const handleDotClick = (index: number) => {
|
||||
handleUserInteraction();
|
||||
goToSlide(index);
|
||||
setActiveIdx(index);
|
||||
onSlideChange(index);
|
||||
};
|
||||
|
||||
// Auto-scroll functionality
|
||||
useEffect(() => {
|
||||
if (autoScroll && views.length > 1) {
|
||||
if (autoScroll && views && views.length > 1) {
|
||||
autoScrollRef.current = setInterval(() => {
|
||||
setActiveIdx((prevIdx) => {
|
||||
const nextIdx = (prevIdx + 1) % views.length;
|
||||
@ -48,15 +82,17 @@ const SlidingGallery = ({
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [autoScroll, autoScrollInterval, views.length]);
|
||||
}, [autoScroll, autoScrollInterval, views?.length, goToSlide, views]);
|
||||
|
||||
// Clear auto-scroll on user interaction
|
||||
const handleUserInteraction = () => {
|
||||
if (autoScrollRef.current) {
|
||||
clearInterval(autoScrollRef.current);
|
||||
autoScrollRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Update dimensions
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (galleryRef.current) {
|
||||
@ -67,18 +103,13 @@ const SlidingGallery = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Initial dimension update
|
||||
updateDimensions();
|
||||
|
||||
// Add resize listener
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
|
||||
// Cleanup
|
||||
return () => window.removeEventListener("resize", updateDimensions);
|
||||
}, []);
|
||||
|
||||
// Recalculate index when dimension changes
|
||||
useEffect(() => {
|
||||
// Recalculate active index when dimensions change
|
||||
if (scrollRef.current && dimensions.width > 0) {
|
||||
const scrollLeft = scrollRef.current.scrollLeft;
|
||||
const slideWidth = dimensions.width;
|
||||
@ -87,34 +118,6 @@ const SlidingGallery = ({
|
||||
}
|
||||
}, [dimensions]);
|
||||
|
||||
const handleScroll = (event: { target: { scrollLeft: any } }) => {
|
||||
handleUserInteraction();
|
||||
const scrollLeft = event.target.scrollLeft;
|
||||
const slideWidth = dimensions.width;
|
||||
const index = Math.round(scrollLeft / slideWidth);
|
||||
if (index !== activeIdx) {
|
||||
setActiveIdx(index);
|
||||
onSlideChange(index);
|
||||
}
|
||||
};
|
||||
|
||||
const goToSlide = (index) => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTo({
|
||||
left: index * dimensions.width,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDotClick = (index) => {
|
||||
handleUserInteraction();
|
||||
goToSlide(index);
|
||||
setActiveIdx(index);
|
||||
onSlideChange(index);
|
||||
};
|
||||
|
||||
// Early return if no views
|
||||
if (!views || views.length === 0) {
|
||||
return (
|
||||
<div className={`${styles.gallery} ${className}`}>
|
||||
@ -138,6 +141,8 @@ const SlidingGallery = ({
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflowX: "scroll",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{views.map((item) => (
|
||||
@ -154,6 +159,7 @@ const SlidingGallery = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showPagination && views.length > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
{views.map((_, index) => (
|
||||
|
||||
Reference in New Issue
Block a user