├── .DS_Store ├── .gitattributes ├── public ├── .DS_Store ├── meowth.png └── vite.svg ├── src ├── index.css ├── styles │ └── NavBar.css ├── pages │ ├── HomePage.jsx │ ├── TopPage.jsx │ ├── FavoritesPage.jsx │ └── VotePage.jsx ├── components │ ├── PrivateRoute.jsx │ ├── NavBar.jsx │ └── Login.jsx ├── main.jsx ├── App.css ├── App.jsx ├── services │ └── AuthProvider.jsx └── assets │ └── react.svg ├── postcss.config.js ├── vite.config.js ├── tailwind.config.js ├── package.json ├── index.html ├── eslint.config.js ├── README.md └── .gitignore /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggt3/CutieVotes_Capstone_front-end/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggt3/CutieVotes_Capstone_front-end/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/meowth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggt3/CutieVotes_Capstone_front-end/HEAD/public/meowth.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /src/styles/NavBar.css: -------------------------------------------------------------------------------- 1 | .active-link { 2 | color: #007bff; 3 | font-weight: bold; 4 | border-bottom: 2px solid #007bff; 5 | 6 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,jsx}"], 4 | 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | 2 | //if logged in, should show the top 20 upvoted pictures 3 | //if logged out, should show info and login page 4 | export default function HomePage() { 5 | return( 6 |
7 |

Homepage!

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from "react"; 4 | import { Navigate, Outlet } from "react-router-dom"; 5 | import { useAuth } from "../services/AuthProvider"; 6 | 7 | const PrivateRoute = () => { 8 | const {token} = useAuth(); 9 | if (!token) return ; 10 | return ; 11 | }; 12 | 13 | export default PrivateRoute; 14 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App.jsx"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | 7 | createRoot(document.getElementById("root")).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "capstone-front-end", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.7", 14 | "react": "^18.3.1", 15 | "react-bootstrap": "^2.10.5", 16 | "react-dom": "^18.3.1", 17 | "react-icons": "^5.3.0", 18 | "react-router-dom": "^6.27.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.13.0", 22 | "@types/react": "^18.3.11", 23 | "@types/react-dom": "^18.3.1", 24 | "@vitejs/plugin-react": "^4.3.3", 25 | "autoprefixer": "^10.4.20", 26 | "eslint": "^9.13.0", 27 | "eslint-plugin-react": "^7.37.1", 28 | "eslint-plugin-react-hooks": "^5.0.0", 29 | "eslint-plugin-react-refresh": "^0.4.13", 30 | "globals": "^15.11.0", 31 | "postcss": "^8.4.47", 32 | "tailwindcss": "^3.4.14", 33 | "vite": "^5.4.9" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from "react-router-dom"; 2 | import NavBar from "./components/NavBar"; 3 | import HomePage from "./pages/HomePage"; 4 | import VotePage from "./pages/VotePage"; 5 | import Login from "./components/Login"; 6 | import TopPage from "./pages/TopPage"; 7 | import AuthProvider from "./services/AuthProvider"; 8 | import PrivateRoute from "./components/PrivateRoute"; 9 | import FavoritesPage from "./pages/FavoritesPage"; 10 | 11 | 12 | function App() { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | } /> 19 | } /> 20 | }> 21 | } /> 22 | } /> 23 | } /> 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | CutieVotes 18 | 19 | 20 |
21 | 22 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capstone Project - CutieVotes (front-end) 2 | 3 | A website to vote on cute pictures and see your favorites. You must log in to play. 4 | 5 | ### Run Server 6 | 7 | `npm run dev` command after building the project 8 | 9 | ### Built With 10 | * React.js 11 | * React-Bootstrap 12 | * Axios 13 | * Vite 14 | 15 | 16 | ### Objectives Covered 17 | - Use React to create the application's front-end 18 | - Use CSS to style the application 19 | - Create at least four different views or pages for the application 20 | - Create some navigation that is included across the application's pages, utilizing React Router for page rendering. 21 | - Use React Hooks or Redux for your application state management. 22 | - Interface directly with the [server and API you created](https://github.com/ggt3/CutieVotes_Capstone_back-end) 23 | 24 | ### Resources 25 | [useContext YT Tutorial](https://www.youtube.com/watch?v=hUhWtYXgg0I) 26 | 27 | ### What I learned 28 | I am the most proud of learning user authentication with useContext and protecting routes. Next steps are to increase password security. 29 | 30 | 31 | ### Feature to-do's 32 | - [X] user auth /protected routes 33 | - [ ] secure password hashing 34 | - [ ] create-and-update-user functionality/page 35 | - [ ] removing a favorite from your likes 36 | -------------------------------------------------------------------------------- /src/pages/TopPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import Container from "react-bootstrap/Container"; 4 | import Row from "react-bootstrap/Row"; 5 | import Col from "react-bootstrap/Col"; 6 | import Image from "react-bootstrap/Image"; 7 | import Label from "react-bootstrap/FormLabel"; 8 | 9 | //show the current top 20 photos 10 | export default function TopPage() { 11 | const [pictures, setPictures] = useState([]); 12 | 13 | useEffect(() => { 14 | const fetchTopPics = async () => { 15 | const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/pictures/top`); 16 | const data = await res.data; 17 | console.log(data); 18 | setPictures(data); 19 | }; 20 | fetchTopPics(); 21 | }, []); 22 | return ( 23 |
24 | 25 | 26 | {pictures.map((picture) => ( 27 | 28 | 34 | 35 | 36 | ))} 37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/FavoritesPage.jsx: -------------------------------------------------------------------------------- 1 | //load the user's liked images 2 | //show total votes under each image 3 | import {useAuth} from '../services/AuthProvider' 4 | import { useEffect, useState } from "react"; 5 | import axios from "axios"; 6 | import Container from "react-bootstrap/Container"; 7 | import Row from "react-bootstrap/Row"; 8 | import Col from "react-bootstrap/Col"; 9 | import Image from "react-bootstrap/Image"; 10 | import Label from "react-bootstrap/FormLabel" 11 | 12 | export default function FavoritesPage() { 13 | const [pictures, setPictures] = useState([]); 14 | const {user} = useAuth() 15 | 16 | useEffect(() => { 17 | const fetchFavePics = async () => { 18 | const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/users/${user}/liked`); 19 | const data = await res.data; 20 | console.log("users favorites",data); 21 | setPictures(data); 22 | }; 23 | fetchFavePics(); 24 | }, []); 25 | return( 26 | 27 | 28 | {pictures.map((picture) => ( 29 | 30 | 36 | 37 | 38 | ))} 39 | 40 | 41 | ) 42 | } -------------------------------------------------------------------------------- /src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import "../styles/NavBar.css"; 3 | import { useAuth } from "../services/AuthProvider"; 4 | import Navbar from "react-bootstrap/Navbar" 5 | export default function NavBar() { 6 | const { user, logout } = useAuth(); 7 | return ( 8 | 12 | (isActive ? "active-link" : "")} 15 | > 16 | Home 17 | 18 | (isActive ? "active-link" : "")} 21 | > 22 | Top 23 | 24 | (isActive ? "active-link" : "")} 27 | > 28 | Vote 29 | 30 | 31 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Form from "react-bootstrap/Form"; 3 | import { useAuth } from "../services/AuthProvider"; 4 | 5 | export default function Login() { 6 | const auth = useAuth(); 7 | const [username, setUsername] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | 10 | 11 | 12 | const handleSubmit = async (e) => { 13 | e.preventDefault(); 14 | //handle login auth 15 | console.log(username, password); 16 | if (username !== "" && password !== "") { 17 | const tokenUser = await auth.loginAction({ username, password }); 18 | return; 19 | } 20 | alert("pleae provide a valid username and password"); 21 | 22 | console.log("login form submit"); 23 | }; 24 | return ( 25 |
26 |
27 | 30 | setUsername(e.target.value)} 35 | /> 36 |
37 |
38 | 41 | setPassword(e.target.value)} 46 | /> 47 |
48 |
49 | 52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/services/AuthProvider.jsx: -------------------------------------------------------------------------------- 1 | import { useContext, createContext, useState } from "react"; 2 | import { useNavigate, useLocation } from "react-router-dom"; 3 | import axios from "axios"; 4 | 5 | const AuthContext = createContext(); 6 | 7 | const AuthProvider = ({ children }) => { 8 | const [user, setUser] = useState(sessionStorage.getItem("user") || null); 9 | const [token, setToken] = useState(sessionStorage.getItem("token") || null); 10 | const navigate = useNavigate(); 11 | const location = useLocation(); 12 | 13 | const from = location.state?.from?.pathname || "/"; 14 | 15 | const loginAction = async ({username,password}) => { 16 | try { 17 | console.log("loginAction() - data", username,password); 18 | const response = await axios.post(`${import.meta.env.VITE_API_BASE_URL}/login`, { 19 | username, 20 | password, 21 | }); 22 | const res = await response.data; 23 | console.log(res); 24 | if (res) { //login passes 25 | setUser(res.username); 26 | setToken(res.token); 27 | sessionStorage.setItem("user", res.username) 28 | sessionStorage.setItem("token", res.token); 29 | navigate(from, { replace: true }); 30 | return; 31 | } 32 | throw new Error(res.message); 33 | } catch (err) { 34 | console.error(err); 35 | } 36 | }; 37 | 38 | const logout = () => { 39 | setUser(null); 40 | setToken(null); 41 | console.log("logging out"); 42 | sessionStorage.removeItem("token"); 43 | sessionStorage.removeItem("user"); 44 | navigate("/login"); 45 | }; 46 | 47 | // const getToken = () => { 48 | // const tokenString = sessionStorage.getItem("token"); 49 | // const userToken = JSON.parse(tokenString); 50 | // return userToken?.token; 51 | // }; 52 | 53 | return ( 54 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | export default AuthProvider; 63 | 64 | export const useAuth = () => { 65 | return useContext(AuthContext); 66 | }; 67 | -------------------------------------------------------------------------------- /src/pages/VotePage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import Container from "react-bootstrap/Container"; 4 | import Row from "react-bootstrap/Row"; 5 | import Col from "react-bootstrap/Col"; 6 | import Image from "react-bootstrap/Image"; 7 | import Button from "react-bootstrap/Button"; 8 | import { AiFillLike } from "react-icons/ai"; 9 | import { useAuth } from "../services/AuthProvider"; 10 | 11 | export default function VotePage() { 12 | const {user} = useAuth() 13 | const [pictures, setPictures] = useState([]); 14 | const [voted, setVoted] = useState(false); 15 | 16 | useEffect(() => { 17 | const fetchVote = async () => { 18 | const res = await axios.get( 19 | `${import.meta.env.VITE_API_BASE_URL}/pictures/compare` 20 | ); 21 | const data = await res.data; 22 | setPictures(data); 23 | setVoted(false); 24 | }; 25 | fetchVote(); 26 | }, [voted]); 27 | 28 | const handleVote = async (id) => { 29 | console.log("hi", id); 30 | const res = await axios.post( 31 | `${import.meta.env.VITE_API_BASE_URL}/pictures/${id}/upvote?user=${user}` 32 | ); 33 | setVoted(true); 34 | }; 35 | return ( 36 |
37 | 38 |

Which is cuter?

39 | 40 | {pictures.map((picture) => ( 41 | 42 | handleVote(picture._id)} 47 | style={{ cursor: "pointer", maxHeight: "500px", width: "100%" }} 48 | /> 49 | 56 | 57 | ))} 58 | 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | .env* 11 | # Bower dependency directory (https://bower.io/) 12 | bower_components 13 | 14 | # node-waf configuration 15 | .lock-wscript 16 | 17 | # Compiled binary addons (https://nodejs.org/api/addons.html) 18 | build/Release 19 | 20 | # Dependency directories 21 | node_modules/ 22 | jspm_packages/ 23 | 24 | # Snowpack dependency directory (https://snowpack.dev/) 25 | web_modules/ 26 | 27 | # TypeScript cache 28 | *.tsbuildinfo 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional eslint cache 34 | .eslintcache 35 | 36 | # Optional stylelint cache 37 | .stylelintcache 38 | 39 | # Microbundle cache 40 | .rpt2_cache/ 41 | .rts2_cache_cjs/ 42 | .rts2_cache_es/ 43 | .rts2_cache_umd/ 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variable files 55 | .env 56 | .env.development.local 57 | .env.test.local 58 | .env.production.local 59 | .env.local 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | .parcel-cache 64 | 65 | # Next.js build output 66 | .next 67 | out 68 | 69 | # Nuxt.js build / generate output 70 | .nuxt 71 | dist 72 | 73 | # Gatsby files 74 | .cache/ 75 | # Comment in the public line in if your project uses Gatsby and not Next.js 76 | # https://nextjs.org/blog/next-9-1#public-directory-support 77 | # public 78 | 79 | # vuepress build output 80 | .vuepress/dist 81 | 82 | # vuepress v2.x temp and cache directory 83 | .temp 84 | .cache 85 | 86 | # Docusaurus cache and generated files 87 | .docusaurus 88 | 89 | # Serverless directories 90 | .serverless/ 91 | 92 | # FuseBox cache 93 | .fusebox/ 94 | 95 | # DynamoDB Local files 96 | .dynamodb/ 97 | 98 | # TernJS port file 99 | .tern-port 100 | 101 | # Stores VSCode versions used for testing VSCode extensions 102 | .vscode-test 103 | 104 | # yarn v2 105 | .yarn/cache 106 | .yarn/unplugged 107 | .yarn/build-state.yml 108 | .yarn/install-state.gz 109 | .pnp.* 110 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------