-
-
New Collection
-
- Please add new collections
-
-
+
+
New Collection
+
Please add new collections
+
-
);
};
diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx
index cdbd4ac..9d2b3be 100644
--- a/frontend/src/contexts/AuthContext.tsx
+++ b/frontend/src/contexts/AuthContext.tsx
@@ -17,6 +17,7 @@ interface AuthContextType {
components["schemas"]["UserInfoResponseItem"] | undefined
>
>;
+ is_auth: boolean;
setApiKeyId: React.Dispatch
>;
signout: () => void;
apiKeyId: string | null;
@@ -40,6 +41,7 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const [auth, setAuth] = useState<
components["schemas"]["UserInfoResponseItem"] | undefined
>(undefined);
+ const [is_auth, setIsAuth] = useState(true);
const [apiKeyId, setApiKeyId] = useState(
getLocalStorageAuth(),
);
@@ -47,6 +49,7 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
localStorage.removeItem("token");
setAuth(undefined);
setApiKeyId("");
+ setIsAuth(false);
};
const client = useMemo(
() =>
@@ -74,8 +77,9 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
const fetch_data = async () => {
const { data, error } = await client.GET("/me");
if (error) {
- console.error("Failed to fetch current user", error);
+ signout();
} else {
+ setIsAuth(true);
setAuth(data);
setApiKeyId(data.token);
}
@@ -101,6 +105,7 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {
) {
this.client = client;
}
-
- public async upload(files: File[], listing_id: string) {
- return await this.client.POST("/artifacts/upload/{listing_id}", {
- body: {
- files: [],
- },
- params: {
- path: {
- listing_id,
- },
- },
- bodySerializer() {
- const fd = new FormData();
- files.forEach((file) => fd.append("files", file));
- return fd;
- },
- });
- }
}
diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts
index 4d505c4..750a8c7 100644
--- a/frontend/src/gen/api.ts
+++ b/frontend/src/gen/api.ts
@@ -4,125 +4,6 @@
*/
import { Collection, Image } from "types/model";
export interface paths {
- "/": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Read Root */
- get: operations["read_root__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/url/{artifact_type}/{listing_id}/{name}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Artifact Url */
- get: operations["artifact_url_artifacts_url__artifact_type___listing_id___name__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/info/{artifact_id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Artifact Info */
- get: operations["get_artifact_info_artifacts_info__artifact_id__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/list/{listing_id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** List Artifacts */
- get: operations["list_artifacts_artifacts_list__listing_id__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/upload/{listing_id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /** Upload */
- post: operations["upload_artifacts_upload__listing_id__post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/edit/{artifact_id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- /** Edit Artifact */
- put: operations["edit_artifact_artifacts_edit__artifact_id__put"];
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/artifacts/delete/{artifact_id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete: operations["delete_artifact_artifacts_delete__artifact_id__delete"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
"/translate": {
parameters: {
query?: never;
@@ -132,341 +13,49 @@ export interface paths {
};
get?: never;
put?: never;
- post: operations["translate"];
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/get_images": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["get_images"];
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/delete_image": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["delete_image"];
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/create_collection": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["create_collection"];
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/edit_collection": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["edit_collection"];
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/get_collection": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["get_collection"];
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/get_collections": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["get_collections"];
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/delete_collection": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get: operations["delete_collections"];
- put?: never;
- post?: never;
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/create_subscription": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post: operations["create_subscription"];
- /** Delete Artifact */
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/email/signup/create": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /**
- * Create Signup Token
- * @description Creates a signup token and emails it to the user.
- */
- post: operations["create_signup_token_email_signup_create_post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/email/signup/get/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Signup Token */
- get: operations["get_signup_token_email_signup_get__id__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/email/signup/delete/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post?: never;
- /** Delete Signup Token */
- delete: operations["delete_signup_token_email_signup_delete__id__delete"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/keys/new": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /** New Key */
- post: operations["new_key_keys_new_post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/keys/list": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** List Keys */
- get: operations["list_keys_keys_list_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/keys/delete/{key}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- post?: never;
- /** Delete Key */
- delete: operations["delete_key_keys_delete__key__delete"];
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/listings/search": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** List Listings */
- get: operations["list_listings_listings_search_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/listings/batch": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Batch Listing Info */
- get: operations["get_batch_listing_info_listings_batch_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/listings/dump": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Dump Listings */
- get: operations["dump_listings_listings_dump_get"];
- put?: never;
- post?: never;
+ post: operations["translate"];
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/user/{id}": {
+ "/get_images": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** List User Listings */
- get: operations["list_user_listings_listings_user__id__get"];
+ get: operations["get_images"];
put?: never;
post?: never;
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/me": {
+ "/delete_image": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** List My Listings */
- get: operations["list_my_listings_listings_me_get"];
+ get: operations["delete_image"];
put?: never;
post?: never;
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/add": {
+ "/create_collection": {
parameters: {
query?: never;
header?: never;
@@ -475,15 +64,15 @@ export interface paths {
};
get?: never;
put?: never;
- /** Add Listing */
- post: operations["add_listing_listings_add_post"];
+ post: operations["create_collection"];
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/delete/{listing_id}": {
+ "/edit_collection": {
parameters: {
query?: never;
header?: never;
@@ -492,66 +81,66 @@ export interface paths {
};
get?: never;
put?: never;
- post?: never;
- /** Delete Listing */
- delete: operations["delete_listing_listings_delete__listing_id__delete"];
+ post: operations["edit_collection"];
+ /** Delete Artifact */
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/edit/{id}": {
+ "/get_collection": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
- /** Edit Listing */
- put: operations["edit_listing_listings_edit__id__put"];
+ get: operations["get_collection"];
+ put?: never;
post?: never;
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/{id}": {
+ "/get_collections": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Get Listing */
- get: operations["get_listing_listings__id__get"];
+ get: operations["get_collections"];
put?: never;
post?: never;
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/{id}/view": {
+ "/delete_collection": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ get: operations["delete_collections"];
put?: never;
- /** Increment View Count */
- post: operations["increment_view_count_listings__id__view_post"];
+ post?: never;
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/listings/{id}/vote": {
+ "/publish_collection": {
parameters: {
query?: never;
header?: never;
@@ -560,43 +149,40 @@ export interface paths {
};
get?: never;
put?: never;
- /** Vote Listing */
- post: operations["vote_listing_listings__id__vote_post"];
- /** Remove Vote */
- delete: operations["remove_vote_listings__id__vote_delete"];
+ post: operations["publish_collection"];
+ delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/onshape/set/{listing_id}": {
+ "/public_collections": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- get?: never;
+ get: operations["public_collections"];
put?: never;
- /** Set Onshape Document */
- post: operations["set_onshape_document_onshape_set__listing_id__post"];
+ post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
- "/onshape/pull/{listing_id}": {
+ "/create_subscription": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
- /** Pull Onshape Document */
- get: operations["pull_onshape_document_onshape_pull__listing_id__get"];
+ get?: never;
put?: never;
- post?: never;
+ post: operations["create_subscription"];
+ /** Delete Artifact */
delete?: never;
options?: never;
head?: never;
@@ -673,204 +259,6 @@ export interface paths {
patch?: never;
trace?: never;
};
- "/users/batch": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Users Batch Endpoint */
- get: operations["get_users_batch_endpoint_users_batch_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/public/batch": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Users Public Batch Endpoint */
- get: operations["get_users_public_batch_endpoint_users_public_batch_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get User Info By Id Endpoint */
- get: operations["get_user_info_by_id_endpoint_users__id__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/public/me": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get My Public User Info Endpoint */
- get: operations["get_my_public_user_info_endpoint_users_public_me_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/public/{id}": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Get Public User Info By Id Endpoint */
- get: operations["get_public_user_info_by_id_endpoint_users_public__id__get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/validate-api-key": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Validate Api Key Endpoint */
- get: operations["validate_api_key_endpoint_users_validate_api_key_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/github/client-id": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Github Client Id Endpoint */
- get: operations["github_client_id_endpoint_users_github_client_id_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/github/code": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /**
- * Github Code
- * @description Gives the user a session token upon successful github authentication and creation of user.
- *
- * Args:
- * data: The request body, containing the code from the OAuth redirect.
- * crud: The CRUD object.
- * response: The response object.
- *
- * Returns:
- * UserInfoResponse.
- */
- post: operations["github_code_users_github_code_post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/google/client-id": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /** Google Client Id Endpoint */
- get: operations["google_client_id_endpoint_users_google_client_id_get"];
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/google/login": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /** Google Login Endpoint */
- post: operations["google_login_endpoint_users_google_login_post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
- "/users/set-moderator": {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- get?: never;
- put?: never;
- /** Set Moderator */
- post: operations["set_moderator_users_set_moderator_post"];
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
}
export type webhooks = Record;
export interface components {
@@ -983,6 +371,10 @@ export interface components {
title: string;
description: string;
};
+ CollectionPublishRequest: {
+ id: string;
+ flag: boolean;
+ };
SubscriptionRequest: {
payment_method_id: string;
email: string;
@@ -1742,6 +1134,68 @@ export interface operations {
};
};
};
+ publish_collection: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": components["schemas"]["CollectionPublishRequest"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": never;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ public_collections: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": Array;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
translate: {
parameters: {
query?: never;
diff --git a/frontend/src/hooks/alerts.tsx b/frontend/src/hooks/alerts.tsx
index 84e8ec1..596d7b8 100644
--- a/frontend/src/hooks/alerts.tsx
+++ b/frontend/src/hooks/alerts.tsx
@@ -5,7 +5,6 @@ import {
useContext,
useState,
} from "react";
-import { Toast, ToastContainer } from "react-bootstrap";
const DELAY = 5000;
const MAX_ERRORS = 10;
@@ -15,13 +14,15 @@ type AlertType = "error" | "success" | "primary" | "info";
const alertTypeToBg = (kind: AlertType) => {
switch (kind) {
case "error":
- return "danger";
+ return "bg-red-500 text-white"; // Tailwind-style classes
case "success":
- return "success";
+ return "bg-green-500 text-white";
case "primary":
- return "primary";
+ return "bg-blue-500 text-white";
case "info":
- return "secondary";
+ return "bg-gray-500 text-white";
+ default:
+ return "bg-gray-500 text-white";
}
};
@@ -52,9 +53,9 @@ export const AlertQueueProvider = (props: AlertQueueProviderProps) => {
const addAlert = useCallback(
(alert: string | ReactNode, kind: AlertType) => {
+ const alertId = generateAlertId();
setAlerts((prev) => {
const newAlerts = new Map(prev);
- const alertId = generateAlertId();
newAlerts.set(alertId, [alert, kind]);
// Ensure the map doesn't exceed MAX_ERRORS
@@ -65,6 +66,10 @@ export const AlertQueueProvider = (props: AlertQueueProviderProps) => {
return newAlerts;
});
+ // Automatically remove the alert after DELAY
+ setTimeout(() => {
+ removeAlert(alertId);
+ }, DELAY);
},
[generateAlertId],
);
@@ -93,7 +98,7 @@ export const AlertQueueProvider = (props: AlertQueueProviderProps) => {
export const useAlertQueue = () => {
const context = useContext(AlertQueueContext);
if (context === undefined) {
- throw new Error("useAlertQueue must be used within a ErrorQueueProvider");
+ throw new Error("useAlertQueue must be used within an AlertQueueProvider");
}
return context;
};
@@ -109,31 +114,45 @@ export const AlertQueue = (props: AlertQueueProps) => {
return (
<>
{children}
-
- {Array.from(alerts).map(([alertId, [alert, kind]]) => {
- return (
- removeAlert(alertId)}
- animation={true}
- >
-
-
- {kind.charAt(0).toUpperCase() + kind.slice(1)}
-
-
- {alert}
-
- );
- })}
-
+ {Array.from(alerts).map(([alertId, [alert, kind]]) => (
+
+
+
{kind}
+
removeAlert(alertId)}
+ className="ml-2 text-white hover:text-gray-200 cursor-pointer"
+ >
+ ×
+
+
+
{alert}
+
+ ))}
+
+
+ {/* Remove the jsx attribute */}
+
>
);
};
diff --git a/frontend/src/hooks/auth.tsx b/frontend/src/hooks/auth.tsx
deleted file mode 100644
index e07ea68..0000000
--- a/frontend/src/hooks/auth.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import axios, { AxiosInstance } from "axios";
-import { BACKEND_URL } from "constants/backend";
-import {
- createContext,
- ReactNode,
- useCallback,
- useContext,
- useEffect,
- useState,
-} from "react";
-import { useNavigate, useSearchParams } from "react-router-dom";
-
-const API_KEY_ID = "__API_KEY";
-
-const getLocalStorageApiKey = (): string | null => {
- return localStorage.getItem(API_KEY_ID);
-};
-
-const setLocalStorageApiKey = (token: string) => {
- localStorage.setItem(API_KEY_ID, token);
-};
-
-const deleteLocalStorageApiKey = () => {
- localStorage.removeItem(API_KEY_ID);
-};
-
-interface AuthenticationContextProps {
- apiKey: string | null;
- setApiKey: (token: string) => void;
- logout: () => void;
- isAuthenticated: boolean;
- api: AxiosInstance;
-}
-
-const AuthenticationContext = createContext<
- AuthenticationContextProps | undefined
->(undefined);
-
-interface AuthenticationProviderProps {
- children: ReactNode;
-}
-
-export const AuthenticationProvider = (props: AuthenticationProviderProps) => {
- const { children } = props;
-
- const [apiKey, setApiKey] = useState
(getLocalStorageApiKey());
-
- const navigate = useNavigate();
-
- const isAuthenticated = apiKey !== null;
-
- const api = axios.create({
- baseURL: BACKEND_URL,
- withCredentials: true,
- });
-
- if (apiKey !== null) {
- // Adds the API key to the request header since it is set.
- api.interceptors.request.use(
- (config) => {
- config.headers.Authorization = `Bearer ${apiKey}`;
- return config;
- },
- (error) => {
- return Promise.reject(error);
- },
- );
- }
-
- useEffect(() => {
- if (apiKey === null) {
- deleteLocalStorageApiKey();
- } else {
- setLocalStorageApiKey(apiKey);
- }
- }, [apiKey]);
-
- const logout = useCallback(() => {
- (async () => {
- await api.delete("/users/logout");
- setApiKey(null);
- navigate("/");
- })();
- }, [navigate]);
-
- return (
-
- {children}
-
- );
-};
-
-export const useAuthentication = (): AuthenticationContextProps => {
- const context = useContext(AuthenticationContext);
- if (!context) {
- throw new Error(
- "useAuthentication must be used within a AuthenticationProvider",
- );
- }
- return context;
-};
-
-interface OneTimePasswordWrapperProps {
- children: ReactNode;
-}
-
-interface UserLoginResponse {
- api_key: string;
-}
-
-export const OneTimePasswordWrapper = ({
- children,
-}: OneTimePasswordWrapperProps) => {
- const [searchParams] = useSearchParams();
- const navigate = useNavigate();
- const { setApiKey, api } = useAuthentication();
-
- useEffect(() => {
- (async () => {
- const payload = searchParams.get("otp");
- if (payload !== null) {
- try {
- const response = await api.post("/users/otp", {
- payload,
- });
- setApiKey(response.data.api_key);
- navigate("/");
- } finally {
- searchParams.delete("otp");
- }
- }
- })();
- }, []);
-
- return <>{children}>;
-};
diff --git a/frontend/src/hooks/theme.tsx b/frontend/src/hooks/theme.tsx
index d62eb6b..ad19870 100644
--- a/frontend/src/hooks/theme.tsx
+++ b/frontend/src/hooks/theme.tsx
@@ -21,7 +21,7 @@ const COLORS: { [key in Theme]: ThemeColors } = {
color: "#201a42",
},
dark: {
- backgroundColor: "#000000",
+ backgroundColor: "#ffffff",
color: "#f5f2ef",
},
};
diff --git a/frontend/src/images/KScaleASCII.png b/frontend/src/images/KScaleASCII.png
new file mode 100644
index 0000000..1deb260
Binary files /dev/null and b/frontend/src/images/KScaleASCII.png differ
diff --git a/frontend/src/images/KScaleASCIIMobile.png b/frontend/src/images/KScaleASCIIMobile.png
new file mode 100644
index 0000000..d8e832c
Binary files /dev/null and b/frontend/src/images/KScaleASCIIMobile.png differ
diff --git a/frontend/src/images/bookplaceholder.png b/frontend/src/images/bookplaceholder.png
new file mode 100644
index 0000000..ebe352d
Binary files /dev/null and b/frontend/src/images/bookplaceholder.png differ
diff --git a/frontend/src/images/small-logo.png b/frontend/src/images/small-logo.png
new file mode 100644
index 0000000..487ff9a
Binary files /dev/null and b/frontend/src/images/small-logo.png differ
diff --git a/frontend/src/pages/Collection.tsx b/frontend/src/pages/Collection.tsx
index a7ca922..6b92adb 100644
--- a/frontend/src/pages/Collection.tsx
+++ b/frontend/src/pages/Collection.tsx
@@ -1,104 +1,30 @@
-import { Api } from "api/api";
-import axios, { AxiosInstance } from "axios";
-import AudioPlayer from "components/Audio";
-import ImageComponent from "components/image";
-import Modal from "components/modal";
-import UploadContent from "components/UploadContent";
+import CollectionEdit from "components/collection/Edit";
+import CollectionNew from "components/collection/New";
+import CollectionView from "components/collection/View";
import { useAuth } from "contexts/AuthContext";
-import { useLoading } from "contexts/LoadingContext";
import { useAlertQueue } from "hooks/alerts";
import React, { useEffect, useMemo, useState } from "react";
-import { ListManager } from "react-beautiful-dnd-grid";
-import { Col } from "react-bootstrap";
-import {
- ArrowLeft,
- CaretLeft,
- CaretRight,
- SkipBackward,
- SkipForward,
-} from "react-bootstrap-icons";
-import { useLocation, useNavigate, useParams } from "react-router-dom";
-import { Collection, Image } from "types/model";
+import { useLocation, useParams } from "react-router-dom";
+import { Collection } from "types/model";
const CollectionPage: React.FC = () => {
const { id } = useParams<{ id?: string }>();
const location = useLocation();
- const navigate = useNavigate();
- const [title, setTitle] = useState("");
- const [description, setDescription] = useState("");
- const [currentImageIndex, setCurrentImageIndex] = useState(0);
- const [currentTranscriptionIndex, setCurrentTranscriptionIndex] = useState(0);
- const [currentImage, setCurrentImage] = useState(null);
const [collection, setCollection] = useState(
undefined,
);
const { auth, client } = useAuth();
- const { startLoading, stopLoading } = useLoading();
- const [showUploadModal, setShowUploadModal] = useState(false);
- const [showDeleteImageModal, setShowDeleteImageModal] = useState(false);
- const [images, setImages] = useState | undefined>([]);
- const [reorderImageIds, setReorderImageIds] = useState | null>(
- [],
- );
const { addAlert } = useAlertQueue();
- const [deleteImageId, setDeleteImageId] = useState("");
- const apiClient: AxiosInstance = useMemo(
- () =>
- axios.create({
- baseURL: process.env.REACT_APP_BACKEND_URL, // Base URL for all requests
- timeout: 10000, // Request timeout (in milliseconds)
- headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${auth?.token}`, // Add any default headers you need
- },
- }),
- [auth?.token],
- );
- useEffect(() => {
- if (collection && collection.images) {
- setReorderImageIds([...collection.images]);
- }
- }, [collection]);
- const apiClient1: AxiosInstance = useMemo(
- () =>
- axios.create({
- baseURL: process.env.REACT_APP_BACKEND_URL,
- timeout: 1000000,
- headers: {
- "Content-Type": "multipart/form-data",
- Authorization: `Bearer ${auth?.token}`,
- },
- }),
- [auth?.token],
- );
- const API = useMemo(() => new Api(apiClient), [apiClient]);
- const API_Uploader = useMemo(() => new Api(apiClient1), [apiClient1]);
+
// Helper to check if it's an edit action
const isEditAction = useMemo(
() => location.search.includes("Action=edit"),
[location.search],
);
- // Get translated images
- const translatedImages = useMemo(() => {
- // Get translated images
- if (images) {
- const filter = images.filter((img) => img.is_translated);
- const final_filter = reorderImageIds
- ?.map((img) => {
- const foundItem = filter.find((item) => item.id == img);
- return foundItem ? foundItem : null; // Return `null` or skip
- })
- .filter(Boolean); // Filters out `null` or `undefined`
- if (final_filter) return final_filter;
- }
- return [];
- }, [images]);
-
// Simulate fetching data for the edit page (mocking API call)
useEffect(() => {
if (id && auth?.is_auth) {
- startLoading();
const asyncfunction = async () => {
const { data: collection, error } = await client.GET(
"/get_collection",
@@ -106,436 +32,67 @@ const CollectionPage: React.FC = () => {
);
if (error) addAlert(error.detail?.toString(), "error");
else setCollection(collection);
- stopLoading();
};
asyncfunction();
}
}, [id, auth]);
- useEffect(() => {
- if (translatedImages.length > 0) {
- setCurrentImage(translatedImages[currentImageIndex]);
- }
- }, [currentImageIndex, translatedImages]);
-
- useEffect(() => {
- if (collection) {
- const asyncfunction = async () => {
- startLoading();
- const { data: images, error } = await client.GET("/get_images", {
- params: { query: { collection_id: collection.id } },
- });
- if (error) addAlert(error.detail?.toString(), "error");
- else setImages(images);
- stopLoading();
- };
- asyncfunction();
- }
- }, [collection?.id]);
-
- const handleCreate = async (e: React.FormEvent) => {
- e.preventDefault();
- startLoading();
- const { data: collection, error } = await client.POST(
- "/create_collection",
- { body: { title, description } },
- );
- if (error) addAlert(error.detail?.toString(), "error");
- else if (collection != null) {
- navigate(`/collection/${collection.id}?Action=edit`);
- addAlert("New collection has been created successfully!", "success");
- } else addAlert("The process has gone wrong!", "error");
- stopLoading();
- };
- // Navigate between images
- const handleNext = () => {
- if (currentImageIndex < translatedImages.length - 1) {
- setCurrentImageIndex(currentImageIndex + 1);
- setCurrentTranscriptionIndex(0);
- }
- };
-
- const handlePrev = () => {
- if (currentImageIndex > 0) {
- setCurrentImageIndex(currentImageIndex - 1);
- setCurrentTranscriptionIndex(0);
- }
- };
- // Navigate transcriptions
- const handleTranscriptionNext = () => {
- if (
- currentImage?.transcriptions &&
- currentTranscriptionIndex < currentImage?.transcriptions.length - 1
- ) {
- setCurrentTranscriptionIndex(currentTranscriptionIndex + 1);
- }
- };
-
- const handleTranscriptionPrev = () => {
- if (currentTranscriptionIndex > 0) {
- setCurrentTranscriptionIndex(currentTranscriptionIndex - 1);
- }
- };
// Return button handler
- const handleReturn = () => {
- navigate("/collections");
- };
-
- const handleSave = (e: React.FormEvent) => {
- e.preventDefault();
- if (collection && reorderImageIds) {
- const asyncfunction = async () => {
- startLoading();
- collection.images = reorderImageIds;
- const { error } = await client.POST("/edit_collection", {
- body: collection,
- });
- if (error) addAlert(error.detail?.toString(), "error");
- else {
- setCollection({ ...collection });
- addAlert("The collection has been updated successfully!", "success");
- }
- stopLoading();
- };
- asyncfunction();
- }
- };
- const handleUpload = async (file: File) => {
- if (collection) {
- startLoading();
- const Image = await API_Uploader.uploadImage(file, collection?.id);
- stopLoading();
- if (Image) {
- const new_images: Array | undefined = images;
- new_images?.push(Image);
- if (new_images != undefined) {
- setImages(new_images);
- collection.images.push(Image.id);
- setCollection({ ...collection });
- }
- }
- }
- };
- const handleTranslateOneImage = async (image_id: string) => {
- if (images) {
- startLoading();
- addAlert(
- "The image is being tranlated. Please wait a moment.",
- "primary",
- );
- const image_response = await API.translateImages([image_id]);
- const i = images?.findIndex((image) => image.id == image_id);
- images[i] = image_response[0];
- setImages([...images]);
- addAlert("The image has been tranlated!", "success");
- stopLoading();
- }
- };
- // Inside your CollectionPage component
- /* eslint-disable */
- const handleDragEnd = (sourceIndex: number, destinationIndex: number) => {
- /* eslint-enable */
- if (!reorderImageIds) return;
- const [removed] = reorderImageIds.splice(sourceIndex, 1);
- reorderImageIds.splice(destinationIndex, 0, removed);
- setReorderImageIds([...reorderImageIds]);
- // Optionally, you can save the new order to your backend here
- };
-
- const onShowDeleteImageModal = (id: string) => {
- setDeleteImageId(id);
- setShowDeleteImageModal(true);
- };
- const onDeleteImage = async () => {
- if (deleteImageId) {
- startLoading();
- const { error } = await client.GET("/delete_image", {
- params: { query: { id: deleteImageId } },
- });
- if (error) addAlert(error.detail?.toString(), "error");
- else if (images) {
- const filter = images.filter((image) => image.id !== deleteImageId);
- setImages(filter);
- const filteredId = collection?.images.filter(
- (image) => image !== deleteImageId,
- );
- if (filteredId) setReorderImageIds(filteredId);
- else setReorderImageIds([]);
- addAlert("The image has been deleted!", "success");
- }
- setShowDeleteImageModal(false);
- stopLoading();
- }
- };
+ // const handleReturn = () => {
+ // navigate("/collections");
+ // };
+
+ // Navigate View Page
+ // const handlePreview = () => {
+ // navigate("/collection/" + collection?.id);
+ // };
// Custom Return Button (fixed top-left with border)
- const ReturnButton = () => (
-
- );
+ // const ReturnButton = () => (
+ //
+ // );
+ // Custom Return Button (fixed top-left with border)
+ // const PreviewButton = () => (
+ //
+ //
+ //
+ //
+ //
+ // );
// Rendering New Collection Page
if (!id) {
- return (
-
-
New Collection
-
-
-
- );
+ return ;
}
-
// Rendering Edit Collection Page
if (id && isEditAction && collection) {
return (
-
-
Edit Collection
-
-
-
-
-
- {/* Upload Modal */}
-
setShowUploadModal(false)}
- >
-
-
-
-
-
-
setShowDeleteImageModal(false)}
- >
-
- Are you sure you want to delete the collection?
-
-
-
-
-
- {reorderImageIds && (
-
{
- const image = images?.find((item) => item.id === id);
- return (
-
- {image ? (
-
- ) : (
-
- )}
-
- );
- }}
- />
- )}
-
-
+
);
}
-
// Rendering Collection Detail Page
if (id && !isEditAction && collection) {
- return (
-
-
{collection.title}
-
{collection.description}
- {currentImage ? (
-
-
-
- {currentImage.transcriptions[currentTranscriptionIndex].text}
-
-
- {currentImage.transcriptions[currentTranscriptionIndex].pinyin}
-
-
- {
- currentImage.transcriptions[currentTranscriptionIndex]
- .translation
- }
-
-
-
- {/* Navigation Buttons */}
-
-
-
-
-
-
-
- ) : (
-
No translated images available.
- )}
-
-
- );
+ return ;
}
- return <>>;
+ //skeleton
+ return (
+
+ );
};
-
export default CollectionPage;
diff --git a/frontend/src/pages/Collections.tsx b/frontend/src/pages/Collections.tsx
index 6570e2c..99acabd 100644
--- a/frontend/src/pages/Collections.tsx
+++ b/frontend/src/pages/Collections.tsx
@@ -1,11 +1,11 @@
-import CardItem from "components/card";
+import Book from "components/Book";
+import BookSkeleton from "components/BookSkeleton";
import Modal from "components/modal";
import NewCardItem from "components/new_card";
import { useAuth } from "contexts/AuthContext";
import { useLoading } from "contexts/LoadingContext";
import { useAlertQueue } from "hooks/alerts";
import { useEffect, useState } from "react";
-import { Col, Row } from "react-bootstrap";
import { Collection } from "types/model";
const Collections = () => {
@@ -15,6 +15,7 @@ const Collections = () => {
const { startLoading, stopLoading } = useLoading();
const [delete_ID, setDeleteID] = useState(String);
const { addAlert } = useAlertQueue();
+ const [is_loading, setIsLoading] = useState(true);
const onDeleteModalShow = (id: string) => {
setDeleteID(id);
setShowModal(true);
@@ -41,32 +42,51 @@ const Collections = () => {
useEffect(() => {
if (auth?.is_auth) {
const asyncfunction = async () => {
- startLoading();
const { data: collections, error } =
await client.GET("/get_collections");
if (error) addAlert(error.detail?.toString(), "error");
else setCollection(collections);
- stopLoading();
+ setIsLoading(false);
};
asyncfunction();
}
}, [auth]);
return (
-
-
My Collections
-
-
-
-
- {collections?.map((collection) => {
- return (
-
-
-
- );
- })}
-
+
+ {auth?.is_auth ? (
+
+
My Collections
+
+
+
+
+ {is_loading ? (
+
+ ) : (
+ collections?.map((collection) => {
+ return (
+
+
+
+ );
+ })
+ )}
+
+
+ ) : (
+ <>>
+ )}
{/* Delete Modal */}
setShowModal(false)}>
diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx
index 1c83be4..b6ea186 100644
--- a/frontend/src/pages/Home.tsx
+++ b/frontend/src/pages/Home.tsx
@@ -1,45 +1,103 @@
-import avatar from "assets/avatar.png";
-import { Col, Container, Row } from "react-bootstrap";
+import Book from "components/Book";
+import BookSkeleton from "components/BookSkeleton";
+import { useAuth } from "contexts/AuthContext";
+import { useEffect, useState } from "react";
+import { Collection } from "types/model";
const Home = () => {
+ const [public_collections, setPublicCollections] = useState<
+ Array
| undefined
+ >([]);
+ const [collections, setCollections] = useState | undefined>(
+ [],
+ );
+ const { auth, client } = useAuth();
+ const [is_collection_roading, setIsCollectionRoading] =
+ useState(true);
+ const [is_public_collection_roading, setIsPublicCollectionRoading] =
+ useState(true);
+ useEffect(() => {
+ (async () => {
+ const { data } = await client.GET("/public_collections");
+ setPublicCollections(data);
+ setIsPublicCollectionRoading(false);
+ })();
+ }, [client]);
+ useEffect(() => {
+ if (auth?.is_auth)
+ (async () => {
+ const { data } = await client.GET("/get_collections");
+ setCollections(data);
+ setIsCollectionRoading(false);
+ })();
+ }, [client, auth]);
return (
-
-
+
+
{/* Text Section */}
-
-
LinguaPhoto
-
Visual language learning for everyone!
-
{/* GoogleAuthComponent placeholder */}
-
-
- {/* Image Section */}
-
-
-
-
-
+
+
LinguaPhoto
+
Visual language learning for everyone!
+
+ {/* GoogleAuthComponent placeholder */}
+
+
+
+
+
Public Collections
+
+ {is_public_collection_roading ? (
+ // skeleton for public collections
+
+ ) : (
+ public_collections?.map((collection) => {
+ return (
+
+
+
+ );
+ })
+ )}
+
+
+ {auth?.is_auth ? (
+
+
My Collections
+
+ {is_collection_roading ? (
+ // skeleton for my collections
+
+ ) : (
+ collections?.map((collection) => {
+ return (
+
+
+
+ );
+ })
+ )}
+
+
+ ) : (
+ <>>
+ )}
+
);
};
diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx
index d74f61a..a302bcd 100644
--- a/frontend/src/pages/Login.tsx
+++ b/frontend/src/pages/Login.tsx
@@ -10,12 +10,12 @@ const LoginPage: React.FC = () => {
const [password, setPassword] = useState("");
const [username, setName] = useState("");
const { startLoading, stopLoading } = useLoading();
- const { auth, setAuth, client, setApiKeyId } = useAuth();
+ const { auth, is_auth, setAuth, client, setApiKeyId } = useAuth();
const navigate = useNavigate();
const { addAlert } = useAlertQueue();
useEffect(() => {
- if (auth?.is_auth) navigate("/collections");
- }, [auth]);
+ if (is_auth && auth?.is_auth) navigate("/collections");
+ }, [is_auth, auth?.is_auth]);
// Toggle between login and signup forms
const handleSwitch = () => {
setIsSignup(!isSignup);
@@ -58,9 +58,9 @@ const LoginPage: React.FC = () => {
};
return (
-
-
-
+
+
+
{isSignup ? "Sign Up" : "Login"}
-