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
bOZa=5ECsvRPtgOVfAL0oX1c{_EdIX$Go=nF
literal 0
HcmV?d00001
diff --git a/src/components/atoms/HomeElements/spacePic.png b/src/assets/images/spacePic.png
similarity index 100%
rename from src/components/atoms/HomeElements/spacePic.png
rename to src/assets/images/spacePic.png
diff --git a/src/components/common/Button/Button.css b/src/components/common/Button/Button.css
new file mode 100644
index 0000000..443d09f
--- /dev/null
+++ b/src/components/common/Button/Button.css
@@ -0,0 +1,24 @@
+// src/components/common/Button/Button.css
+.button {
+ padding: var(--spacing-sm) var(--spacing-md);
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: var(--font-size-md);
+ transition: background-color 0.3s ease;
+}
+
+.button-primary {
+ background-color: var(--primary-color);
+ color: var(--secondary-color);
+}
+
+.button-secondary {
+ background-color: var(--secondary-color);
+ color: var(--primary-color);
+}
+
+.button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
\ No newline at end of file
diff --git a/src/components/common/Button/Button.tsx b/src/components/common/Button/Button.tsx
new file mode 100644
index 0000000..ecb6da9
--- /dev/null
+++ b/src/components/common/Button/Button.tsx
@@ -0,0 +1,25 @@
+// src/components/common/Button/Button.tsx
+import React from 'react';
+
+interface ButtonProps {
+ variant: 'primary' | 'secondary';
+ children: React.ReactNode;
+ onClick?: (event: React.MouseEvent) => void;
+ disabled?: boolean;
+ type?: 'button' | 'submit' | 'reset';
+}
+
+const Button: React.FC = ({ variant, children, onClick, disabled = false, type = 'button' }) => {
+ return (
+
+ );
+};
+
+export default Button;
\ No newline at end of file
diff --git a/src/components/common/ErrorMessage/ErrorMessage.css b/src/components/common/ErrorMessage/ErrorMessage.css
new file mode 100644
index 0000000..32a5eab
--- /dev/null
+++ b/src/components/common/ErrorMessage/ErrorMessage.css
@@ -0,0 +1,9 @@
+// src/components/common/ErrorMessage/ErrorMessage.css
+.error-message {
+ background-color: #fff5f5;
+ border: 1px solid #ffa8a8;
+ border-radius: 4px;
+ padding: var(--spacing-sm);
+ margin-bottom: var(--spacing-md);
+ color: #ff6b6b;
+}
\ No newline at end of file
diff --git a/src/components/common/ErrorMessage/ErrorMessage.tsx b/src/components/common/ErrorMessage/ErrorMessage.tsx
new file mode 100644
index 0000000..56f5d76
--- /dev/null
+++ b/src/components/common/ErrorMessage/ErrorMessage.tsx
@@ -0,0 +1,17 @@
+// src/components/common/ErrorMessage/ErrorMessage.tsx
+import './ErrorMessage.css';
+import React from 'react';
+
+interface ErrorMessageProps {
+ message: string;
+}
+
+const ErrorMessage: React.FC = ({ message }) => {
+ return (
+
+ );
+};
+
+export default ErrorMessage;
\ No newline at end of file
diff --git a/src/components/common/ImageTile/ImageTile.tsx b/src/components/common/ImageTile/ImageTile.tsx
new file mode 100644
index 0000000..fbfee18
--- /dev/null
+++ b/src/components/common/ImageTile/ImageTile.tsx
@@ -0,0 +1,18 @@
+// src/components/common/ImageTile/ImageTile.tsx
+import { Image } from '../../../types/image';
+import React from 'react';
+
+interface ImageTileProps {
+ image: Image;
+}
+
+const ImageTile: React.FC = ({ image }) => {
+ return (
+
+
+ {image.name}
+
+ );
+};
+
+export default ImageTile;
\ No newline at end of file
diff --git a/src/components/common/Input/Input.css b/src/components/common/Input/Input.css
new file mode 100644
index 0000000..a93a1ef
--- /dev/null
+++ b/src/components/common/Input/Input.css
@@ -0,0 +1,25 @@
+// src/components/common/Input/Input.css
+.input-group {
+ margin-bottom: var(--spacing-md);
+}
+
+.input-group label {
+ display: block;
+ margin-bottom: var(--spacing-xs);
+ color: var(--text-color);
+}
+
+.input-group input {
+ width: 100%;
+ padding: var(--spacing-sm);
+ border: 1px solid var(--accent-color);
+ border-radius: 4px;
+ font-size: var(--font-size-md);
+ background-color: var(--background-color);
+ color: var(--text-color);
+}
+
+.input-group .required {
+ color: #ff6b6b;
+ margin-left: var(--spacing-xs);
+}
\ No newline at end of file
diff --git a/src/components/common/Input/Input.tsx b/src/components/common/Input/Input.tsx
new file mode 100644
index 0000000..3f6d2c3
--- /dev/null
+++ b/src/components/common/Input/Input.tsx
@@ -0,0 +1,42 @@
+// src/components/common/Input/Input.tsx
+import React from 'react';
+
+interface InputProps {
+ type: 'text' | 'number' | 'email' | 'password' | 'date';
+ label: string;
+ value: string;
+ onChange: (e: React.ChangeEvent) => void;
+ name: string;
+ placeholder?: string;
+ required?: boolean;
+ error?: string;
+}
+
+const Input: React.FC = ({
+ type,
+ label,
+ value,
+ onChange,
+ name,
+ placeholder,
+ required = false,
+ error
+}) => {
+ return (
+
+
+
+ {error && {error}}
+
+ );
+};
+
+export default Input;
\ No newline at end of file
diff --git a/src/components/common/LoadingMessage/LoadingMessage.tsx b/src/components/common/LoadingMessage/LoadingMessage.tsx
new file mode 100644
index 0000000..b59d31c
--- /dev/null
+++ b/src/components/common/LoadingMessage/LoadingMessage.tsx
@@ -0,0 +1,8 @@
+// src/components/common/LoadingMessage/LoadingMessage.tsx
+import React from 'react';
+
+const LoadingMessage: React.FC = () => {
+ return Loading... ;
+};
+
+export default LoadingMessage;
\ No newline at end of file
diff --git a/src/components/common/Modal/Modal.css b/src/components/common/Modal/Modal.css
new file mode 100644
index 0000000..6e06d0e
--- /dev/null
+++ b/src/components/common/Modal/Modal.css
@@ -0,0 +1,39 @@
+// src/components/common/Modal/Modal.css
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.modal {
+ background-color: var(--background-color);
+ padding: var(--spacing-lg);
+ border-radius: 4px;
+ width: 80%;
+ max-width: 500px;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-md);
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: var(--font-size-xl);
+ cursor: pointer;
+ color: var(--text-color);
+}
+
+.modal-content {
+ color: var(--text-color);
+}
\ No newline at end of file
diff --git a/src/components/common/Modal/Modal.tsx b/src/components/common/Modal/Modal.tsx
new file mode 100644
index 0000000..d8da289
--- /dev/null
+++ b/src/components/common/Modal/Modal.tsx
@@ -0,0 +1,30 @@
+// src/components/common/Modal/Modal.tsx
+import React from 'react';
+import './Modal.css';
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ children: React.ReactNode;
+}
+
+const Modal: React.FC = ({ isOpen, onClose, title, children }) => {
+ if (!isOpen) return null;
+
+ return (
+
+ e.stopPropagation()}>
+
+ {title}
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export default Modal;
diff --git a/src/components/features/Gallery/GalleryHeader.tsx b/src/components/features/Gallery/GalleryHeader.tsx
new file mode 100644
index 0000000..8981967
--- /dev/null
+++ b/src/components/features/Gallery/GalleryHeader.tsx
@@ -0,0 +1,16 @@
+// src/components/features/Gallery/GalleryHeader.tsx
+import React from 'react';
+
+const GalleryHeader: React.FC = () => {
+ return (
+
+ Gallery Search
+
+
+
+
+
+ );
+};
+
+export default GalleryHeader;
\ No newline at end of file
diff --git a/src/components/features/Gallery/GalleryList.tsx b/src/components/features/Gallery/GalleryList.tsx
new file mode 100644
index 0000000..83e7d9f
--- /dev/null
+++ b/src/components/features/Gallery/GalleryList.tsx
@@ -0,0 +1,24 @@
+// src/components/features/Gallery/GalleryList.tsx
+import ErrorMessage from '../../common/ErrorMessage/ErrorMessage';
+import ImageTile from '../../common/ImageTile/ImageTile';
+import LoadingMessage from '../../common/LoadingMessage/LoadingMessage';
+import React from 'react';
+import useFetchImageList from '../../../hooks/useFetchImageList';
+
+const GalleryList: React.FC = () => {
+ const { isLoading, isError, data, error } = useFetchImageList();
+
+ if (isLoading) return ;
+ if (isError) return ;
+ if (!data) return ;
+
+ return (
+
+ {data.map((image) => (
+
+ ))}
+
+ );
+};
+
+export default GalleryList;
\ No newline at end of file
diff --git a/src/components/features/Home/HomeHero.tsx b/src/components/features/Home/HomeHero.tsx
new file mode 100644
index 0000000..b219763
--- /dev/null
+++ b/src/components/features/Home/HomeHero.tsx
@@ -0,0 +1,16 @@
+// src/components/features/Home/HomeHero.tsx
+import { Link } from 'react-router-dom';
+import React from 'react';
+
+const HomeHero: React.FC = () => {
+ return (
+
+
+ Send us your Satellite Streak
+ Learn more
+
+
+ );
+};
+
+export default HomeHero;
\ No newline at end of file
diff --git a/src/components/features/Home/HomeImageList.tsx b/src/components/features/Home/HomeImageList.tsx
new file mode 100644
index 0000000..2e7e16b
--- /dev/null
+++ b/src/components/features/Home/HomeImageList.tsx
@@ -0,0 +1,30 @@
+// src/components/features/Home/HomeImageList.tsx
+import ErrorMessage from '../../common/ErrorMessage/ErrorMessage';
+import ImageTile from '../../common/ImageTile/ImageTile';
+import { Link } from 'react-router-dom';
+import LoadingMessage from '../../common/LoadingMessage/LoadingMessage';
+import React from 'react';
+import useFetchImageList from '../../../hooks/useFetchImageList';
+
+const HomeImageList: React.FC = () => {
+ const { isLoading, isError, data, error } = useFetchImageList();
+
+ if (isLoading) return ;
+ if (isError) return ;
+ if (!data) return ;
+
+ const firstRowImages = data.slice(0, 6);
+
+ return (
+
+
+ {firstRowImages.map((image) => (
+
+ ))}
+
+ View Gallery ›
+
+ );
+};
+
+export default HomeImageList;
\ No newline at end of file
diff --git a/src/components/features/ImageInfo/ImageDetails.tsx b/src/components/features/ImageInfo/ImageDetails.tsx
new file mode 100644
index 0000000..47aaa14
--- /dev/null
+++ b/src/components/features/ImageInfo/ImageDetails.tsx
@@ -0,0 +1,27 @@
+// src/components/features/ImageInfo/ImageDetails.tsx
+import { Image } from '../../../types/image';
+import React from 'react';
+
+interface ImageDetailsProps {
+ image: Image;
+}
+
+const ImageDetails: React.FC = ({ image }) => {
+ return (
+
+
+
+ {image.name}
+ Uploader: {image.uploader}
+ Upload Date: {new Date(image.uploadDate).toLocaleDateString()}
+
+ {image.tags.map((tag, index) => (
+ {tag}
+ ))}
+
+
+
+ );
+};
+
+export default ImageDetails;
\ No newline at end of file
diff --git a/src/components/features/Upload/DragAndDrop.tsx b/src/components/features/Upload/DragAndDrop.tsx
new file mode 100644
index 0000000..93d3de5
--- /dev/null
+++ b/src/components/features/Upload/DragAndDrop.tsx
@@ -0,0 +1,18 @@
+// src/components/features/Upload/DragAndDrop.tsx
+import React from 'react';
+
+const DragAndDrop: React.FC = () => {
+ return (
+
+
+ Drag and drop your files here
+ or
+
+
+
+ );
+};
+
+export default DragAndDrop;
\ No newline at end of file
diff --git a/src/components/features/Upload/UploadForm.tsx b/src/components/features/Upload/UploadForm.tsx
new file mode 100644
index 0000000..5969da3
--- /dev/null
+++ b/src/components/features/Upload/UploadForm.tsx
@@ -0,0 +1,95 @@
+// src/components/features/Upload/UploadForm.tsx
+import React, { useState } from 'react';
+import Button from '../../common/Button/Button';
+import Input from '../../common/Input/Input';
+
+const UploadForm: React.FC = () => {
+ const [formData, setFormData] = useState({
+ name: '',
+ observatory: '',
+ ra: '',
+ dec: '',
+ jd: '',
+ ed: '',
+ isFits: false,
+ streakType: {
+ cr: false,
+ rso: false,
+ neo: false,
+ da: false,
+ other: false
+ },
+ permissions: {
+ ml: false,
+ publish: false
+ }
+ });
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value, type, checked } = e.target;
+ setFormData(prevState => ({
+ ...prevState,
+ [name]: type === 'checkbox' ? checked : value
+ }));
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ console.log(formData);
+ // Handle form submission logic here
+ };
+
+ return (
+
+ );
+};
+
+export default UploadForm;
diff --git a/src/components/layout/Footer/Footer.tsx b/src/components/layout/Footer/Footer.tsx
new file mode 100644
index 0000000..db2c4a8
--- /dev/null
+++ b/src/components/layout/Footer/Footer.tsx
@@ -0,0 +1,35 @@
+// src/components/layout/Footer/Footer.tsx
+import { Link } from 'react-router-dom';
+import React from 'react';
+
+const Footer: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Footer;
\ No newline at end of file
diff --git a/src/components/layout/NavBar/NavBar.tsx b/src/components/layout/NavBar/NavBar.tsx
new file mode 100644
index 0000000..a33a282
--- /dev/null
+++ b/src/components/layout/NavBar/NavBar.tsx
@@ -0,0 +1,31 @@
+// src/components/layout/NavBar/NavBar.tsx
+import { Link } from 'react-router-dom';
+import React from 'react';
+
+const NavBar: React.FC = () => {
+ return (
+
+ );
+};
+
+export default NavBar;
\ No newline at end of file
diff --git a/src/constants/api.ts b/src/constants/api.ts
new file mode 100644
index 0000000..e3c97f1
--- /dev/null
+++ b/src/constants/api.ts
@@ -0,0 +1,28 @@
+// src/constants/api.ts
+
+export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'https://api.example.com';
+
+export const API_ENDPOINTS = {
+ IMAGES: '/images',
+ USERS: '/users',
+ LOGIN: '/login',
+ REGISTER: '/register',
+ UPLOAD: '/upload',
+};
+
+export const API_METHODS = {
+ GET: 'GET',
+ POST: 'POST',
+ PUT: 'PUT',
+ DELETE: 'DELETE',
+};
+
+export const API_HEADERS = {
+ CONTENT_TYPE: 'Content-Type',
+ AUTHORIZATION: 'Authorization',
+};
+
+export const CONTENT_TYPES = {
+ JSON: 'application/json',
+ FORM_DATA: 'multipart/form-data',
+};
\ No newline at end of file
diff --git a/src/constants/index.ts b/src/constants/index.ts
new file mode 100644
index 0000000..5e45934
--- /dev/null
+++ b/src/constants/index.ts
@@ -0,0 +1,6 @@
+// src/constants/index.ts
+
+export * from './api';
+export * from './routes';
+export * from './ui';
+export * from './validation';
\ No newline at end of file
diff --git a/src/constants/routes.ts b/src/constants/routes.ts
new file mode 100644
index 0000000..2547fc4
--- /dev/null
+++ b/src/constants/routes.ts
@@ -0,0 +1,19 @@
+// src/constants/routes.ts
+
+export const ROUTES = {
+ HOME: '/',
+ GALLERY: '/gallery',
+ IMAGE_DETAILS: '/image/:id',
+ UPLOAD: '/upload',
+ LOGIN: '/login',
+ REGISTER: '/register',
+ PROFILE: '/profile',
+ NOT_FOUND: '*',
+};
+
+export const generatePath = (route: string, params: Record) => {
+ return Object.entries(params).reduce(
+ (path, [key, value]) => path.replace(`:${key}`, value),
+ route
+ );
+};
\ No newline at end of file
diff --git a/src/constants/ui.ts b/src/constants/ui.ts
new file mode 100644
index 0000000..33c43f3
--- /dev/null
+++ b/src/constants/ui.ts
@@ -0,0 +1,41 @@
+// src/constants/ui.ts
+
+export const BREAKPOINTS = {
+ SM: 640,
+ MD: 768,
+ LG: 1024,
+ XL: 1280,
+};
+
+export const Z_INDEX = {
+ MODAL: 1000,
+ DROPDOWN: 100,
+ HEADER: 50,
+};
+
+export const COLORS = {
+ PRIMARY: '#1C2533',
+ SECONDARY: '#E0E8F4',
+ ACCENT: '#7A8CA8',
+ ERROR: '#ff6b6b',
+ SUCCESS: '#51cf66',
+ WARNING: '#fcc419',
+};
+
+export const FONT_SIZES = {
+ XS: '12px',
+ SM: '14px',
+ MD: '16px',
+ LG: '18px',
+ XL: '24px',
+ XXL: '32px',
+};
+
+export const SPACING = {
+ XS: '4px',
+ SM: '8px',
+ MD: '16px',
+ LG: '24px',
+ XL: '32px',
+ XXL: '48px',
+};
\ No newline at end of file
diff --git a/src/constants/validation.ts b/src/constants/validation.ts
new file mode 100644
index 0000000..4a8c7ab
--- /dev/null
+++ b/src/constants/validation.ts
@@ -0,0 +1,30 @@
+// src/constants/validation.ts
+
+export const PASSWORD_REQUIREMENTS = {
+ MIN_LENGTH: 8,
+ REQUIRE_UPPERCASE: true,
+ REQUIRE_LOWERCASE: true,
+ REQUIRE_NUMBER: true,
+ REQUIRE_SPECIAL_CHAR: true,
+};
+
+export const USERNAME_REQUIREMENTS = {
+ MIN_LENGTH: 3,
+ MAX_LENGTH: 20,
+ ALLOW_SPECIAL_CHARS: false,
+};
+
+export const IMAGE_UPLOAD = {
+ MAX_FILE_SIZE: 5 * 1024 * 1024, // 5MB
+ ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif'],
+ MAX_DIMENSION: 4096, // pixels
+};
+
+export const ERROR_MESSAGES = {
+ REQUIRED_FIELD: 'This field is required',
+ INVALID_EMAIL: 'Please enter a valid email address',
+ INVALID_PASSWORD: 'Password does not meet the requirements',
+ INVALID_USERNAME: 'Username must be between 3 and 20 characters',
+ FILE_TOO_LARGE: 'File size exceeds the maximum limit',
+ INVALID_FILE_TYPE: 'Invalid file type',
+};
\ No newline at end of file
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..9ff9506
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,80 @@
+// src/context/AuthContext.tsx
+import { fetchUserProfile, login } from '../services/api/userApi';
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { User } from '../types';
+
+interface AuthContextType {
+ user: User | null;
+ isLoading: boolean;
+ error: string | null;
+ login: (email: string, password: string) => Promise;
+ logout: () => Promise;
+}
+
+const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const initAuth = async () => {
+ try {
+ const currentUser = await fetchUserProfile();
+ setUser(currentUser);
+ } catch (err) {
+ console.error('Failed to get current user:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ initAuth();
+ }, []);
+
+ const loginHandler = async (email: string, password: string) => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const user = await login({ email, password });
+ setUser(user);
+ } catch (err) {
+ setError('Failed to login. Please check your credentials.');
+ throw err;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const logoutHandler = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ // Implement logout logic here
+ setUser(null);
+ } catch (err) {
+ setError('Failed to logout.');
+ throw err;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const value = {
+ user,
+ isLoading,
+ error,
+ login: loginHandler,
+ logout: logoutHandler,
+ };
+
+ return {children};
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (context === undefined) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/context/UIContext.tsx b/src/context/UIContext.tsx
new file mode 100644
index 0000000..1ac388f
--- /dev/null
+++ b/src/context/UIContext.tsx
@@ -0,0 +1,36 @@
+// src/context/UIContext.tsx
+import React, { createContext, useContext, useState } from 'react';
+
+interface UIContextType {
+ isNavbarOpen: boolean;
+ toggleNavbar: () => void;
+ theme: 'light' | 'dark';
+ toggleTheme: () => void;
+}
+
+const UIContext = createContext(undefined);
+
+export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const [isNavbarOpen, setIsNavbarOpen] = useState(false);
+ const [theme, setTheme] = useState<'light' | 'dark'>('light');
+
+ const toggleNavbar = () => setIsNavbarOpen(prev => !prev);
+ const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
+
+ const value = {
+ isNavbarOpen,
+ toggleNavbar,
+ theme,
+ toggleTheme,
+ };
+
+ return {children};
+};
+
+export const useUI = () => {
+ const context = useContext(UIContext);
+ if (context === undefined) {
+ throw new Error('useUI must be used within a UIProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/src/context/index.ts b/src/context/index.ts
new file mode 100644
index 0000000..707c97a
--- /dev/null
+++ b/src/context/index.ts
@@ -0,0 +1,3 @@
+// src/context/index.ts
+export { AuthProvider, useAuth } from './AuthContext';
+export { UIProvider, useUI } from './UIContext';
\ No newline at end of file
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
new file mode 100644
index 0000000..a2880b0
--- /dev/null
+++ b/src/hooks/index.ts
@@ -0,0 +1,4 @@
+// src/hooks/index.ts
+export { default as useFetchImageList } from './useFetchImageList';
+export { default as useFetchImage } from './useFetchImage';
+export { default as useFormState } from './useFormState';
\ No newline at end of file
diff --git a/src/hooks/useFetchImage.ts b/src/hooks/useFetchImage.ts
new file mode 100644
index 0000000..b351947
--- /dev/null
+++ b/src/hooks/useFetchImage.ts
@@ -0,0 +1,16 @@
+// src/hooks/useFetchImage.ts
+import { useQuery, UseQueryResult } from 'react-query';
+import { fetchImage } from '../services/api/imageApi';
+import { Image } from '../types';
+
+export const useFetchImage = (imageId: string | undefined): UseQueryResult => {
+ return useQuery(
+ ['image', imageId],
+ () => fetchImage(imageId!),
+ {
+ enabled: !!imageId,
+ }
+ );
+};
+
+export default useFetchImage;
\ No newline at end of file
diff --git a/src/hooks/useFetchImageList.ts b/src/hooks/useFetchImageList.ts
index 759f8fb..74f2f62 100644
--- a/src/hooks/useFetchImageList.ts
+++ b/src/hooks/useFetchImageList.ts
@@ -1,13 +1,17 @@
-import fetchImageList from "../api/fetchImageList";
-import { ImageList } from "../types/domain/images";
-import { imageResponseListToImageList } from "../types/mapper/images";
-import { useQuery } from "react-query";
+// src/hooks/useFetchImageList.ts
+import { useQuery, UseQueryResult } from 'react-query';
+import { fetchImageList } from '../services/api/imageApi';
+import { Image } from '../types/image';
-const useFetchImageList = () => {
- return useQuery("image-list", async () => {
- const result = await fetchImageList();
- return imageResponseListToImageList(result);
- });
+export const useFetchImageList = (limit?: number): UseQueryResult => {
+ return useQuery(
+ ['imageList', limit],
+ () => fetchImageList(limit),
+ {
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ cacheTime: 30 * 60 * 1000, // 30 minutes
+ }
+ );
};
export default useFetchImageList;
diff --git a/src/hooks/useFormState.ts b/src/hooks/useFormState.ts
new file mode 100644
index 0000000..adff51d
--- /dev/null
+++ b/src/hooks/useFormState.ts
@@ -0,0 +1,54 @@
+// src/hooks/useFormState.ts
+import { ChangeEvent , useState} from 'react';
+
+type FormState = {
+ values: T;
+ errors: Partial>;
+};
+
+export const useFormState = >(initialState: T) => {
+ const [formState, setFormState] = useState>({
+ values: initialState,
+ errors: {},
+ });
+
+ const handleChange = (e: ChangeEvent) => {
+ const { name, value, type, checked } = e.target;
+ setFormState((prev) => ({
+ ...prev,
+ values: {
+ ...prev.values,
+ [name]: type === 'checkbox' ? checked : value,
+ },
+ }));
+ };
+
+ const setFieldValue = (name: keyof T, value: any) => {
+ setFormState((prev) => ({
+ ...prev,
+ values: {
+ ...prev.values,
+ [name]: value,
+ },
+ }));
+ };
+
+ const setFieldError = (name: keyof T, error: string | null) => {
+ setFormState((prev) => ({
+ ...prev,
+ errors: error
+ ? { ...prev.errors, [name]: error }
+ : { ...prev.errors, [name]: undefined },
+ }));
+ };
+
+ return {
+ values: formState.values,
+ errors: formState.errors,
+ handleChange,
+ setFieldValue,
+ setFieldError,
+ };
+};
+
+export default useFormState;
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index d759b14..2deeff5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,25 +1,28 @@
-import { QueryClient, QueryClientProvider } from "react-query";
-import ReactDOM from "react-dom/client";
+// src/index.tsx
+import { QueryClient, QueryClientProvider } from 'react-query';
+import App from './App';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { ReactQueryDevtools } from 'react-query/devtools';
-import "src/styles/globals.css";
-import AppRouter from "src/router/AppRouter";
+
+// Initialize React Query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
- cacheTime: 0,
},
},
});
-const root = ReactDOM.createRoot(
- document.getElementById("root") as HTMLElement
-);
-
-root.render(
-
-
-
-);
+ReactDOM.render(
+
+
+
+
+
+ ,
+ document.getElementById('root')
+);
\ No newline at end of file
diff --git a/src/pages/GalleryPage.tsx b/src/pages/GalleryPage.tsx
new file mode 100644
index 0000000..79c2d7f
--- /dev/null
+++ b/src/pages/GalleryPage.tsx
@@ -0,0 +1,15 @@
+// src/pages/GalleryPage.tsx
+import GalleryHeader from '../components/features/Gallery/GalleryHeader';
+import GalleryList from '../components/features/Gallery/GalleryList';
+import React from 'react';
+
+const GalleryPage: React.FC = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default GalleryPage;
\ No newline at end of file
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..f05b746
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,15 @@
+// src/pages/HomePage.tsx
+import HomeHero from '../components/features/Home/HomeHero';
+import HomeImageList from '../components/features/Home/HomeImageList';
+import React from 'react';
+
+const HomePage: React.FC = () => {
+ return (
+
+
+
+
+ );
+};
+
+export default HomePage;
\ No newline at end of file
diff --git a/src/pages/ImageInfoPage.tsx b/src/pages/ImageInfoPage.tsx
new file mode 100644
index 0000000..82f3fe0
--- /dev/null
+++ b/src/pages/ImageInfoPage.tsx
@@ -0,0 +1,24 @@
+// src/pages/ImageInfoPage.tsx
+import ErrorMessage from '../components/common/ErrorMessage/ErrorMessage';
+import ImageDetails from '../components/features/ImageInfo/ImageDetails';
+import LoadingMessage from '../components/common/LoadingMessage/LoadingMessage';
+import React from 'react';
+import { useFetchImage } from '../hooks';
+import { useParams } from 'react-router-dom';
+
+const ImageInfoPage: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const { data: image, isLoading, error } = useFetchImage(id);
+
+ if (isLoading) return ;
+ if (error) return ;
+ if (!image) return ;
+
+ return (
+
+
+
+ );
+};
+
+export default ImageInfoPage;
\ No newline at end of file
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..02dfc0d
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,95 @@
+// src/pages/LoginPage.tsx
+import React, { useState } from 'react';
+
+import Button from '../components/common/Button/Button';
+import ErrorMessage from '../components/common/ErrorMessage/ErrorMessage';
+import Input from '../components/common/Input/Input';
+
+import { isValidEmail } from '../utils/validationUtils';
+
+import { ROUTES } from '../constants';
+import { useAuth } from '../context';
+import { useFormState } from '../hooks';
+import { useNavigate } from 'react-router-dom';
+
+interface LoginForm {
+ email: string;
+ password: string;
+}
+
+const LoginPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { login } = useAuth();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const { values, handleChange, errors, setFieldError } = useFormState({
+ email: '',
+ password: '',
+ });
+
+ const validateForm = (): boolean => {
+ let isValid = true;
+ if (!values.email) {
+ setFieldError('email', 'Email is required');
+ isValid = false;
+ } else if (!isValidEmail(values.email)) {
+ setFieldError('email', 'Invalid email format');
+ isValid = false;
+ }
+ if (!values.password) {
+ setFieldError('password', 'Password is required');
+ isValid = false;
+ }
+ return isValid;
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+
+ if (!validateForm()) return;
+
+ setIsLoading(true);
+ try {
+ await login(values.email, values.password);
+ navigate(ROUTES.HOME);
+ } catch (err) {
+ setError('Failed to login. Please check your credentials.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ Login
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..77dc370
--- /dev/null
+++ b/src/pages/NotFoundPage.tsx
@@ -0,0 +1,15 @@
+// src/pages/NotFoundPage.tsx
+import { Link } from 'react-router-dom';
+import React from 'react';
+
+const NotFoundPage: React.FC = () => {
+ return (
+
+ 404 - Page Not Found
+ The page you are looking for does not exist.
+ Return to Home
+
+ );
+};
+
+export default NotFoundPage;
\ No newline at end of file
diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx
new file mode 100644
index 0000000..b02bb10
--- /dev/null
+++ b/src/pages/RegisterPage.tsx
@@ -0,0 +1,13 @@
+// src/pages/RegisterPage.tsx
+import React from 'react';
+
+const RegisterPage: React.FC = () => {
+ return (
+
+ Register
+ {/* Add registration form here */}
+
+ );
+};
+
+export default RegisterPage;
\ No newline at end of file
diff --git a/src/pages/UploadPage.tsx b/src/pages/UploadPage.tsx
new file mode 100644
index 0000000..575b69e
--- /dev/null
+++ b/src/pages/UploadPage.tsx
@@ -0,0 +1,17 @@
+// src/pages/UploadPage.tsx
+import DragAndDrop from '../components/features/Upload/DragAndDrop';
+import React from 'react';
+import UploadForm from '../components/features/Upload/UploadForm';
+
+
+const UploadPage: React.FC = () => {
+ return (
+
+ Upload Your Image
+
+
+
+ );
+};
+
+export default UploadPage;
diff --git a/src/services/api/client.ts b/src/services/api/client.ts
new file mode 100644
index 0000000..d4ac728
--- /dev/null
+++ b/src/services/api/client.ts
@@ -0,0 +1,60 @@
+// src/services/api/client.ts
+
+const BASE_URL = process.env.REACT_APP_API_BASE_URL || 'https://api.example.com';
+
+class ApiClient {
+ private getHeaders(): HeadersInit {
+ const headers: HeadersInit = {
+ 'Content-Type': 'application/json',
+ };
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ return headers;
+ }
+
+ private async handleResponse(response: Response) {
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || 'An error occurred');
+ }
+ return response.json();
+ }
+
+ public async get(url: string): Promise {
+ const response = await fetch(`${BASE_URL}${url}`, {
+ method: 'GET',
+ headers: this.getHeaders(),
+ });
+ return this.handleResponse(response);
+ }
+
+ public async post(url: string, data: any): Promise {
+ const response = await fetch(`${BASE_URL}${url}`, {
+ method: 'POST',
+ headers: this.getHeaders(),
+ body: JSON.stringify(data),
+ });
+ return this.handleResponse(response);
+ }
+
+ public async put(url: string, data: any): Promise {
+ const response = await fetch(`${BASE_URL}${url}`, {
+ method: 'PUT',
+ headers: this.getHeaders(),
+ body: JSON.stringify(data),
+ });
+ return this.handleResponse(response);
+ }
+
+ public async delete(url: string): Promise {
+ const response = await fetch(`${BASE_URL}${url}`, {
+ method: 'DELETE',
+ headers: this.getHeaders(),
+ });
+ return this.handleResponse(response);
+ }
+}
+
+export const apiClient = new ApiClient();
\ No newline at end of file
diff --git a/src/services/api/imageApi.ts b/src/services/api/imageApi.ts
new file mode 100644
index 0000000..5209c0a
--- /dev/null
+++ b/src/services/api/imageApi.ts
@@ -0,0 +1,24 @@
+// src/services/api/imageApi.ts
+
+import { Image, ImageUploadData } from '../../types/image';
+import { apiClient } from './client';
+
+export const fetchImageList = async (limit?: number): Promise => {
+ return apiClient.get(`/images${limit ? `?limit=${limit}` : ''}`);
+};
+
+export const fetchImage = async (id: string): Promise => {
+ return apiClient.get(`/images/${id}`);
+};
+
+export const uploadImage = async (data: ImageUploadData): Promise => {
+ return apiClient.post('/images', data);
+};
+
+export const updateImage = async (id: string, data: Partial): Promise => {
+ return apiClient.put(`/images/${id}`, data);
+};
+
+export const deleteImage = async (id: string): Promise => {
+ return apiClient.delete(`/images/${id}`);
+};
\ No newline at end of file
diff --git a/src/services/api/index.ts b/src/services/api/index.ts
new file mode 100644
index 0000000..e007662
--- /dev/null
+++ b/src/services/api/index.ts
@@ -0,0 +1,3 @@
+// src/services/api/index.ts
+export * from './imageApi';
+export * from './userApi';
\ No newline at end of file
diff --git a/src/services/api/userApi.ts b/src/services/api/userApi.ts
new file mode 100644
index 0000000..a7a0963
--- /dev/null
+++ b/src/services/api/userApi.ts
@@ -0,0 +1,20 @@
+// src/services/api/userApi.ts
+
+import { LoginCredentials, RegistrationData, User } from '../../types/user';
+import { apiClient } from './client';
+
+export const login = async (credentials: LoginCredentials): Promise => {
+ return apiClient.post('/login', credentials);
+};
+
+export const register = async (data: RegistrationData): Promise => {
+ return apiClient.post('/register', data);
+};
+
+export const fetchUserProfile = async (): Promise => {
+ return apiClient.get('/profile');
+};
+
+export const updateUserProfile = async (data: Partial): Promise => {
+ return apiClient.put('/profile', data);
+};
\ No newline at end of file
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 0000000..eb7eaf7
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,55 @@
+/* src/styles/global.css */
+body {
+ font-family: var(--font-family);
+ font-size: var(--font-size-md);
+ color: var(--text-color);
+ background-color: var(--background-color);
+ line-height: 1.5;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: var(--spacing-md);
+}
+
+h1 { font-size: var(--font-size-xl); }
+h2 { font-size: var(--font-size-lg); }
+
+a {
+ color: var(--accent-color);
+ text-decoration: none;
+ transition: color var(--transition-speed) ease;
+}
+
+a:hover {
+ color: var(--secondary-color);
+}
+
+button {
+ cursor: pointer;
+ font-size: var(--font-size-md);
+ padding: var(--spacing-sm) var(--spacing-md);
+ border: none;
+ border-radius: var(--border-radius-sm);
+ background-color: var(--accent-color);
+ color: var(--text-color);
+ transition: background-color var(--transition-speed) ease;
+}
+
+button:hover {
+ background-color: var(--primary-color);
+}
+
+input, textarea, select {
+ font-size: var(--font-size-md);
+ padding: var(--spacing-sm);
+ border: 1px solid var(--accent-color);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--background-color);
+ color: var(--text-color);
+}
+
+.error-message {
+ color: var(--error-color);
+ font-size: var(--font-size-sm);
+ margin-top: var(--spacing-xs);
+}
\ No newline at end of file
diff --git a/src/styles/index.css b/src/styles/index.css
new file mode 100644
index 0000000..f232887
--- /dev/null
+++ b/src/styles/index.css
@@ -0,0 +1,32 @@
+/* src/styles/index.css */
+@import './variables.css';
+@import './utilities.css';
+@import './global.css';
+
+/* You can add any additional global styles or overrides here */
+
+/* Existing page-specific styles */
+.home-page,
+.gallery-page,
+.image-info-page,
+.upload-page,
+.not-found-page {
+ padding: var(--spacing-xl) var(--spacing-lg);
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+/* Navbar styles */
+.navbar {
+ background-color: var(--primary-color);
+ padding: var(--spacing-md) 0;
+}
+
+/* Footer styles */
+.footer {
+ background-color: var(--primary-color);
+ color: var(--text-color);
+ padding: var(--spacing-xl) 0 var(--spacing-md);
+}
+
+/* Add any other existing styles from your current globals.css here */
\ No newline at end of file
diff --git a/src/styles/utilities.css b/src/styles/utilities.css
new file mode 100644
index 0000000..8a42abf
--- /dev/null
+++ b/src/styles/utilities.css
@@ -0,0 +1,35 @@
+/* src/styles/utilities.css */
+.container {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 var(--spacing-md);
+}
+
+.text-center {
+ text-align: center;
+}
+
+.mt-1 { margin-top: var(--spacing-xs); }
+.mt-2 { margin-top: var(--spacing-sm); }
+.mt-3 { margin-top: var(--spacing-md); }
+.mt-4 { margin-top: var(--spacing-lg); }
+.mt-5 { margin-top: var(--spacing-xl); }
+
+.mb-1 { margin-bottom: var(--spacing-xs); }
+.mb-2 { margin-bottom: var(--spacing-sm); }
+.mb-3 { margin-bottom: var(--spacing-md); }
+.mb-4 { margin-bottom: var(--spacing-lg); }
+.mb-5 { margin-bottom: var(--spacing-xl); }
+
+.flex { display: flex; }
+.flex-col { flex-direction: column; }
+.items-center { align-items: center; }
+.justify-center { justify-content: center; }
+.justify-between { justify-content: space-between; }
+
+.rounded-sm { border-radius: var(--border-radius-sm); }
+.rounded-md { border-radius: var(--border-radius-md); }
+.rounded-lg { border-radius: var(--border-radius-lg); }
+
+.transition { transition: all var(--transition-speed) ease; }
\ No newline at end of file
diff --git a/src/styles/variables.css b/src/styles/variables.css
new file mode 100644
index 0000000..84acd9f
--- /dev/null
+++ b/src/styles/variables.css
@@ -0,0 +1,32 @@
+/* src/styles/variables.css */
+:root {
+ /* Colors */
+ --primary-color: #1C2533;
+ --secondary-color: #E0E8F4;
+ --accent-color: #7A8CA8;
+ --background-color: #1C2533;
+ --text-color: #E0E8F4;
+ --error-color: #ff6b6b;
+
+ /* Typography */
+ --font-family: Arial, sans-serif;
+ --font-size-sm: 14px;
+ --font-size-md: 16px;
+ --font-size-lg: 20px;
+ --font-size-xl: 24px;
+
+ /* Spacing */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 16px;
+ --spacing-lg: 24px;
+ --spacing-xl: 32px;
+
+ /* Border Radius */
+ --border-radius-sm: 4px;
+ --border-radius-md: 8px;
+ --border-radius-lg: 16px;
+
+ /* Transitions */
+ --transition-speed: 0.3s;
+}
\ No newline at end of file
diff --git a/src/types/api.ts b/src/types/api.ts
new file mode 100644
index 0000000..962e0f9
--- /dev/null
+++ b/src/types/api.ts
@@ -0,0 +1,18 @@
+// src/types/api.ts
+
+export interface ApiResponse {
+ data: T;
+ message: string;
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ total: number;
+ page: number;
+ pageSize: number;
+}
+
+export interface ApiError {
+ message: string;
+ code: string;
+}
\ No newline at end of file
diff --git a/src/types/image.ts b/src/types/image.ts
new file mode 100644
index 0000000..292cc09
--- /dev/null
+++ b/src/types/image.ts
@@ -0,0 +1,34 @@
+// src/types/image.ts
+
+export type ImageTag = 'HAS_SUN' | 'HAS_ASTEROID' | 'HAS_STAR';
+
+export interface Image {
+ id: string;
+ name: string;
+ url: string;
+ uploader: string;
+ uploadDate: string;
+ tags: ImageTag[];
+}
+
+export interface ImageUploadData {
+ name: string;
+ file: File;
+ observatory: string;
+ rightAscension: number;
+ declination: number;
+ julianDate: string;
+ exposureDuration: number;
+ isFits: boolean;
+ streakType: {
+ cr: boolean;
+ rso: boolean;
+ neo: boolean;
+ da: boolean;
+ other: boolean;
+ };
+ permissions: {
+ ml: boolean;
+ publish: boolean;
+ };
+}
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..19b7376
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,5 @@
+// src/types/index.ts
+
+export * from './image';
+export * from './user';
+export * from './api';
\ No newline at end of file
diff --git a/src/types/user.ts b/src/types/user.ts
new file mode 100644
index 0000000..1383c74
--- /dev/null
+++ b/src/types/user.ts
@@ -0,0 +1,20 @@
+// src/types/user.ts
+
+export interface User {
+ id: string;
+ username: string;
+ email: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface LoginCredentials {
+ email: string;
+ password: string;
+}
+
+export interface RegistrationData {
+ username: string;
+ email: string;
+ password: string;
+}
\ No newline at end of file
diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts
new file mode 100644
index 0000000..644bee2
--- /dev/null
+++ b/src/utils/dateUtils.ts
@@ -0,0 +1,30 @@
+// src/utils/dateUtils.ts
+
+export const formatDate = (date: string | Date): string => {
+ const d = new Date(date);
+ return d.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+};
+
+export const formatDateTime = (date: string | Date): string => {
+ const d = new Date(date);
+ return d.toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+};
+
+export const isValidDate = (date: string): boolean => {
+ return !isNaN(Date.parse(date));
+};
+
+export const getDaysBetweenDates = (start: Date, end: Date): number => {
+ const diff = end.getTime() - start.getTime();
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
+};
\ No newline at end of file
diff --git a/src/utils/imageUtils.ts b/src/utils/imageUtils.ts
new file mode 100644
index 0000000..d1d9650
--- /dev/null
+++ b/src/utils/imageUtils.ts
@@ -0,0 +1,25 @@
+// src/utils/imageUtils.ts
+
+export const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
+ return new Promise((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ resolve({ width: img.width, height: img.height });
+ };
+ img.onerror = reject;
+ img.src = URL.createObjectURL(file);
+ });
+};
+
+export const isValidImageType = (file: File): boolean => {
+ const acceptedTypes = ['image/jpeg', 'image/png', 'image/gif'];
+ return acceptedTypes.includes(file.type);
+};
+
+export const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+};
\ No newline at end of file
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..8bcd424
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,6 @@
+// src/utils/index.ts
+
+export * from './dateUtils';
+export * from './stringUtils';
+export * from './validationUtils';
+export * from './imageUtils';
\ No newline at end of file
diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts
new file mode 100644
index 0000000..a34eb1c
--- /dev/null
+++ b/src/utils/stringUtils.ts
@@ -0,0 +1,25 @@
+// src/utils/stringUtils.ts
+
+export const capitalize = (str: string): string => {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
+export const truncate = (str: string, length: number): string => {
+ if (str.length <= length) return str;
+ return str.slice(0, length) + '...';
+};
+
+export const slugify = (str: string): string => {
+ return str
+ .toLowerCase()
+ .replace(/[^\w ]+/g, '')
+ .replace(/ +/g, '-');
+};
+
+export const getInitials = (name: string): string => {
+ return name
+ .split(' ')
+ .map(n => n[0])
+ .join('')
+ .toUpperCase();
+};
\ No newline at end of file
diff --git a/src/utils/validationUtils.ts b/src/utils/validationUtils.ts
new file mode 100644
index 0000000..94d828b
--- /dev/null
+++ b/src/utils/validationUtils.ts
@@ -0,0 +1,22 @@
+// src/utils/validationUtils.ts
+
+export const isValidEmail = (email: string): boolean => {
+ const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+ return re.test(email);
+};
+
+export const isValidPassword = (password: string): boolean => {
+ // At least 8 characters, 1 uppercase, 1 lowercase, 1 number
+ const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
+ return re.test(password);
+};
+
+export const isValidUsername = (username: string): boolean => {
+ // Alphanumeric, 3-20 characters
+ const re = /^[a-zA-Z0-9]{3,20}$/;
+ return re.test(username);
+};
+
+export const isNotEmpty = (value: string): boolean => {
+ return value.trim().length > 0;
+};
\ No newline at end of file
From 9c212c2300d5439498eb5369888c779520aa8f96 Mon Sep 17 00:00:00 2001
From: Yas <137867978+01YM@users.noreply.github.com>
Date: Sun, 1 Sep 2024 01:00:31 +1000
Subject: [PATCH 2/6] More Stuff
Stuff
---
{src => public}/assets/icons/search-icon.png | Bin
{src => public}/assets/icons/uploadDocs.png | Bin
{src => public}/assets/images/placeholder.png | Bin
{src => public}/assets/images/spacePic.png | Bin
src/App.tsx | 33 +-
src/__test__/mocks/handlers.ts | 43 --
src/__test__/mocks/image-info/1/image.json | 11 -
src/__test__/mocks/image-lists/images.json | 30 --
src/__test__/mocks/server.ts | 3 -
src/__test__/mocks/urls.ts | 5 -
.../render-with-query-client-provider.tsx | 20 -
.../utils/suppress-unsuppress-errors.ts | 17 -
src/api/fetchImageList.test.ts | 24 -
src/api/fetchImageList.ts | 13 -
src/api/headers.ts | 8 -
.../atoms/Description/Description.tsx | 9 -
.../atoms/GalleryElements/GalleryHead.tsx | 21 -
.../atoms/HomeElements/HomeBackgroundPic.tsx | 22 -
.../atoms/HomeLink/HomeLink.test.tsx | 15 -
src/components/atoms/HomeLink/HomeLink.tsx | 11 -
.../LoadingMessage/LoadingMessage.test.tsx | 10 -
.../atoms/LoadingMessage/LoadingMessage.tsx | 9 -
src/components/atoms/NavBar/NavBar.tsx | 32 --
.../atoms/UploadElements/UploadDragnDrop.tsx | 30 --
.../atoms/UploadElements/UploadElements.tsx | 27 -
.../atoms/UploadElements/UploadForm.tsx | 85 ----
.../features/Gallery/GalleryHeader.tsx | 2 +-
src/components/features/Home/spacePic.png | Bin 0 -> 582021 bytes
src/components/layout/Footer/Footer.css | 66 +++
src/components/layout/Footer/Footer.tsx | 1 +
src/components/layout/NavBar/NavBar.css | 66 +++
src/components/layout/NavBar/NavBar.tsx | 3 +-
.../ErrorMessage/ErrorMessage.test.tsx | 16 -
.../molecules/ErrorMessage/ErrorMessage.tsx | 18 -
.../molecules/Health/Health.test.tsx | 23 -
src/components/molecules/Health/Health.tsx | 16 -
.../molecules/ImageDetails/ImageDetails.tsx | 13 -
.../molecules/ImageTile/ImageTile.test.tsx | 24 -
.../molecules/ImageTile/ImageTile.tsx | 29 --
.../organisms/ImagesList/HomeImageList.tsx | 36 --
.../organisms/ImagesList/ImageList.test.tsx | 65 ---
.../organisms/ImagesList/ImageList.tsx | 33 --
src/components/pages/Gallery/Gallery.tsx | 13 -
src/components/pages/Home/Home.test.tsx | 22 -
src/components/pages/Home/Home.tsx | 13 -
.../pages/ImageInfo/ImageInfo.test.tsx | 69 ---
src/components/pages/ImageInfo/ImageInfo.tsx | 44 --
.../pages/NotFound/NotFound.test.tsx | 16 -
src/components/pages/NotFound/NotFound.tsx | 14 -
src/components/pages/Upload/Upload.tsx | 11 -
src/config.ts | 12 -
src/env.test.ts | 57 ---
src/env.ts | 37 --
src/index.tsx | 3 +-
src/router/AppRouter.test.tsx | 14 -
src/router/AppRouter.tsx | 28 --
src/setupTests.ts | 16 -
src/styles/components.css | 91 ++++
src/styles/global.css | 54 +-
src/styles/globals.css | 470 ------------------
src/styles/index.css | 39 +-
src/styles/pages.css | 239 +++++++++
src/styles/utilities.css | 49 +-
src/styles/variables.css | 17 +-
src/types/domain/images.ts | 26 -
src/types/mapper/images.ts | 24 -
src/types/service/header.ts | 3 -
src/types/service/images.ts | 9 -
68 files changed, 536 insertions(+), 1743 deletions(-)
rename {src => public}/assets/icons/search-icon.png (100%)
rename {src => public}/assets/icons/uploadDocs.png (100%)
rename {src => public}/assets/images/placeholder.png (100%)
rename {src => public}/assets/images/spacePic.png (100%)
delete mode 100644 src/__test__/mocks/handlers.ts
delete mode 100644 src/__test__/mocks/image-info/1/image.json
delete mode 100644 src/__test__/mocks/image-lists/images.json
delete mode 100644 src/__test__/mocks/server.ts
delete mode 100644 src/__test__/mocks/urls.ts
delete mode 100644 src/__test__/utils/render-with-query-client-provider.tsx
delete mode 100644 src/__test__/utils/suppress-unsuppress-errors.ts
delete mode 100644 src/api/fetchImageList.test.ts
delete mode 100644 src/api/fetchImageList.ts
delete mode 100644 src/api/headers.ts
delete mode 100644 src/components/atoms/Description/Description.tsx
delete mode 100644 src/components/atoms/GalleryElements/GalleryHead.tsx
delete mode 100644 src/components/atoms/HomeElements/HomeBackgroundPic.tsx
delete mode 100644 src/components/atoms/HomeLink/HomeLink.test.tsx
delete mode 100644 src/components/atoms/HomeLink/HomeLink.tsx
delete mode 100644 src/components/atoms/LoadingMessage/LoadingMessage.test.tsx
delete mode 100644 src/components/atoms/LoadingMessage/LoadingMessage.tsx
delete mode 100644 src/components/atoms/NavBar/NavBar.tsx
delete mode 100644 src/components/atoms/UploadElements/UploadDragnDrop.tsx
delete mode 100644 src/components/atoms/UploadElements/UploadElements.tsx
delete mode 100644 src/components/atoms/UploadElements/UploadForm.tsx
create mode 100644 src/components/features/Home/spacePic.png
create mode 100644 src/components/layout/Footer/Footer.css
create mode 100644 src/components/layout/NavBar/NavBar.css
delete mode 100644 src/components/molecules/ErrorMessage/ErrorMessage.test.tsx
delete mode 100644 src/components/molecules/ErrorMessage/ErrorMessage.tsx
delete mode 100644 src/components/molecules/Health/Health.test.tsx
delete mode 100644 src/components/molecules/Health/Health.tsx
delete mode 100644 src/components/molecules/ImageDetails/ImageDetails.tsx
delete mode 100644 src/components/molecules/ImageTile/ImageTile.test.tsx
delete mode 100644 src/components/molecules/ImageTile/ImageTile.tsx
delete mode 100644 src/components/organisms/ImagesList/HomeImageList.tsx
delete mode 100644 src/components/organisms/ImagesList/ImageList.test.tsx
delete mode 100644 src/components/organisms/ImagesList/ImageList.tsx
delete mode 100644 src/components/pages/Gallery/Gallery.tsx
delete mode 100644 src/components/pages/Home/Home.test.tsx
delete mode 100644 src/components/pages/Home/Home.tsx
delete mode 100644 src/components/pages/ImageInfo/ImageInfo.test.tsx
delete mode 100644 src/components/pages/ImageInfo/ImageInfo.tsx
delete mode 100644 src/components/pages/NotFound/NotFound.test.tsx
delete mode 100644 src/components/pages/NotFound/NotFound.tsx
delete mode 100644 src/components/pages/Upload/Upload.tsx
delete mode 100644 src/config.ts
delete mode 100644 src/env.test.ts
delete mode 100644 src/env.ts
delete mode 100644 src/router/AppRouter.test.tsx
delete mode 100644 src/router/AppRouter.tsx
delete mode 100644 src/setupTests.ts
create mode 100644 src/styles/components.css
delete mode 100644 src/styles/globals.css
create mode 100644 src/styles/pages.css
delete mode 100644 src/types/domain/images.ts
delete mode 100644 src/types/mapper/images.ts
delete mode 100644 src/types/service/header.ts
delete mode 100644 src/types/service/images.ts
diff --git a/src/assets/icons/search-icon.png b/public/assets/icons/search-icon.png
similarity index 100%
rename from src/assets/icons/search-icon.png
rename to public/assets/icons/search-icon.png
diff --git a/src/assets/icons/uploadDocs.png b/public/assets/icons/uploadDocs.png
similarity index 100%
rename from src/assets/icons/uploadDocs.png
rename to public/assets/icons/uploadDocs.png
diff --git a/src/assets/images/placeholder.png b/public/assets/images/placeholder.png
similarity index 100%
rename from src/assets/images/placeholder.png
rename to public/assets/images/placeholder.png
diff --git a/src/assets/images/spacePic.png b/public/assets/images/spacePic.png
similarity index 100%
rename from src/assets/images/spacePic.png
rename to public/assets/images/spacePic.png
diff --git a/src/App.tsx b/src/App.tsx
index 959993a..e42bcfa 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,26 +1,25 @@
-// src/App.tsx
-import './styles/index.css'; // Single import from internal modules
-import { AuthProvider, UIProvider } from './context'; // Multiple imports from internal modules
-import { BrowserRouter as Route, Router, Routes } from 'react-router-dom'; // Multiple imports from an external module
+import './styles/index.css';
-import Footer from './components/layout/Footer/Footer'; // Single import from internal modules
-import GalleryPage from './pages/GalleryPage'; // Single import from internal modules
-import HomePage from './pages/HomePage'; // Single import from internal modules
-import ImageInfoPage from './pages/ImageInfoPage'; // Single import from internal modules
-import LoginPage from './pages/LoginPage'; // Single import from internal modules
-import NavBar from './components/layout/NavBar/NavBar'; // Single import from internal modules
-import NotFoundPage from './pages/NotFoundPage'; // Single import from internal modules
-import React from 'react'; // Single import from an external module
-import RegisterPage from './pages/RegisterPage'; // Single import from internal modules
-import { ROUTES } from './constants'; // Single import from internal modules
-import UploadPage from './pages/UploadPage'; // Single import from internal modules
+import { AuthProvider, UIProvider } from './context';
+import { BrowserRouter, Route, Routes } from 'react-router-dom';
+import { ROUTES } from './constants';
+import Footer from './components/layout/Footer/Footer';
+import GalleryPage from './pages/GalleryPage';
+import HomePage from './pages/HomePage';
+import ImageInfoPage from './pages/ImageInfoPage';
+import LoginPage from './pages/LoginPage';
+import NavBar from './components/layout/NavBar/NavBar';
+import NotFoundPage from './pages/NotFoundPage';
+import React from 'react';
+import RegisterPage from './pages/RegisterPage';
+import UploadPage from './pages/UploadPage';
const App: React.FC = () => {
return (
-
+
@@ -36,7 +35,7 @@ const App: React.FC = () => {
-
+
);
diff --git a/src/__test__/mocks/handlers.ts b/src/__test__/mocks/handlers.ts
deleted file mode 100644
index 0abb686..0000000
--- a/src/__test__/mocks/handlers.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { challengeApi } from "../../config";
-import images from "src/__test__/mocks/image-lists/images.json";
-import { rest } from "msw";
-
-export const Any500Factory = () => {
- return rest.get(`${challengeApi.baseUrl}/*`, (req, res, ctx) => {
- return res.once(ctx.status(500), ctx.json({ message: "Fail" }));
- });
-};
-
-export const imageListSuccess = rest.get(
- `${challengeApi.baseUrl}/images/`,
- (req, res, ctx) => {
- return res(ctx.json(images));
- }
-);
-
-type Options = {
- once?: boolean;
-};
-
-/*
- * Factory to return handlers that return a 500 response for the image info
- * endpoint.
- * once: used to run the handler only once for simulating single failures.
- */
-export const imageList500Factory = ({ once }: Options = {}) => {
- return rest.get(`${challengeApi.baseUrl}/images/`, (req, res, ctx) => {
- if (once) {
- return res.once(ctx.status(500), ctx.json({ message: "Fail" }));
- } else {
- return res(ctx.status(500), ctx.json({ message: "Fail" }));
- }
- });
-};
-
-export const scenarios = {
- success: [imageListSuccess],
- fail: [imageList500Factory()],
- failOnce: [imageListSuccess],
-};
-
-export type Scenario = keyof typeof scenarios;
diff --git a/src/__test__/mocks/image-info/1/image.json b/src/__test__/mocks/image-info/1/image.json
deleted file mode 100644
index bc041c9..0000000
--- a/src/__test__/mocks/image-info/1/image.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "image1",
- "uploader": "Test_User",
- "uploadDate": 1713187881486,
- "url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
- "tags": [
- "HAS_SUN",
- "HAS_ASTEROID",
- "HAS_STAR"
- ]
-}
\ No newline at end of file
diff --git a/src/__test__/mocks/image-lists/images.json b/src/__test__/mocks/image-lists/images.json
deleted file mode 100644
index addb028..0000000
--- a/src/__test__/mocks/image-lists/images.json
+++ /dev/null
@@ -1,30 +0,0 @@
-[
- {
- "name": "image1",
- "uploader": "Test_User",
- "uploadDate": 1713187881486,
- "url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png",
- "tags": [
- "HAS_SUN",
- "HAS_ASTEROID",
- "HAS_STAR"
- ]
- },
- {
- "name": "image2",
- "uploader": "Test_User",
- "uploadDate": 1713187881486,
- "url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png",
- "tags": [
- "HAS_SUN",
- "HAS_STAR"
- ]
- },
- {
- "name": "image3",
- "uploader": "Test_User",
- "uploadDate": 1713187881486,
- "url": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/3.png",
- "tags": []
- }
-]
\ No newline at end of file
diff --git a/src/__test__/mocks/server.ts b/src/__test__/mocks/server.ts
deleted file mode 100644
index bd0bda5..0000000
--- a/src/__test__/mocks/server.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { setupServer } from "msw/node";
-
-export const server = setupServer();
diff --git a/src/__test__/mocks/urls.ts b/src/__test__/mocks/urls.ts
deleted file mode 100644
index f275261..0000000
--- a/src/__test__/mocks/urls.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { challengeApi } from "../../config";
-
-export default {
- movies: `${challengeApi.baseUrl}/images/`,
-};
diff --git a/src/__test__/utils/render-with-query-client-provider.tsx b/src/__test__/utils/render-with-query-client-provider.tsx
deleted file mode 100644
index f943a2b..0000000
--- a/src/__test__/utils/render-with-query-client-provider.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { QueryClient, QueryClientProvider } from "react-query";
-import { render } from "@testing-library/react";
-
-export const renderWithQueryClientProvider = async (children: JSX.Element) => {
- await render(
-
- {children}
-
- );
-};
-
-const getQueryClient = () =>
- new QueryClient({
- defaultOptions: {
- queries: {
- retry: 1,
- retryDelay: 0,
- },
- },
- });
diff --git a/src/__test__/utils/suppress-unsuppress-errors.ts b/src/__test__/utils/suppress-unsuppress-errors.ts
deleted file mode 100644
index 73e1a34..0000000
--- a/src/__test__/utils/suppress-unsuppress-errors.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/* When we fail a request in the tests it is automatically logged to
- * console.error. This is annoying and obfuscates the test output.
- * Provide hooks to be used with jest before / after to silence this.
- * beforeEach(suppressErrors);
- * afterEach(unSuppressErrors);
- */
-const suppressErrors = () => {
- jest.spyOn(console, "error").mockImplementation(() => {
- return;
- });
-};
-
-const unSuppressErrors = () => {
- jest.spyOn(console, "error").mockRestore();
-};
-
-export { suppressErrors, unSuppressErrors };
diff --git a/src/api/fetchImageList.test.ts b/src/api/fetchImageList.test.ts
deleted file mode 100644
index ccfb4a8..0000000
--- a/src/api/fetchImageList.test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {
- suppressErrors,
- unSuppressErrors,
-} from "src/__test__/utils/suppress-unsuppress-errors";
-import fetchImageList from "./fetchImageList";
-import { imageList500Factory } from "src/__test__/mocks/handlers";
-import images from "src/__test__/mocks/image-lists/images.json";
-import { server } from "src/__test__/mocks/server";
-
-describe("fetchImageList", () => {
- it("should successfully fetch a list of images", async () => {
- const result = await fetchImageList();
- expect(result).toEqual(images);
- });
-});
-
-describe("fetchImageList Errors", () => {
- beforeEach(suppressErrors);
- afterEach(unSuppressErrors);
- it("should fail and throw an error message", async () => {
- server.use(imageList500Factory({ once: true }));
- await expect(fetchImageList()).rejects.toThrowError("Fail");
- });
-});
diff --git a/src/api/fetchImageList.ts b/src/api/fetchImageList.ts
deleted file mode 100644
index b46d84f..0000000
--- a/src/api/fetchImageList.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { challengeApi } from "../config";
-import headers from "src/api/headers";
-import { ImageResponseList } from "../types/service/images";
-
-const fetchImageList = async (): Promise => {
- const result = await fetch(`${challengeApi.baseUrl}/images/`, { headers });
-
- const data = await result.json();
- if (!result.ok) throw Error(data.message);
- return data;
-};
-
-export default fetchImageList;
diff --git a/src/api/headers.ts b/src/api/headers.ts
deleted file mode 100644
index 35596ea..0000000
--- a/src/api/headers.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { challengeApi } from "../config";
-import { Header } from "src/types/service/header";
-
-const headers: Header = {
- "x-api-key": challengeApi.apiKey as string,
-};
-
-export default headers;
diff --git a/src/components/atoms/Description/Description.tsx b/src/components/atoms/Description/Description.tsx
deleted file mode 100644
index 3cf38ec..0000000
--- a/src/components/atoms/Description/Description.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-const Description = () => {
- return (
-
- );
-};
-
-export default Description;
diff --git a/src/components/atoms/GalleryElements/GalleryHead.tsx b/src/components/atoms/GalleryElements/GalleryHead.tsx
deleted file mode 100644
index 7431d26..0000000
--- a/src/components/atoms/GalleryElements/GalleryHead.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-const GalleryHead = () => {
- return (
-
-
- Gallery Search*-
-
-
-
-
-
-
-
-
- );
-};
-
-export default GalleryHead;
diff --git a/src/components/atoms/HomeElements/HomeBackgroundPic.tsx b/src/components/atoms/HomeElements/HomeBackgroundPic.tsx
deleted file mode 100644
index 7fa6bd0..0000000
--- a/src/components/atoms/HomeElements/HomeBackgroundPic.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-const HomeBackgroundImage: React.FC = () => {
- return (
-
- );
-};
-
-export default HomeBackgroundImage;
diff --git a/src/components/atoms/HomeLink/HomeLink.test.tsx b/src/components/atoms/HomeLink/HomeLink.test.tsx
deleted file mode 100644
index 3b9d58c..0000000
--- a/src/components/atoms/HomeLink/HomeLink.test.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import HomeLink from "./HomeLink";
-import { MemoryRouter } from "react-router-dom";
-
-describe("HomeLink", () => {
- it("should display", async () => {
- render(
-
-
-
- );
- const link = screen.getByRole("link");
- expect(link).toHaveTextContent("Return Home");
- });
-});
diff --git a/src/components/atoms/HomeLink/HomeLink.tsx b/src/components/atoms/HomeLink/HomeLink.tsx
deleted file mode 100644
index a60e49a..0000000
--- a/src/components/atoms/HomeLink/HomeLink.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Link } from "react-router-dom";
-
-const HomeLink = () => {
- return (
-
- {"Return Home"}
-
- );
-};
-
-export default HomeLink;
diff --git a/src/components/atoms/LoadingMessage/LoadingMessage.test.tsx b/src/components/atoms/LoadingMessage/LoadingMessage.test.tsx
deleted file mode 100644
index 4015e0b..0000000
--- a/src/components/atoms/LoadingMessage/LoadingMessage.test.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import LoadingMessage from "./LoadingMessage";
-
-describe("LoadingMessage", () => {
- it("should display", async () => {
- render();
- const textElement = screen.getByText("Loading...");
- expect(textElement).toBeInTheDocument();
- });
-});
diff --git a/src/components/atoms/LoadingMessage/LoadingMessage.tsx b/src/components/atoms/LoadingMessage/LoadingMessage.tsx
deleted file mode 100644
index b660964..0000000
--- a/src/components/atoms/LoadingMessage/LoadingMessage.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-const LoadingMessage = () => {
- return (
-
- );
-};
-
-export default LoadingMessage;
diff --git a/src/components/atoms/NavBar/NavBar.tsx b/src/components/atoms/NavBar/NavBar.tsx
deleted file mode 100644
index 3027859..0000000
--- a/src/components/atoms/NavBar/NavBar.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-const Navbar = () => {
- return (
-
- );
-};
-
-export default Navbar;
diff --git a/src/components/atoms/UploadElements/UploadDragnDrop.tsx b/src/components/atoms/UploadElements/UploadDragnDrop.tsx
deleted file mode 100644
index b8ae99b..0000000
--- a/src/components/atoms/UploadElements/UploadDragnDrop.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-const UploadDragDrop: React.FC = () => {
- return (
-
-
- {/* Logo image */}
-
-
- {/* Choose files button */}
-
-
-
- {/* Text below the button */}
- or drag here
-
-
- );
-};
-
-export default UploadDragDrop;
diff --git a/src/components/atoms/UploadElements/UploadElements.tsx b/src/components/atoms/UploadElements/UploadElements.tsx
deleted file mode 100644
index db57ff9..0000000
--- a/src/components/atoms/UploadElements/UploadElements.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import UploadDragnDrop from "src/components/atoms/UploadElements/UploadDragnDrop";
-import UploadForm from "src/components/atoms/UploadElements/UploadForm";
-
-const UploadElements: React.FC = () => {
- return (
-
- {/* Line below the navbar */}
-
- {/* Upload Title */}
- Upload
-
- {/* Upload Container */}
-
- {/* Upload Drag and Drop Section */}
-
-
-
- {/* Upload Form Section */}
-
-
-
-
-
- );
-};
-
-export default UploadElements;
diff --git a/src/components/atoms/UploadElements/UploadForm.tsx b/src/components/atoms/UploadElements/UploadForm.tsx
deleted file mode 100644
index 1b895ed..0000000
--- a/src/components/atoms/UploadElements/UploadForm.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-const UploadForm: React.FC = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default UploadForm;
diff --git a/src/components/features/Gallery/GalleryHeader.tsx b/src/components/features/Gallery/GalleryHeader.tsx
index 8981967..d8cd8a0 100644
--- a/src/components/features/Gallery/GalleryHeader.tsx
+++ b/src/components/features/Gallery/GalleryHeader.tsx
@@ -7,7 +7,7 @@ const GalleryHeader: React.FC = () => {
Gallery Search
);
diff --git a/src/components/features/Home/spacePic.png b/src/components/features/Home/spacePic.png
new file mode 100644
index 0000000000000000000000000000000000000000..81590e863eba023dc4bc3b060b02a81827e8b615
GIT binary patch
literal 582021
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 | |