diff --git a/client/src/Assets/Images/secretCloset/.DS_Store b/.DS_Store similarity index 87% rename from client/src/Assets/Images/secretCloset/.DS_Store rename to .DS_Store index 5008ddf..7a73315 100644 Binary files a/client/src/Assets/Images/secretCloset/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 382f778..d296706 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .env -node_modules -.env node_modules/ +package-lock.json \ No newline at end of file diff --git a/Client/Auth/AuthContext.js b/Client/Auth/AuthContext.js new file mode 100644 index 0000000..86e754e --- /dev/null +++ b/Client/Auth/AuthContext.js @@ -0,0 +1,93 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import axios from 'axios'; + +/** + * Context object created + */ +const AuthContext = createContext(); + +/** + * Functional component that will use the Provider functionality in the AuthContext to + * share the state and functions with child components. + * AuthContext : { ..., Provider: {the auth state}} + * @param {*} children deconstructed prop - refers to any nested components wrapped by AuthProvider + * in the virtual DOM (see client/index.js) + * @returns + */ +export const AuthProvider = ({ children }) => { + + /** + * State variables and setters for holding top level authentication information + */ + const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [avatarUrl, setAvatarUrl] = useState(null); + const [loading, setLoading] = useState(true); + + /** + * sessionStorage does not persist beyond the current session, + * only accessible in the same window/tab and cleared when either closed. + * Stored in memory on web browser, not written to disk like cookies + */ + useEffect(() => { + const storedUser = JSON.parse(sessionStorage.getItem('user')); + const storedSession = JSON.parse(sessionStorage.getItem('session')); + const storedAvatarUrl = sessionStorage.getItem('avatarUrl'); + if (storedUser) { + setUser(storedUser); + } + if (storedSession) { + setSession(storedSession); + } + if (storedAvatarUrl) { + setAvatarUrl(storedAvatarUrl); + } + setLoading(false); // Indicates that the data fetching has completed + }, []); + + /** + * Async function that makes a request + * @param {*} user + * @param {*} session + */ + const login = async (user, session) => { + setUser(user); + setSession(session); + fetchAvatarUrl(user.id); + sessionStorage.setItem('user', JSON.stringify(user)); + sessionStorage.setItem('session', JSON.stringify(session)); + }; + + const fetchAvatarUrl = async (userId) => { + try { + const response = await axios.get(`/api/user-profile/${userId}`); + const avatarUrl = response.data.avatar_url; + setAvatarUrl(avatarUrl); + sessionStorage.setItem('avatarUrl', avatarUrl); + } catch (error) { + console.error('Error fetching avatar URL:', error); + } + }; + + const logout = () => { + setUser(null); + setSession(null); + setAvatarUrl(null); + sessionStorage.removeItem('user'); + sessionStorage.removeItem('session'); + sessionStorage.removeItem('avatarUrl'); + }; + + /** + * Using React + */ + return ( + + {children} + + ); +}; + +export const useAuth = () => { + return useContext(AuthContext); +}; diff --git a/Client/Auth/AuthProviderDebugger.jsx b/Client/Auth/AuthProviderDebugger.jsx new file mode 100644 index 0000000..e174172 --- /dev/null +++ b/Client/Auth/AuthProviderDebugger.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { useAuth } from './AuthContext'; // Adjust the path as needed + +const style = { + position: 'fixed', + bottom: '10px', + right: '10px', + width: 300, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 24, + p: 2, + zIndex: 1000, + overflow: 'auto', + maxHeight: '50vh', +}; + +const AuthProviderDebugger = () => { + const { user, session, avatarUrl } = useAuth(); + + return ( + + + AuthContext Debug Info + + +
{JSON.stringify({ user, session, avatarUrl }, null, 2)}
+
+
+ ); +}; + +export default AuthProviderDebugger; diff --git a/Client/Auth/Login.jsx b/Client/Auth/Login.jsx new file mode 100644 index 0000000..dd272e0 --- /dev/null +++ b/Client/Auth/Login.jsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { useHistory } from 'react-router-dom'; +import { Typography, CircularProgress, Snackbar, Alert } from '@mui/material'; +import { useAuth } from './AuthContext'; +import AuthNavigationButton from '../components/AuthNavigationButton'; +import '../styles/AuthForm.css'; + +/** + * User-side functionality for existing user at login and route path + * @returns + */ +const Login = () => { + /** + * local state for email, password, loading + */ + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: '' }); + const history = useHistory(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + const response = await axios.post('/api/login', { email, password }); + const { user, session } = response.data; + login(user, session); + setSnackbar({ open: true, message: 'Login successful', severity: 'success' }); + setTimeout(() => { + history.push('/search'); + }, 1200); + } catch (error) { + setSnackbar({ open: true, message: 'Login failed', severity: 'error' }); + console.error('AxiosError:', error); + } finally { + setLoading(false); + } + }; + + const handleCloseSnackbar = () => { + setSnackbar({ ...snackbar, open: false }); + }; + + return ( +
+
+ Login +
+ + setEmail(e.target.value)} required /> + + setPassword(e.target.value)} required /> + +
+ {loading && ( +
+ +
+ )} +
+ + + + {snackbar.message} + + +
+ ); +}; + +export default Login; diff --git a/Client/Auth/LoginStatus.js b/Client/Auth/LoginStatus.js new file mode 100644 index 0000000..a63350d --- /dev/null +++ b/Client/Auth/LoginStatus.js @@ -0,0 +1,13 @@ +import React from 'react'; +import { useAuth } from './AuthContext'; + +const LoginStatus = () => { + const { user } = useAuth(); + return ( +
+ {user ? `Logged in as: ${user.user_metadata.first_name}` : 'Not logged in'} +
+ ); +}; + +export default LoginStatus; diff --git a/Client/Auth/Signup.jsx b/Client/Auth/Signup.jsx new file mode 100644 index 0000000..0a8db8a --- /dev/null +++ b/Client/Auth/Signup.jsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { useHistory } from 'react-router-dom'; +import { Typography, CircularProgress, Snackbar, Alert } from '@mui/material'; +import { useAuth } from './AuthContext'; +import AuthNavigationButton from '../components/AuthNavigationButton'; +import '../styles/AuthForm.css'; + +const Signup = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [loading, setLoading] = useState(false); + const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: '' }); + const history = useHistory(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + const response = await axios.post('/api/signup', { email, password, firstName, lastName }); + const { user, session } = response.data; + login(user, session); + setTimeout(() => { + setSnackbar({ open: true, message: 'Signup successful', severity: 'success' }); + setTimeout(() => { + history.push('/search'); + }, 2000); // Redirect after 2 seconds to ensure the user sees the Snackbar + }, 1000); // Ensure spinner shows for at least 1 second + } catch (error) { + setTimeout(() => { + setSnackbar({ open: true, message: 'Signup failed', severity: 'error' }); + }, 1000); // Ensure spinner shows for at least 1 second + console.error('AxiosError:', error); + } finally { + setTimeout(() => { + setLoading(false); + }, 1000); // Ensure spinner shows for at least 1 second + } + }; + + const handleCloseSnackbar = () => { + setSnackbar({ ...snackbar, open: false }); + }; + + return ( +
+
+ Signup +
+ + setEmail(e.target.value)} required /> + + setPassword(e.target.value)} required /> + + setFirstName(e.target.value)} required /> + + setLastName(e.target.value)} required /> + +
+ {loading && ( +
+ +
+ )} +
+ + + + {snackbar.message} + + +
+ ); +}; + +export default Signup; diff --git a/Client/Pages/About.jsx b/Client/Pages/About.jsx new file mode 100644 index 0000000..97621ad --- /dev/null +++ b/Client/Pages/About.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Container, Typography, Paper } from '@mui/material'; + +const About = () => { + return ( + {/* Adjust marginTop as needed */} + + + About Fashion Selector + + + Fashion Selector is your AI-powered fashion assistant, designed to help you discover and refine your personal style. + + + Our mission is to make fashion accessible and enjoyable for everyone. Whether you're looking for outfit inspiration or trying to build a wardrobe that truly reflects your personality, Fashion Selector is here to guide you. + + + Explore, experiment, and express yourself through fashion with us! + + + + ); +}; + +export default About; diff --git a/Client/Pages/HomePage.jsx b/Client/Pages/HomePage.jsx new file mode 100644 index 0000000..fec9f5a --- /dev/null +++ b/Client/Pages/HomePage.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import Login from '../Auth/Login'; // Adjust the path as needed + +const HomePage = () => { + return ( + + ); +}; + +export default HomePage; diff --git a/Client/Pages/MyAccount.jsx b/Client/Pages/MyAccount.jsx new file mode 100644 index 0000000..453580c --- /dev/null +++ b/Client/Pages/MyAccount.jsx @@ -0,0 +1,160 @@ +import React, { useState } from 'react'; +import { useAuth } from '../Auth/AuthContext'; +import axios from 'axios'; +import '../styles/MyAccount.css'; + +const MyAccount = () => { + const { user, session, login, loading } = useAuth(); + const [first_name, setFirstName] = useState(user?.user_metadata?.first_name || ''); + const [last_name, setLastName] = useState(user?.user_metadata?.last_name || ''); + const [email, setEmail] = useState(user?.user_metadata?.email || ''); + const [password, setPassword] = useState(''); + const [avatar, setAvatar] = useState(null); + + const [editField, setEditField] = useState(null); + + const handleFieldUpdate = async (field, value) => { + try { + await axios.post(`/api/update-profile`, { + userId: user.id, + field, + value, + }); + + // Fetch updated user data after profile update + const updatedUser = await fetchUpdatedUserInfo(); + if (updatedUser) { + // Update AuthContext with the new user data + login(updatedUser, session); + } + } catch (error) { + console.error(`Error updating ${field}:`, error); + } + setEditField(null); + }; + + const handleEditClick = (field) => { + setEditField(field); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + if (name === 'first_name') setFirstName(value); + if (name === 'last_name') setLastName(value); + if (name === 'email') setEmail(value); + if (name === 'password') setPassword(value); + }; + + const handleAvatarChange = (e) => { + setAvatar(e.target.files[0]); + }; + + const fetchUpdatedUserInfo = async () => { + try { + const response = await axios.get(`/api/user-profile/${user.id}`); + return response.data; + } catch (error) { + console.error('Error fetching updated user info:', error); + } + }; + + const handleAvatarUpload = async () => { + const formData = new FormData(); + formData.append('avatar', avatar); + formData.append('userId', user.id); // Include user ID in the form data + + try { + await axios.post('/api/upload-avatar', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const updatedUser = await fetchUpdatedUserInfo(); + if (updatedUser) { + login(updatedUser, session); + } + } catch (error) { + console.error('Error uploading avatar:', error); + } + }; + + if (loading) { + return null; // or a loading spinner + } + + return ( +
+

My Account

+
+
+ First Name: {first_name} + +
+ {editField === 'first_name' && ( + handleFieldUpdate('first_name', first_name)} + /> + )} +
+
+
+ Last Name: {last_name} + +
+ {editField === 'last_name' && ( + handleFieldUpdate('last_name', last_name)} + /> + )} +
+
+
+ Email: {email} + +
+ {editField === 'email' && ( + handleFieldUpdate('email', email)} + /> + )} +
+
+
+ Password: ****** + +
+ {editField === 'password' && ( + handleFieldUpdate('password', password)} + /> + )} +
+
+
+ Avatar: + + +
+
+
+ ); +}; + +export default MyAccount; diff --git a/Client/Routes.js b/Client/Routes.js new file mode 100644 index 0000000..ba464a1 --- /dev/null +++ b/Client/Routes.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import Login from './Auth/Login'; +import Signup from './Auth/Signup'; +import LoginStatus from './Auth/LoginStatus'; +import ImageSearch from './components/StyleImageSearchPage'; +import ResponsiveAppBar from './components/ResponsiveAppBar'; +import HomePage from './Pages/HomePage'; +import About from './Pages/About'; +import MyAccount from './Pages/MyAccount'; + +const Routes = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default Routes; diff --git a/Client/components/AI-Gen-Form.jsx b/Client/components/AI-Gen-Form.jsx new file mode 100644 index 0000000..ab0dfb5 --- /dev/null +++ b/Client/components/AI-Gen-Form.jsx @@ -0,0 +1,84 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import SpeechRecognition from './SpeechRecognition'; + +function AIGenForm({ onImageGenerated, setLoading, setCurrentImageUrl }) { + const [item, setItem] = useState(''); + const [color, setColor] = useState(''); + const [style, setStyle] = useState(''); + const [features, setFeatures] = useState(''); + const [additional, setAdditional] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + setCurrentImageUrl(null); + setLoading(true); + + try { + const response = await axios.post('/api/generate-image', { item, color, style, features, additional }); + onImageGenerated(response.data.image_url, { item, color, style, features, additional }); + } catch (error) { + console.error('Error generating image:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ + + + + + + + + + + +
+ ); +} + +export default AIGenForm; diff --git a/Client/components/AI-Gen-Result.jsx b/Client/components/AI-Gen-Result.jsx new file mode 100644 index 0000000..7054f4e --- /dev/null +++ b/Client/components/AI-Gen-Result.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +function AIGenResult({ imageUrl, onTryAgainClick, onFindMatchingItemsClick }) { + return ( +
+ Generated +
+ + +
+ ); +} + +export default AIGenResult; diff --git a/Client/components/AuthNavigationButton.jsx b/Client/components/AuthNavigationButton.jsx new file mode 100644 index 0000000..fe12005 --- /dev/null +++ b/Client/components/AuthNavigationButton.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { Button } from '@mui/material'; + +const AuthNavigationButton = ({ navigateTo, label }) => { + const history = useHistory(); + + const handleClick = () => { + history.push(navigateTo); + }; + + return ( + + ); +}; + +export default AuthNavigationButton; diff --git a/Client/components/Matched-Image.jsx b/Client/components/Matched-Image.jsx new file mode 100644 index 0000000..3329c7c --- /dev/null +++ b/Client/components/Matched-Image.jsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import axios from 'axios'; +import ImageGenerationForm from './AI-Gen-Form'; +import ImageResults from './ImageResults'; +import CircularProgress from '@mui/material/CircularProgress'; +import LinearProgress from '@mui/material/LinearProgress'; + +function ImageSearch() { + const [currentImageUrl, setCurrentImageUrl] = useState(null); + const [currentPrompt, setCurrentPrompt] = useState(''); + const [bingData, setBingData] = useState(''); + const history = useHistory(); + const [loading, setLoading] = useState(false); + const [loadingBing, setLoadingBing] = useState(false); + + const handleImageGenerated = (imageUrl, prompt) => { + setCurrentImageUrl(imageUrl); + setCurrentPrompt(prompt); + setLoading(false); + }; + + const handleNoClick = async () => { + setCurrentImageUrl(null); + setLoading(true); + try { + const response = await axios.post('/api/generate-image', { + item: currentPrompt.item, + color: currentPrompt.color, + style: currentPrompt.style, + features: currentPrompt.features, + additional: currentPrompt.additional + }); + handleImageGenerated(response.data.image_url, currentPrompt); + } catch (error) { + console.error('Error generating new image:', error); + } finally { + setLoading(false); + } + }; + + const handleYesClick = async () => { + setBingData(''); + setLoadingBing(true); + try { + const response = await axios.post('/api/match-service', { + imageUrl: currentImageUrl, + }); + setBingData(response.data); + } catch (error) { + console.error('Error searching Bing:', error); + } finally { + setLoadingBing(false); + } + }; + + return ( +
+
+

Discover Your Style

+ +
+ {loading && } + {currentImageUrl && ( +
+ Generated +
+ + +
+ )} +
+ {bingData ? ( + + ) : ( +
+
+ )} + {loadingBing && } +
+ ); +} + +export default ImageSearch; diff --git a/Client/components/Matched-Result.jsx b/Client/components/Matched-Result.jsx new file mode 100644 index 0000000..139c4fb --- /dev/null +++ b/Client/components/Matched-Result.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +function MatchedResult({ image }) { + console.log(`Rendering image: ${image.name} with URL: ${image.contentUrl}`); + + return ( +
+ {image.name} +
+ ); +} + +export default MatchedResult; diff --git a/Client/components/Matched-Results.jsx b/Client/components/Matched-Results.jsx new file mode 100644 index 0000000..0039fd1 --- /dev/null +++ b/Client/components/Matched-Results.jsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; +import MatchedResult from './Matched-Result'; + +function MatchedResults({ bingData }) { + useEffect(() => { + console.log('MatchedResults component loaded'); + console.log('Number of results:', bingData.length); + console.log('bingData:', bingData); + }, [bingData]); + + return ( +
+

Similar Images

+
+ {bingData.map((image, index) => ( + + ))} +
+
+ ); +} + +export default MatchedResults; diff --git a/Client/components/ResponsiveAppBar.jsx b/Client/components/ResponsiveAppBar.jsx new file mode 100644 index 0000000..7367f81 --- /dev/null +++ b/Client/components/ResponsiveAppBar.jsx @@ -0,0 +1,207 @@ +import * as React from 'react'; +import { useHistory } from 'react-router-dom'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import Menu from '@mui/material/Menu'; +import MenuIcon from '@mui/icons-material/Menu'; +import Container from '@mui/material/Container'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; +import MenuItem from '@mui/material/MenuItem'; +import AdbIcon from '@mui/icons-material/Adb'; +import { useAuth } from '../Auth/AuthContext'; + +const pagesLoggedOut = ['Login', 'Sign Up', 'About']; +const pagesLoggedIn = ['Search', 'Feed', 'Favorites']; +const settings = ['My Account', 'Logout']; + +function ResponsiveAppBar() { + const [anchorElNav, setAnchorElNav] = React.useState(null); + const [anchorElUser, setAnchorElUser] = React.useState(null); + const history = useHistory(); + const { user, avatarUrl, logout } = useAuth(); + + const handleOpenNavMenu = (event) => { + setAnchorElNav(event.currentTarget); + }; + + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + + const handleCloseNavMenu = (page) => { + setAnchorElNav(null); + + switch (page) { + case 'Home': + history.push('/'); + break; + case 'Search': + history.push('/search'); + break; + case 'Feed': + history.push('/feed'); + break; + case 'Favorites': + history.push('/favorites'); + break; + case 'Login': + history.push('/login'); + break; + case 'Sign Up': + history.push('/signup'); + break; + case 'About': + history.push('/about'); + break; + default: + console.log('Unknown page:', page); + } + }; + + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + const handleMyAccount = () => { + handleCloseUserMenu(); + history.push('/myAccount'); + }; + + const handleLogout = () => { + logout(); + handleCloseUserMenu(); + history.push('/'); + }; + + const handleUserMenuClick = (setting) => { + switch (setting) { + case 'Logout': + handleLogout(); + break; + case 'My Account': + handleMyAccount(); + break; + default: + handleCloseUserMenu(); + } + }; + + const pages = user ? pagesLoggedIn : pagesLoggedOut; + + return ( + + + + + + + + handleCloseNavMenu(null)} + sx={{ + display: { xs: 'block', md: 'none' }, + }} + > + {pages.map((page) => ( + handleCloseNavMenu(page)}> + {page} + + ))} + + + + + LOGO + + + {pages.map((page) => ( + + ))} + + {user && ( + + + + {avatarUrl ? ( + + ) : ( + {user?.user_metadata?.first_name?.charAt(0).toUpperCase()} + )} + + + + {settings.map((setting) => ( + handleUserMenuClick(setting)}> + {setting} + + ))} + + + )} + + + + ); +} + +export default ResponsiveAppBar; diff --git a/Client/components/Session-mgmt.jsx b/Client/components/Session-mgmt.jsx new file mode 100644 index 0000000..4b5b13c --- /dev/null +++ b/Client/components/Session-mgmt.jsx @@ -0,0 +1,27 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; + +function SessionTimeoutChecker() { + const [showModal, setShowModal] = useState(false); + const [secondsLeft, setSecondsLeft] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + axios.get('/api/session-status') // to do: make endpoint in server + .then(response => { + const { countdownModal } = response.data; + if (countdownModal && countdownModal.show) { + setShowModal(true); + setSecondsLeft(countdownModal.secondsLeft); + } else { + setShowModal(false); + } + }) + .catch(error => console.error('Error fetching session status', error)); + }, 1000); // Check every second + + return () => clearInterval(interval); + }, []); + + return showModal ? : null; +}l \ No newline at end of file diff --git a/Client/components/SpeechRecognition.jsx b/Client/components/SpeechRecognition.jsx new file mode 100644 index 0000000..fe84641 --- /dev/null +++ b/Client/components/SpeechRecognition.jsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react'; + +function SpeechRecognition({ formId, setter }) { + const [buttonLabel, setButtonLabel] = useState('Voice input'); + const [transcript, setTranscript] = useState(''); + + const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); + recognition.lang = 'en-US'; + + recognition.onstart = () => { + setButtonLabel('Listening...'); +}; + + recognition.onresult = (event) => { + const transcriptVar = event.results[0][0].transcript; + setTranscript(transcriptVar); + setter(transcriptVar); + }; + + recognition.onend = () => { + setButtonLabel('Voice input'); + }; + + return ( + <> + + + ) +}; + +export default SpeechRecognition; \ No newline at end of file diff --git a/Client/components/StyleImageSearchPage.jsx b/Client/components/StyleImageSearchPage.jsx new file mode 100644 index 0000000..8aa5788 --- /dev/null +++ b/Client/components/StyleImageSearchPage.jsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import axios from 'axios'; +import AIGenForm from './AI-Gen-Form'; +import AIGenResult from './AI-Gen-Result'; +import MatchedResults from './Matched-Results'; +import CircularProgress from '@mui/material/CircularProgress'; +import LinearProgress from '@mui/material/LinearProgress'; + +function StyleImageSearchPage() { + const [currentImageUrl, setCurrentImageUrl] = useState(null); + const [currentPrompt, setCurrentPrompt] = useState(''); + const [bingData, setBingData] = useState(''); + const history = useHistory(); + const [loading, setLoading] = useState(false); + const [loadingBing, setLoadingBing] = useState(false); + + const handleImageGenerated = (imageUrl, prompt) => { + setCurrentImageUrl(imageUrl); + setCurrentPrompt(prompt); + setLoading(false); + }; + + const handleTryAgainClick = async () => { + setCurrentImageUrl(null); + setLoading(true); + try { + const response = await axios.post('/api/generate-image', { + item: currentPrompt.item, + color: currentPrompt.color, + style: currentPrompt.style, + features: currentPrompt.features, + }); + handleImageGenerated(response.data.image_url, currentPrompt); + } catch (error) { + console.error('Error generating new image:', error); + } finally { + setLoading(false); + } + }; + + const handleFindMatchingItemsClick = async () => { + setBingData(''); + setLoadingBing(true); + try { + const response = await axios.post('/api/match-service', { + imageUrl: currentImageUrl, + }); + setBingData(response.data); + } catch (error) { + console.error('Error searching Bing:', error); + } finally { + setLoadingBing(false); + } + }; + + return ( +
+
+

Discover Your Style

+ +
+ {loading && ( +
+ +

Finding Your Style...

+
+ )} + {currentImageUrl && ( + + )} +
+ {bingData ? ( + + ) : ( +
+ )} + {loadingBing && ( +
+ +

Finding Matching Items...

+
+ )} +
+ ); +} + +export default StyleImageSearchPage; diff --git a/Client/components/generic/Button.style.jsx b/Client/components/generic/Button.style.jsx new file mode 100644 index 0000000..4e71d48 --- /dev/null +++ b/Client/components/generic/Button.style.jsx @@ -0,0 +1,13 @@ +import styled from 'styled-components'; + +export const Button = styled.button` + width: 100px; + height: 30px; + padding: 0; + background-color: #2C2C2C; + color: #F5F5F5; + border-radius: 5px; + line-height: 0px; +`; + +// export default Button; \ No newline at end of file diff --git a/Client/index.css b/Client/index.css new file mode 100644 index 0000000..90730a9 --- /dev/null +++ b/Client/index.css @@ -0,0 +1,179 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #2C6C8F; + color: #999999; +} + +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #333; + padding: 10px 20px; + position: fixed; /* Fixed position so nav bar stays @ top of page */ + width: 100%; + z-index: 1000; +} + +.navbar-brand { + color: #fff; + font-size: 1.5em; +} + +.navbar-buttons .nav-button { + color: #fff; + text-decoration: none; + margin-left: 20px; + padding: 10px 20px; + background-color: #444; + border-radius: 5px; +} + +.navbar-buttons .nav-button:hover { + background-color: #555; +} + +.homepage { + text-align: center; + padding: 20px; +} + +.filter { + margin: 20px 0; +} + +.search-page { + text-align: center; + padding: 100px 20px 20px; /* Added top padding to avoid being covered by navbar */ +} + +.generatedImg { + max-width: 100%; + height: auto; +} + +.login-status { + position: fixed; + bottom: 0; + width: 100%; + background-color: #333; + padding: 5px; + text-align: center; + z-index: 1000; + font-size: 14px; + color: #f0f0f0; +} + +.search-page .loading-container { + text-align: center; + margin-top: 20px; +} + +/* New styles for the updated layout */ +.container { + display: grid; + grid-template-columns: 1fr 3fr; + gap: 20px; + padding: 20px; + max-width: 1200px; + margin: auto; + background-color: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-radius: 10px; +} + +.form-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.form-container form { + display: grid; + grid-template-columns: 1fr auto; + gap: 10px; + width: 100%; + max-width: 400px; +} + +.form-container label { + display: flex; + flex-direction: column; + text-align: left; +} + +.form-container input { + width: 100%; + padding: 8px; + margin-top: 5px; + box-sizing: border-box; +} + +.form-container button { + grid-column: span 2; + padding: 10px 20px; + margin-top: 20px; +} + +.generated-image { + text-align: center; +} + +.image-results { + max-height: 600px; + overflow-y: auto; + overflow-x: hidden; + padding: 10px; + border: 1px solid #ddd; + border-radius: 5px; + background-color: #f9f9f9; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; +} + +.image-results img { + width: 100%; + height: 100px; + object-fit: cover; + border-radius: 5px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); +} + +.speechButton { + margin-left: 10px; + padding: 8px; +} + +button { + margin: 5px; +} + +/* Custom scroll bar styles */ +.image-results::-webkit-scrollbar { + width: 8px; +} + +.image-results::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.image-results::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +.image-results::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Adding centering for the the CircularProgress component */ +.center-loading { + display: flex; + justify-content: center; + align-items: center; + margin-top: 1rem; +} diff --git a/Client/index.js b/Client/index.js new file mode 100644 index 0000000..ac58645 --- /dev/null +++ b/Client/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import Routes from './Routes'; // Assuming this is your main routing component +import './index.css'; +import { AuthProvider } from './Auth/AuthContext'; +import AuthProviderDebugger from './Auth/AuthProviderDebugger'; // Adjust the path as needed + +const App = () => { + return ( + + + + + ); +}; + +ReactDOM.render(, document.getElementById('root')); diff --git a/Client/public/index.html b/Client/public/index.html new file mode 100644 index 0000000..fe3efc9 --- /dev/null +++ b/Client/public/index.html @@ -0,0 +1,11 @@ + + + + + + Fashion Selector + + +
+ + diff --git a/Client/styles/AuthForm.css b/Client/styles/AuthForm.css new file mode 100644 index 0000000..d79b622 --- /dev/null +++ b/Client/styles/AuthForm.css @@ -0,0 +1,52 @@ +.auth-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.auth-form { + max-width: 400px; /* Increase the max-width if needed */ + width: 100%; + padding: 40px; /* Adjust padding as needed */ + background-color: #999999; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-radius: 5px; + color: #f0f0f0; +} + +.auth-form h2 { + margin-bottom: 20px; +} + +.auth-form form { + display: flex; + flex-direction: column; +} + +.auth-form label { + margin-bottom: 5px; +} + +.auth-form input { + margin-bottom: 20px; + padding: 10px; + border: 1px solid #666; + border-radius: 5px; + background-color: #333; + color: #f0f0f0; +} + +.auth-form button { + padding: 10px 20px; + background-color: #444; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.auth-form button:hover { + background-color: #555; +} diff --git a/Client/styles/MyAccount.css b/Client/styles/MyAccount.css new file mode 100644 index 0000000..f59e941 --- /dev/null +++ b/Client/styles/MyAccount.css @@ -0,0 +1,45 @@ +/* MyAccount.css */ +.account-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; +} + +.account-container h1 { + margin-bottom: 20px; +} + +.account-field { + margin-bottom: 20px; + text-align: center; +} + +.account-field div { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 10px; +} + +.account-field input { + margin-top: 10px; + padding: 8px; + width: 200px; +} + +.account-field button { + margin-top: 10px; + padding: 8px 16px; + cursor: pointer; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; +} + +.account-field button:hover { + background-color: #0056b3; +} diff --git a/Server/Controllers/authController.js b/Server/Controllers/authController.js new file mode 100644 index 0000000..b6bcb43 --- /dev/null +++ b/Server/Controllers/authController.js @@ -0,0 +1,70 @@ +const supabase = require('../supabase'); + +const authController = {}; + +authController.signup = async (req, res) => { + console.log('Signup request received'); + const { email, password, firstName, lastName } = req.body; + console.log('Signup details:', { email, password, firstName, lastName }); + + try { + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + first_name: firstName, + last_name: lastName + } + } + }); + + if (error) { + console.log('Signup error:', error.message); + return res.status(400).json({ message: error.message }); + } + + const user = data.user; + console.log(data); + if (!user) { + throw new Error('User object is undefined'); + } + + res.json({ user, session: data.session }); + } catch (err) { + console.error('Server error:', err.message); + res.status(500).send('Server error'); + } +}; + +/** + * + * @param {*} req + * @param {*} res + * @returns + */ +authController.login = async (req, res) => { + console.log('Login request received'); + const { email, password } = req.body; + console.log('Login details:', { email, password }); + + try { + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error) { + console.log('Login error:', error.message); + return res.status(400).json({ message: error.message }); + } + + const user = data.user; + + res.json({ user, session: data.session }); + } catch (err) { + console.error('Server error:', err.message); + res.status(500).send('Server error'); + } +}; + +module.exports = authController; diff --git a/Server/Controllers/bingSearchController.js b/Server/Controllers/bingSearchController.js new file mode 100644 index 0000000..aaffe1f --- /dev/null +++ b/Server/Controllers/bingSearchController.js @@ -0,0 +1,59 @@ +const dotenv = require('dotenv'); +const axios = require('axios'); +const FormData = require('form-data'); + +dotenv.config(); + +const endpoint = 'https://api.bing.microsoft.com/v7.0/images/visualsearch'; +const subscriptionKey = process.env.SUBSCRIPTION_KEY; + +const bingSearchController = {}; + +bingSearchController.matchService = async (req, res, next) => { + try { + const { imageUrl } = req.body; + if (!imageUrl) { + return res.status(400).json({ error: 'Dall-e Image URL is required' }); + } + + const knowledgeRequest = JSON.stringify({ + imageInfo: { url: imageUrl }, + }); + + const formData = new FormData(); + formData.append('knowledgeRequest', knowledgeRequest); + + const response = await axios.post(`${endpoint}?mkt=en-us`, formData, { + headers: { + 'Ocp-Apim-Subscription-Key': subscriptionKey, + ...formData.getHeaders(), + }, + }); + + const data = response.data; + + const simplifiedData = data.tags[0].actions + .filter((action) => action.actionType === 'VisualSearch') + .flatMap((action) => action.data.value) + .map((item) => ({ + contentUrl: item.contentUrl, + hostPageUrl: item.hostPageUrl, + name: item.name, + })) + .filter( + (item) => + item.contentUrl.toLowerCase().includes('shop') || + item.hostPageUrl.toLowerCase().includes('shop') + ); + + // Limit the number of results + const limitedResults = simplifiedData.slice(0, 15); // Change 15 to the desired number of results + + res.json(limitedResults); + } catch (error) { + console.error('Visual Search Error:', error); + res.status(500).json({ error: 'An error occurred on Visual Search.' }); + } +}; + +module.exports = bingSearchController; diff --git a/Server/Controllers/openaiImageController.js b/Server/Controllers/openaiImageController.js new file mode 100644 index 0000000..a7abb1f --- /dev/null +++ b/Server/Controllers/openaiImageController.js @@ -0,0 +1,59 @@ +const dotenv = require('dotenv'); +const axios = require('axios'); + +dotenv.config(); + +const endpoint_openai = 'https://api.openai.com/v1/images/generations'; +const openai_key = process.env.OPENAI_API_KEY; + +const openaiImageController = {}; + +openaiImageController.ImgGenService = async (req, res, next) => { + const { item, color, style, features, additional } = req.body; + + if (!item || !color || !style || !features) { + return res + .status(400) + .json({ error: "Item, color, style or features is missing." }); + } + + try { + let prompt; + if (!additional) { + prompt = `Create an image of a ${style} ${item} in ${color} , + featuring ${features}. The image should have a white background + and the item should be facing the front. + The item should be 100% within the image border.`; + } else { + prompt = `Create an image of a ${style} ${item} in ${color} , + featuring ${features}. ${additional}. The image should have a white background + and the item should be facing the front. + The item should be 100% within the image border.`; + }; + + const response = await axios.post( + endpoint_openai, + { + prompt: prompt, + n: 1, + }, + { + headers: { + Authorization: `Bearer ${openai_key}`, + 'Content-Type': 'application/json', + }, + } + ); + + const image_url = response.data.data[0].url; + + res.json({ image_url }); + } catch (error) { + console.error("Dall E Image Generator Error: ", error); + res + .status(500) + .json({ error: "An error occurred on Dall E Image Generator." }); + } +}; + +module.exports = openaiImageController; diff --git a/Server/Controllers/profileController.js b/Server/Controllers/profileController.js new file mode 100644 index 0000000..78abdc3 --- /dev/null +++ b/Server/Controllers/profileController.js @@ -0,0 +1,102 @@ +const { v4: uuidv4 } = require('uuid'); +const supabase = require('../supabase'); + +const profileController = {}; + +/** + * Middleware to decode binary avatar-img + * data from the request object, assigns random file name, stores + * in supabase bucket, stores URL reference to associated user + * in profile record + * @param {*} req http request as express object with multipart/form-data content-type header and raw binary payload + * @param {*} res http response with URL for success or error + */ +profileController.uploadAvatar = async (req, res) => { + const { userId } = req.body; + const file = req.file; + const fileName = `${uuidv4()}-${file.originalname}`; + + try { + const { error: uploadError, data: uploadData } = await supabase.storage + .from('User-Avatars') + .upload(fileName, file.buffer, { + cacheControl: '3600', + upsert: false, + }); + + if (uploadError) { + throw uploadError; + } + + const avatarUrl = `${process.env.SUPABASE_URL}/storage/v1/object/public/User-Avatars/${fileName}`; + + // Update the user's profile with the new avatar URL + const { error: updateError } = await supabase + .from('profiles') + .update({ avatar_url: avatarUrl }) + .eq('id', userId); + + if (updateError) { + throw updateError; + } + + res.status(200).json({ avatarUrl }); + } catch (error) { + console.error('Error uploading avatar:', error); + res.status(500).json({ error: 'Failed to upload avatar' }); + } +}; + +profileController.getUserProfile = async (req, res) => { + const { userId } = req.params; + + try { + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single(); + + if (error) { + throw error; + } + + res.json(data); + } catch (error) { + console.error('Error fetching user profile:', error); + res.status(500).json({ error: 'Failed to fetch user profile' }); + } +}; + +profileController.updateProfile = async (req, res) => { + const { userId, field, value } = req.body; + + if (!userId || !field || !value) { + return res.status(400).json({ error: 'Missing required fields' }); + } + + try { + console.log('Told to update userId: ', userId); + console.log('Told to update field: ', field); + console.log('Told to update value: ', value); + + const updateData = {}; + updateData[field] = value; + + const { data, error } = await supabase + .from('profiles') + .update(updateData) + .eq('id', userId); + + if (error) { + throw error; + } + + res.status(200).json(data); + } catch (error) { + console.error(`Error updating ${field}:`, error); + res.status(500).json({ error: `Failed to update ${field}` }); + } +}; + +module.exports = profileController; diff --git a/Server/Controllers/sessionController.js b/Server/Controllers/sessionController.js new file mode 100644 index 0000000..8f23875 --- /dev/null +++ b/Server/Controllers/sessionController.js @@ -0,0 +1,11 @@ +const sessionController = {}; + +sessionController.getSessionStatus = (req, res) => { + if (req.session && req.session.countdownModal) { + return res.json({ countdownModal: req.session.countdownModal }); + } else { + return res.json({ countdownModal: { show: false } }); + } +}; + +module.exports = sessionController; \ No newline at end of file diff --git a/Server/index.js b/Server/index.js new file mode 100644 index 0000000..484bf29 --- /dev/null +++ b/Server/index.js @@ -0,0 +1,138 @@ +/** + * Imports + * @express routing middleware framework for server-side Node.js + * @cors middleware to allow app to access the resources not hosted on our server -- however + * we are using webpack proxy for all requests made to /api to this server and all outbound client + * requests are proceeded by /api + * +*/ +const session = require('express-session'); //for session cookies +const express = require('express'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const multer = require('multer'); +const path = require('path') +/** + * @v4 importing v4 function from uuid library under nickname x + * uuidd. The v4 methods generaters a random uuid which we then + * use to generate unique file names so that we can avoid + * file name collisions during DB insertions. +*/ +const { v4: uuidv4 } = require('uuid'); + +const app = express(); +const PORT = process.env.PORT || 3003; +const authController = require('./Controllers/authController'); +const openaiImageController = require('./Controllers/openaiImageController'); +const bingSearchController = require('./Controllers/bingSearchController'); +const profileController = require('./Controllers/profileController'); +const sessionController = require('./Controllers/sessionController'); + +dotenv.config(); + +console.log('Supabase URL:', process.env.SUPABASE_URL); // Add logging +console.log('Supabase Key:', process.env.SUPABASE_KEY); // Add logging +console.log('Session Key:', process.env.SESSION_KEY); // Add logging + +app.use(cors()); +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); + +//sessions middleware +app.use(session({ + secret: 'Session Key', // in .env + resave: false, // causes session to be saved back to the session store, even if it wasn't modified during the request + saveUninitialized: false, // if there is no session, does it save it to the session store? + cookie: { + secure: false, // does it have to use https? in dev it doesnt need to + httpOnly: true, // xss prevention starts with you + maxAge: 24 * 60 * 60 * 1000 // 1 day lifespan + } +})); + +// Track activity and reset session timer +app.use((req, res, next) => { + if (req.session) { + req.session.lastActivity = Date.now(); + req.session.save(err => { + if (err) { + console.error(`Error saving session: ${err}`); + } else { + console.log(`Session Last Activity: ${req.session.lastActivity}`); + } + }); + } + next(); +}); + +// Prompt user after 50 seconds and show countdown modal +app.use((req, res, next) => { + const now = Date.now(); + if (req.session && req.session.lastActivity) { + const timeSinceLastActivity = now - req.session.lastActivity; + console.log(`Time since last activity: ${timeSinceLastActivity} ms`); + + if (timeSinceLastActivity > 50 * 1000 && timeSinceLastActivity < 60 * 1000) { + const secondsLeft = Math.floor((60 * 1000 - timeSinceLastActivity) / 1000); + req.session.countdownModal = { + show: true, + secondsLeft + }; + console.log(`Session Countdown Modal: ${JSON.stringify(req.session.countdownModal)}`); + req.session.save(err => { + if (err) { + console.error(`Error saving session: ${err}`); + } + }); + } + } + next(); +}); + +// Destroy session after 60 seconds of inactivity +app.use((req, res, next) => { + const now = Date.now(); + if (req.session && req.session.lastActivity) { + const timeSinceLastActivity = now - req.session.lastActivity; + console.log(`Time since last activity: ${timeSinceLastActivity} ms`); + + if (timeSinceLastActivity > 60 * 1000) { // Adjusted for quicker testing + console.log('Session has timed out. Destroying session.'); + req.session.destroy(err => { + if (err) { + console.error(`Error destroying session: ${err}`); + } else { + return res.status(401).send('Session has timed out. Please log in again.'); + } + }); + } + } + next(); +}); + + + +// Routes +// SESSION STATUS +app.get('/api/session-status', sessionController.getSessionStatus); + +app.post('/api/signup', authController.signup); +app.post('/api/login', authController.login); + +// IMAGE CREATION AND DISCOVERY +app.post('/api/generate-image', openaiImageController.ImgGenService); +app.post('/api/match-service', bingSearchController.matchService); + +// ACCOUNT MANAGEMENT +// app.post('/api/upload-avatar', upload.single('avatar'), profileController.uploadAvatar); +app.get('/api/user-profile/:userId', profileController.getUserProfile); +app.post('/api/update-profile', profileController.updateProfile); + +// Catch-all route to serve index.html for any other routes +app.get('*', (req, res) => { + console.log(path.join(__dirname, '../dist/index.html')) + res.sendFile(path.join(__dirname, '../dist/index.html')); +}); +app.listen(PORT, () => { + console.log(`Example app listening on port ${PORT}`); +}); diff --git a/Server/supabase.js b/Server/supabase.js new file mode 100644 index 0000000..35adc74 --- /dev/null +++ b/Server/supabase.js @@ -0,0 +1,9 @@ +require('dotenv').config(); +const { createClient } = require('@supabase/supabase-js'); + +const supabaseUrl = process.env.SUPABASE_URL; +const supabaseKey = process.env.SUPABASE_KEY; + +const supabase = createClient(supabaseUrl, supabaseKey); + +module.exports = supabase; diff --git a/client/public/SignIn.jsx b/client/public/SignIn.jsx deleted file mode 100644 index e757890..0000000 --- a/client/public/SignIn.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react'; -import Avatar from '@mui/material/Avatar'; -import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; -import TextField from '@mui/material/TextField'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Checkbox from '@mui/material/Checkbox'; -import Link from '@mui/material/Link'; -import Grid from '@mui/material/Grid'; -import Box from '@mui/material/Box'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import Typography from '@mui/material/Typography'; -import Container from '@mui/material/Container'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; - -function Copyright(props) { - return ( - - {'Copyright © '} - - Your Website - {' '} - {new Date().getFullYear()} - {'.'} - - ); -} - -// TODO remove, this demo shouldn't need to reset the theme. - -const defaultTheme = createTheme(); - -export default function SignIn() { - const handleSubmit = (event) => { - event.preventDefault(); - const data = new FormData(event.currentTarget); - console.log({ - email: data.get('email'), - password: data.get('password'), - }); - }; - - return ( - - - - - - - - - Sign in - - - - - } - label="Remember me" - /> - - - - - Forgot password? - - - - - {"Don't have an account? Sign Up"} - - - - - - - - - ); -} \ No newline at end of file diff --git a/client/public/index.html b/client/public/index.html deleted file mode 100644 index 568bf1f..0000000 --- a/client/public/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Fashion Advisor - - -
- - - diff --git a/client/src/Assets/Images/carousel-by-apollon.jpg b/client/src/Assets/Images/carousel-by-apollon.jpg deleted file mode 100644 index a5fe896..0000000 Binary files a/client/src/Assets/Images/carousel-by-apollon.jpg and /dev/null differ diff --git a/client/src/Assets/Images/carousel-by-fluxweed.jpg b/client/src/Assets/Images/carousel-by-fluxweed.jpg deleted file mode 100644 index 9caac59..0000000 Binary files a/client/src/Assets/Images/carousel-by-fluxweed.jpg and /dev/null differ diff --git a/client/src/Assets/Images/carousel-by-karsten116.jpg b/client/src/Assets/Images/carousel-by-karsten116.jpg deleted file mode 100644 index 812cf95..0000000 Binary files a/client/src/Assets/Images/carousel-by-karsten116.jpg and /dev/null differ diff --git a/client/src/Assets/Images/carousel-by-khaledkagii.jpg b/client/src/Assets/Images/carousel-by-khaledkagii.jpg deleted file mode 100644 index adefdb2..0000000 Binary files a/client/src/Assets/Images/carousel-by-khaledkagii.jpg and /dev/null differ diff --git a/client/src/Assets/Images/carousel-by-minhphamdesign.jpg b/client/src/Assets/Images/carousel-by-minhphamdesign.jpg deleted file mode 100644 index f0dd7a4..0000000 Binary files a/client/src/Assets/Images/carousel-by-minhphamdesign.jpg and /dev/null differ diff --git a/client/src/Assets/Images/secretCloset/IMG_20240503_114840.jpg b/client/src/Assets/Images/secretCloset/IMG_20240503_114840.jpg deleted file mode 100644 index d5c0e55..0000000 Binary files a/client/src/Assets/Images/secretCloset/IMG_20240503_114840.jpg and /dev/null differ diff --git a/client/src/Assets/Images/secretCloset/IMG_4166.jpg b/client/src/Assets/Images/secretCloset/IMG_4166.jpg deleted file mode 100644 index 9575214..0000000 Binary files a/client/src/Assets/Images/secretCloset/IMG_4166.jpg and /dev/null differ diff --git a/client/src/Assets/Images/secretCloset/Image from iOS (1).jpg b/client/src/Assets/Images/secretCloset/Image from iOS (1).jpg deleted file mode 100644 index 9319de9..0000000 Binary files a/client/src/Assets/Images/secretCloset/Image from iOS (1).jpg and /dev/null differ diff --git a/client/src/Assets/Images/secretCloset/image (1).png b/client/src/Assets/Images/secretCloset/image (1).png deleted file mode 100644 index c77c937..0000000 Binary files a/client/src/Assets/Images/secretCloset/image (1).png and /dev/null differ diff --git a/client/src/assets/images/background1.jpg b/client/src/assets/images/background1.jpg deleted file mode 100644 index 93f4ce4..0000000 Binary files a/client/src/assets/images/background1.jpg and /dev/null differ diff --git a/client/src/assets/images/background2.jpg b/client/src/assets/images/background2.jpg deleted file mode 100644 index 72d36ab..0000000 Binary files a/client/src/assets/images/background2.jpg and /dev/null differ diff --git a/client/src/components/About.jsx b/client/src/components/About.jsx deleted file mode 100644 index ca8da8f..0000000 --- a/client/src/components/About.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { Container, Typography, Paper, Box } from '@mui/material'; - -const About = () => { - return ( - - - - - About Fashion Selector - - - Fashion Selector is your AI-powered fashion assistant, designed to help you discover and refine your personal style. - - - Our mission is to make fashion accessible and enjoyable for everyone. Whether you're looking for outfit inspiration or trying to build a wardrobe that truly reflects your personality, Fashion Selector is here to guide you. - - - Explore, experiment, and express yourself through fashion with us! - - - - - ); -}; - -export default About; \ No newline at end of file diff --git a/client/src/components/App.jsx b/client/src/components/App.jsx deleted file mode 100644 index 4f1a708..0000000 --- a/client/src/components/App.jsx +++ /dev/null @@ -1,118 +0,0 @@ -// App.jsx -import React, {useEffect} from 'react'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -import { Box, CssBaseline } from '@mui/material'; -import { ThemeProvider, styled } from '@mui/material/styles'; -import { gapi } from 'gapi-script'; -import Nav from './Nav'; -import Home from './Home'; -import Search from './Search'; -import ShowImages from './ShowImages'; -import Login from './Login'; -import SignUp from './SignUp'; -import About from './About'; -import Background from './Background'; -import SecretCloset from './SecretCloset'; -import MyCloset from './MyCloset' - -import customTheme from '../themes/customTheme'; -import backgroundImage from '../assets/images/background1.jpg'; -import { GoogleOAuthProvider } from '@react-oauth/google'; -import { GoogleLogin } from '@react-oauth/google'; - -const google_key = process.env.CLIENT_ID; - -const scope = ""; -const BackgroundBox = styled(Box)(({ theme }) => ({ - position: 'relative', - minHeight: '100vh', - width: '100%', - '&::before': { - content: '""', - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundImage: `url(${backgroundImage})`, - backgroundSize: 'cover', - backgroundPosition: 'center', - opacity: 0.5, - zIndex: -1, - }, -})); - - -const ContentContainer = styled(Box)(({ theme }) => ({ - position: 'relative', - zIndex: 1, - minHeight: '100vh', - padding: theme.spacing(2), // Add some padding -})); - -console.log('App.jsx is running'); - - - - -function App() { - - console.log(google_key) - useEffect(() => { - - function start(){ - gapi.auth2.init({ - client_id: '135318755257-cbm6k01p765cp64udecsj4vt7mghnc7s.apps.googleusercontent.com', - scope: '/login' - }) - - // gapi.client.init({ - // client_id: clientID, - // scope: scope - // }) - }; - - gapi.load('client:auth2', start) - -}) -const onSuccess = (res)=>{ - console.log('successfully logged in') - navigate('/home'); - //res.redirect('/home') -} - -const onFailure = (res)=>{ - console.log('fail', res) -} - - return ( - - - - - - - -