Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/components/signature/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const FanSignature = () => {
try {
if (image) {
await updateSignature({
uuid: licenseID!,
uuid: licenseID,
signature: image,
});
}
Expand Down
26 changes: 23 additions & 3 deletions src/context/FormDataContext.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import { createContext, useContext, useState, useMemo } from "react";

interface FormDataContextType {
formData: FormData | null;
setFormData: (data: FormData) => void;
formData: FormDataType;
setFormData: React.Dispatch<React.SetStateAction<FormDataType>>;
}

export interface FormDataType {
uuid: string;
fullname: string;
email: string;
username: string;
dob: string;
location: string;
fileURL?: string;
files?: File[];
}

export const initialFormData: FormDataType = {
uuid: "",
fullname: "",
email: "",
username: "",
dob: "",
location: "",
};

interface FormProviderProps {
children: React.ReactNode;
}
Expand All @@ -20,7 +40,7 @@ export const useFormContext = () => {
};

export const FormDataProvider: React.FC<FormProviderProps> = ({ children }) => {
const [formData, setFormData] = useState<FormData | null>(null);
const [formData, setFormData] = useState<FormDataType>(initialFormData);
const formDataMemoized = useMemo(
() => ({ formData, setFormData }),
[formData]
Expand Down
19 changes: 10 additions & 9 deletions src/context/LicenseContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@ import { createContext, useContext, useState, useEffect, useMemo } from "react";
import { v4 as uuidv4 } from "uuid";

interface LicenseContextValue {
licenseID: string | null;
licenseID: string;
setLicenseID: (licenseID: string) => void;
}

interface LicenseProviderProps {
children: React.ReactNode;
}

const LicenseContext = createContext<LicenseContextValue>({
licenseID: null,
setLicenseID: () => {
/* no-op */
},
});
const LicenseContext = createContext<LicenseContextValue | null>(null);

export const useLicense = () => useContext(LicenseContext);
export const useLicense = () => {
const context = useContext(LicenseContext);
if (!context) {
throw new Error("useLicense must be used within a LicenseProvider");
}
return context;
};

export const LicenseProvider: React.FC<LicenseProviderProps> = ({
children,
}) => {
const [licenseID, setLicenseID] = useState<string | null>(null);
const [licenseID, setLicenseID] = useState<string>("");

// Generate a License ID when the component mounts
useEffect(() => {
Expand Down
51 changes: 44 additions & 7 deletions src/pages/anthem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useRouter } from "next/navigation";
import React, { useState, useEffect } from "react";

// ** Custom Components, Hooks, Utils, etc.
import MediaPlayer from "@/components/media-player";
import { useFormContext } from "@/context/FormDataContext";
import { useLicense } from "@/context/LicenseContext";
import { useSpotify } from "@/context/SpotifyContext";
import type { Catalog } from "@/types/catalog";
Expand Down Expand Up @@ -34,11 +36,16 @@ const Anthem: React.FC = () => {
const { artistCatalog } = useSpotify();

const { licenseID } = useLicense();
const { mutateAsync: newFan } = api.fans.create.useMutation();
const { mutateAsync: updateAnthem } = api.fans.anthem.useMutation();
const { formData } = useFormContext();

const [selectedAnthem, setSelectedAnthem] = useState<Catalog | null>(null);
const [mounted, setMounted] = useState(false);

interface UploadResponse {
fileURL: string;
}
// Add this to prevent hydration mismatch
useEffect(() => {
setMounted(true);
Expand All @@ -47,24 +54,50 @@ const Anthem: React.FC = () => {
if (!mounted) {
return null; // or a loading skeleton
}

const handleSubmit = async () => {
if (!selectedAnthem) {
alert("Please select an anthem first.");
return;
}

try {
const data = await updateAnthem({
uuid: licenseID!,
anthem: selectedAnthem,
const files = formData?.files;
const formDataUpload = new FormData();
if (files) {
formDataUpload.append("file", files[0]);
}

const uploadImageResponse = await fetch("/api/storage", {
method: "POST",
body: formDataUpload,
});

if (!data) {
throw new Error("Failed to save anthem");
if (!uploadImageResponse.ok) {
throw new Error(
`Upload Image Error! status: ${uploadImageResponse.status}`
);
}

router.push("/signature");
const { fileURL } = (await uploadImageResponse.json()) as UploadResponse;

const response = await newFan({
...formData,
profilePicture: fileURL,
});

if (response) {
const data = await updateAnthem({
uuid: licenseID,
anthem: selectedAnthem,
});
if (data) {
router.push("/signature");
}

if (!data) {
throw new Error("Failed to save anthem");
}
}
} catch (error) {
console.error("Error saving anthem:", error);
alert("Failed to save anthem. Please try again.");
Expand Down Expand Up @@ -92,6 +125,10 @@ const Anthem: React.FC = () => {
/>
</div>
)}

<div>
{selectedAnthem && <MediaPlayer selectedAnthem={selectedAnthem} />}
</div>
</div>
</div>
);
Expand Down
139 changes: 103 additions & 36 deletions src/pages/form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
// ** React/Next.js Imports
import { useRouter } from "next/navigation";
import React, { type ChangeEvent } from "react";
import React, { useEffect, useRef, type ChangeEvent } from "react";

// ** React95 Imports
import { Input, Button } from "@react95/core";

// ** Third-Party Imports
import { TRPCClientError } from "@trpc/client";
import { useForm } from "react-hook-form";

// ** Custom Components, Hooks, Utils, etc.
import { useFormContext } from "@/context/FormDataContext";
import { type FormDataType, useFormContext } from "@/context/FormDataContext";
import { useLicense } from "@/context/LicenseContext";
import { api } from "@/utils/trpc";

interface FormInputs {
import { dataURLtoFile } from "../upload";

export interface FormInputs {
fullname: string;
email: string;
username: string;
Expand Down Expand Up @@ -43,58 +46,107 @@ interface UploadResponse {
const Form = () => {
const router = useRouter();
const { licenseID } = useLicense();
const { formData } = useFormContext();
const { mutateAsync: newFan } = api.fans.create.useMutation();
const { formData, setFormData } = useFormContext();
const { mutateAsync: validateUsername } = api.fans.validate.useMutation();

const {
register,
handleSubmit,
watch,
setError,
setValue,
clearErrors,
formState: { errors, isValid },
} = useForm<FormInputs>({
mode: "onBlur",
mode: "onTouched",
reValidateMode: "onChange",
});

// Handle username input transformation
const _username = watch("username");
interface StoredFormData {
data: Partial<FormDataType>;
savedAt: number;
}

const initializedRef = useRef(false);

useEffect(() => {
if (!formData?.fileURL) {
const stored = localStorage.getItem("formData");

if (stored) {
try {
const parsed = JSON.parse(stored) as StoredFormData;
const expired = Date.now() - parsed.savedAt > 20 * 60 * 1000;

if (!expired && parsed.data) {
const parsedData: Partial<FormDataType> = parsed.data;

if (parsedData.fileURL) {
const file = dataURLtoFile(parsedData.fileURL, "recovered.png");

setFormData({
...parsedData,
files: [file],
} as FormDataType);
}
} else {
localStorage.removeItem("formData");
}
} catch (err) {
console.error("Error restoring formData on /form:", err);
localStorage.removeItem("formData");
}
}
}

if (!formData || initializedRef.current) return;

const fields: (keyof FormInputs)[] = [
"fullname",
"email",
"username",
"dob",
"location",
];

fields.forEach((field) => {
const value = formData[field];
if (value) {
setValue(field, value, { shouldValidate: true });
}
});

initializedRef.current = true;

//only rerun when formData changes, setFormData and setValue are stable
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formData]);

// Handle username input transformation
const handleUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const transformValue = value.startsWith("@") ? value : `@${value}`;
setValue("username", transformValue);
clearErrors("username");
};

const formSubmission = async (data: FormInputs) => {
try {
const uploadImageResponse = await fetch("/api/storage", {
method: "POST",
body: formData,
});

if (!uploadImageResponse.ok) {
throw new Error(
`Upload Image Error! status: ${uploadImageResponse.status}`
);
}
const updatedFormData = {
...formData,
uuid: licenseID,
...data,
};

const { fileURL } = (await uploadImageResponse.json()) as UploadResponse;
setFormData(updatedFormData);

const response = await newFan({
uuid: licenseID!,
...data,
profilePicture: fileURL,
});
localStorage.setItem(
"formData",
JSON.stringify({
data: { ...updatedFormData },
savedAt: Date.now(), // refresh timestamp
})
);

if (response) {
router.push("/anthem");
} else {
// Handle error
throw new Error("Server error!");
}
} catch (error) {
console.error("Error making requests:", error);
}
router.push("/anthem");
};

const onSubmit = async (data: FormInputs) => {
Expand All @@ -104,6 +156,21 @@ const Form = () => {
if (!formData) {
throw new Error("Image Data is missing");
}

try {
await validateUsername({ username: data.username });
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (err instanceof TRPCClientError && err.data?.code === "CONFLICT") {
setError("username", {
type: "manual",
message: err.message || "Username already exists",
});
return;
}
throw err; // unexpected error
}

formSubmission(data);
};

Expand Down Expand Up @@ -198,7 +265,7 @@ const Form = () => {
<Button
type='submit'
disabled={!isValid}
className={`mt-7 ${!isValid ? "text-gray-400" : "hover:bg-slate-300 text-black "}`}
className={`mt-7 ${!isValid ? "cursor-not-allowed" : "hover:bg-slate-300 text-black"}`}
>
Next
</Button>
Expand Down
2 changes: 2 additions & 0 deletions src/pages/signature/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const Signature = () => {
const { mutateAsync: welcomeEmail } = api.fans.email.useMutation();

const handleNext = async () => {
localStorage.removeItem("formData");

router.push(`/license/${licenseID}`);

if (fanData) {
Expand Down
Loading