├── .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 |
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 |
--------------------------------------------------------------------------------