Skip to content
Open
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
Binary file modified .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env*
.firebase-key.json
firebase-key.json

# vercel
.vercel
Expand Down
38 changes: 38 additions & 0 deletions components/LoadingSpinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
interface LoadingSpinnerProps {
size?: "sm" | "md" | "lg";
className?: string;
fullScreen?: boolean;
}

export default function LoadingSpinner({
size = "md",
className = "",
fullScreen = false,
}: LoadingSpinnerProps) {
const sizeClasses = {
sm: "h-4 w-4",
md: "h-8 w-8",
lg: "h-12 w-12",
};

const spinner = (
<div
className={`animate-spin rounded-full border-b-2 border-[#509275] ${sizeClasses[size]} ${className}`}
role="status"
aria-label="Loading"
>
<span className="sr-only">Loading...</span>
</div>
);

if (fullScreen) {
return (
<div className="flex items-center justify-center min-h-screen w-full">
{spinner}
</div>
);
}

return spinner;
}

37 changes: 22 additions & 15 deletions components/LogoBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"use client";
import React from "react";
import Image from "next/image";

interface MeteorCardProps {
children: React.ReactNode;
Expand All @@ -10,26 +12,31 @@ interface MeteorCardProps {
const MeteorCard: React.FC<MeteorCardProps> = ({
children,
logoSrc,
logoAlt = "Logo",
logoAlt = "MeteorMate Logo",
className = "",
}) => {
return (
<div className={`relative w-full max-w-md mx-auto ${className}`}>
{/* Logo peeking out the top */}
<div className="absolute -top-10 left-1/2 transform -translate-x-1/2 z-10">
<img
src={logoSrc}
alt={logoAlt}
className="w-40 h-40"
style={{
filter: `
drop-shadow(1px 1px 0 white)
drop-shadow(-1px 1px 0 white)
drop-shadow(1px -1px 0 white)
`,
}}
/>
</div>
{logoSrc && (
<div className="absolute -top-10 left-1/2 transform -translate-x-1/2 z-10">
<Image
src={logoSrc}
alt={logoAlt}
width={160}
height={160}
className="w-40 h-40"
style={{
filter: `
drop-shadow(1px 1px 0 white)
drop-shadow(-1px 1px 0 white)
drop-shadow(1px -1px 0 white)
`,
}}
priority
/>
</div>
)}
<div className="bg-white rounded-2xl shadow-xl px-16 pt-30 pb-15 -mx-20">
{children}
</div>
Expand Down
7 changes: 5 additions & 2 deletions components/ScreenBlocker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComputerDesktopIcon } from "@heroicons/react/24/outline";
import Image from "next/image";

export default function ScreenBlocker() {
return (
Expand All @@ -14,9 +15,11 @@ export default function ScreenBlocker() {
</p>
<div className="mt-5 border-t border-gray-200 pt-2 -mb-3">
<div className="flex items-center justify-center gap-1 mt-2">
<img
<Image
src="/images/peechi_star.png"
alt="Peechi Star"
alt="MeteorMate Star Icon"
width={20}
height={20}
className="size-5 self-center"
/>
<div className="flex flex-col items-center">
Expand Down
32 changes: 32 additions & 0 deletions components/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
interface SkeletonProps {
className?: string;
variant?: "text" | "circular" | "rectangular";
width?: string;
height?: string;
}

export default function Skeleton({
className = "",
variant = "rectangular",
width,
height,
}: SkeletonProps) {
const variantClasses = {
text: "h-4 rounded",
circular: "rounded-full",
rectangular: "rounded",
};

const style: React.CSSProperties = {};
if (width) style.width = width;
if (height) style.height = height;

return (
<div
className={`bg-gray-200 animate-pulse ${variantClasses[variant]} ${className}`}
style={style}
aria-label="Loading content"
/>
);
}

57 changes: 57 additions & 0 deletions components/forms/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";

interface EmailInputProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
label?: string;
error?: string;
disabled?: boolean;
className?: string;
}

export default function EmailInput({
value,
onChange,
placeholder = "Email",
label,
error,
disabled = false,
className = "",
}: EmailInputProps) {
return (
<div className={`flex flex-col ${className}`}>
{label && (
<label className="block text-sm font-urbanist font-light text-gray-700 mb-2">
{label}
</label>
)}
<div className="relative">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-black"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
/>
</svg>
<input
type="email"
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed"
/>
</div>
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
</div>
);
}

106 changes: 106 additions & 0 deletions components/forms/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, { useState } from "react";

interface PasswordInputProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
label?: string;
error?: string;
disabled?: boolean;
className?: string;
showToggle?: boolean;
}

export default function PasswordInput({
value,
onChange,
placeholder = "Password",
label,
error,
disabled = false,
className = "",
showToggle = false,
}: PasswordInputProps) {
const [showPassword, setShowPassword] = useState(false);

return (
<div className={`flex flex-col ${className}`}>
{label && (
<label className="block text-sm font-urbanist font-light text-gray-700 mb-2">
{label}
</label>
)}
<div className="relative">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-black"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z"
/>
</svg>
<input
type={showPassword ? "text" : "password"}
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed"
/>
{showToggle && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
tabIndex={-1}
>
{showPassword ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
)}
</button>
)}
</div>
{error && <p className="text-red-500 text-xs mt-1">{error}</p>}
</div>
);
}

79 changes: 79 additions & 0 deletions components/forms/VerificationCodeInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";
import React, { useRef, useState } from "react";

interface VerificationCodeInputProps {
length?: number;
onComplete?: (code: string) => void;
disabled?: boolean;
className?: string;
}

export default function VerificationCodeInput({
length = 6,
onComplete,
disabled = false,
className = "",
}: VerificationCodeInputProps) {
const [code, setCode] = useState(Array(length).fill(""));
const inputsRef = useRef<(HTMLInputElement | null)[]>([]);

const handleChange = (value: string, index: number) => {
if (/^\d$/.test(value)) {
const newCode = [...code];
newCode[index] = value;
setCode(newCode);

if (index < length - 1 && inputsRef.current[index + 1]) {
inputsRef.current[index + 1]?.focus();
}

// Check if all fields are filled
if (newCode.every((digit) => digit !== "") && onComplete) {
onComplete(newCode.join(""));
}
}
};

const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement>,
index: number
) => {
if (e.key === "Backspace") {
const newCode = [...code];
if (code[index]) {
newCode[index] = "";
setCode(newCode);
} else if (index > 0) {
newCode[index - 1] = "";
setCode(newCode);
inputsRef.current[index - 1]?.focus();
}
} else if (e.key === "Delete") {
const newCode = [...code];
newCode[index] = "";
setCode(newCode);
}
};

return (
<div className={`flex space-x-3 justify-center ${className}`}>
{code.map((digit, index) => (
<input
key={index}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
ref={(el: HTMLInputElement | null) => {
inputsRef.current[index] = el;
}}
disabled={disabled}
className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
/>
))}
</div>
);
}

Loading