Initial Commit

This commit is contained in:
Jonathan Nielson 2025-01-01 15:58:44 -06:00
commit c3b5b654cc
29 changed files with 6745 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

11
README.md Normal file
View File

@ -0,0 +1,11 @@
## Banner Day SPA
Banner Day is a platform for tracking and managing banner ads across affiliate websites. This is the React-based SPA frontend portion of the application.
### Starting the Project
To begin a development web server, run
```bash
npm run start
```

38
eslint.config.js Normal file
View File

@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Banner Day</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5877
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "banner-day",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^7.1.0",
"react-router-dom": "^7.1.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.17",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.13.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.3"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

7
public/banner-day.svg Normal file
View File

@ -0,0 +1,7 @@
<svg aria-hidden="true" role="img" width="800px" height="800px" viewBox="0 0 1024 1024" preserveAspectRatio="xMidYMid meet" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M688.5 325.4l139.9 41.5L659 536.3 477.5 354.8l169.4-169.4z" fill="#F05A1F"/>
<path d="M274.68 446.77l36.264-36.285L770.83 870.11l-36.264 36.285zM292.8 356c-20.1-20-52.6-20-72.6 0-20.1 20-20.1 52.6 0 72.6 20 20 52.5 20 72.6 0 20.1-20.1 20.1-52.6 0-72.6z" fill="#CCCCCC"/>
<path d="M307.21 406.621L488.722 225.11 670.235 406.62 488.722 588.134z" fill="#F05A1F"/>
<path d="M645.5 548.4c3.3 3.3 7.6 5 12 5 4.3 0 8.7-1.7 12-5L837 379.5c4.2-4.2 5.9-10.4 4.4-16.3-1.5-5.8-5.9-10.5-11.6-12.2l-129.6-38.8-38.5-130.6c-1.7-5.8-6.3-10.2-12.1-11.7-5.8-1.4-11.9 0.2-16.1 4.5L547.6 261 501 214c-6.6-6.7-17.3-6.7-23.9 0L322.9 369.5c-3.3-9.1-8.4-17.6-15.7-24.9-12.8-12.9-29.8-20-47.9-20s-35.1 7.1-47.9 20c-12.8 12.9-19.8 30-19.8 48.3 0 18.2 7 35.4 19.8 48.2 12.8 12.9 29.8 20 47.9 20 2.5 0 5-0.2 7.4-0.4L720 917.6c3.2 3.2 7.5 5 12 5s8.8-1.8 12-5l35.9-36.2c6.6-6.7 6.6-17.5 0-24.1L512.9 588l85.9-86.6 46.7 47z m-1.4-49.7l-21.3-21.4 25.1-25.3-3.8 46.7z m-6.7-279.8l33 112c1.6 5.5 5.9 9.9 11.4 11.5L793 375.7 678.7 490.8l6.7-82.3v-0.7c0-0.2 0.1-0.5 0.1-0.7 0-0.4-0.1-0.7-0.1-1 0-0.6-0.1-1.1-0.2-1.7-0.1-0.5-0.2-1-0.3-1.6-0.2-0.6-0.3-1.1-0.5-1.7-0.2-0.5-0.4-1-0.6-1.4-0.3-0.5-0.5-1-0.8-1.5-0.3-0.5-0.6-0.9-0.9-1.3-0.3-0.5-0.7-0.9-1.1-1.3-0.2-0.2-0.3-0.4-0.4-0.5l-109-109.8 65.8-66.4z m-401.9 198c-6.4-6.5-9.9-15-9.9-24.1 0-9.1 3.5-17.7 9.9-24.1 6.4-6.4 14.9-10 23.9-10 9 0 17.5 3.5 23.9 10 13.2 13.3 13.2 35 0 48.2-12.8 12.9-35 12.9-47.8 0z m496.6 464.3L301.3 447.1l5.9-6 5.9-6 163.9 165 267 269.1-11.9 12zM333.4 407.1L489 250.3l155.6 156.8L489 563.9 333.4 407.1z" fill="#211F1E"/>
<path d="M548.5 389.1H505v-43.9c0-9.4-7.6-17.1-16.9-17.1-9.3 0-16.9 7.6-16.9 17.1v43.9h-43.5c-9.3 0-16.9 7.6-16.9 17.1 0 9.4 7.6 17.1 16.9 17.1h43.5v43.9c0 9.4 7.6 17.1 16.9 17.1 9.3 0 16.9-7.6 16.9-17.1v-43.9h43.5c9.3 0 16.9-7.6 16.9-17.1 0.1-9.5-7.5-17.1-16.9-17.1z" fill="#211F1E"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

15
public/vite.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257">
<defs>
<linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%">
<stop offset="0%" stop-color="#41D1FF"></stop>
<stop offset="100%" stop-color="#BD34FE"></stop>
</linearGradient>
<linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%">
<stop offset="0%" stop-color="#FFEA83"></stop>
<stop offset="8.333%" stop-color="#FFDD35"></stop>
<stop offset="100%" stop-color="#FFA800"></stop>
</linearGradient>
</defs>
<path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path>
<path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

22
src/App.css Normal file
View File

@ -0,0 +1,22 @@
#root {
margin: 0 auto;
text-align: center;
}
.logo {
height: 9em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

35
src/App.jsx Normal file
View File

@ -0,0 +1,35 @@
import { useState, createContext } from 'react'
import { BrowserRouter, Route, Routes } from 'react-router'
import './index.css'
import Home from './pages/Home.jsx'
import Login from './pages/Login.jsx'
import Dashboard from './pages/Dashboard.jsx'
import RequiresAuth from './components/RequiresAuth'
import { AuthProvider } from './hooks/useAuth.jsx'
import { AuthContext } from './hooks/useAuth.jsx'
function App() {
const [auth, setAuth] = useState({
name: "",
email: "",
sessionToken: "",
isAuthed: false,
});
//const AuthContext = createContext();
return (
<AuthContext.Provider value={{auth, setAuth}}>
<BrowserRouter>
<Routes>
<Route index element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<RequiresAuth><Dashboard /></RequiresAuth>} />
</Routes>
</BrowserRouter>
</AuthContext.Provider>
)
}
export default App

1
src/assets/react.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

13
src/axios.js Normal file
View File

@ -0,0 +1,13 @@
import Axios from 'axios';
const axios = Axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
withXSRFToken: true,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
});
export default axios;

View File

@ -0,0 +1,23 @@
import { useNavigate } from "react-router";
import { useAuth } from "../hooks/useAuth";
function GlobalNav() {
const { auth, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate("/");
};
return (
<nav>
<ul>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
</ul>
{auth.isAuthed && <button onClick={handleLogout}>Logout</button>}
</nav>
);
}

View File

@ -0,0 +1,14 @@
import AuthConsumer from "../hooks/useAuth";
import { Navigate } from "react-router";
import { AuthContext } from '../hooks/useAuth.jsx'
import { useContext } from 'react'
export default function RequiresAuth({ children }) {
//const { auth } = useAuth();
//const { auth } = AuthConsumer();
//console.log(auth)
const { auth, setAuth } = useContext(AuthContext);
return auth.isAuthed === true ? children : <Navigate to="/login" replace />;
}

View File

@ -0,0 +1,40 @@
import { createContext, useContext, useState } from 'react';
import axios from '../axios';
const AuthContext = createContext({
user: null,
setUser: () => {},
csrfToken: () => {},
});
export const AuthProvider = ({ children }) => {
const [user, _setUser] = useState(
JSON.parse(localStorage.getItem('user')) || null
);
// Store the user in local storage to preserve auth state across refreshes
const setUser = (user) => {
if (user) {
localStorage.setItem('user', JSON.stringify(user));
} else {
localStorage.removeItem('user');
}
_setUser(user);
};
// csrf token generation for guest methods
const csrfToken = async () => {
await axios.get(`${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`);
return true;
};
return (
<AuthContext.Provider value={{ user, setUser, csrfToken }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};

48
src/hooks/useAPI.jsx Normal file
View File

@ -0,0 +1,48 @@
import { useContext } from "react";
import { AuthContext } from "./useAuth";
import { useNavigate } from "react-router";
export function useAPI() {
const { setAuth } = useContext(AuthContext);
const navigate = useNavigate();
function destroySession() {
setAuth({
name: "",
email: "",
sessionToken: "",
isAuthed: false,
})
navigate("/login");
}
return {
async get(endpoint) {
const response = await fetch(`${import.meta.env.VITE_API_URL}${endpoint}`, {
credentials: "include", // Include the cookie set in the browser by the APIs response to a successful login
});
// Session on the server has expired, destroy the session here
if (response.status == 403) {
destroySession();
}
return response.json();
},
async post(endpoint, payload) {
const response = await fetch(`${import.meta.env.VITE_API_URL}${endpoint}`, {
method: "POST",
body: JSON.stringify(payload),
credentials: "include",
});
// Session on the server has expired, destroy the session here
if (response.status == 403) {
destroySession();
}
return response.json();
}
}
}

49
src/hooks/useAuth.jsx Normal file
View File

@ -0,0 +1,49 @@
import { useState, createContext, useContext } from "react";
export const AuthContext = createContext();
export function useAuth() {
const [auth, setAuth] = useState({
name: "",
email: "",
sessionToken: "",
isAuthed: false,
});
return {
auth,
login() {
console.log("yeah were loggin in")
return new Promise((res) => {
setAuth({
name: "Non-existent Guy",
email: "noexist@example.com",
sessionToken: "555",
isAuthed: true,
});
res();
});
},
//logout() {
// return new Promise((res) => {
// setAuth({
// name: "",
// email: "",
// sessionToken: "dead-auth",
// isAuthed: false,
// });
// res();
// });
//},
};
}
export function AuthProvider({ children }) {
const auth = useAuth();
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
export default function AuthConsumer() {
return useContext(AuthContext);
}

79
src/index.css Normal file
View File

@ -0,0 +1,79 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100vw;
max-width: 100%;
height: 100vh;
padding: 0;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import { Navigate, Outlet } from 'react-router';
import { useAuth } from '../contexts/AuthContext';
export default function GuestLayout() {
const { user } = useAuth();
// if user is logged in, redirect to dashboard
if (user) {
return <Navigate to="/dashboard" />;
}
return (
<>
<Outlet />
</>
);
}

View File

@ -0,0 +1,98 @@
import React, { useEffect } from 'react';
import { Navigate, Outlet, NavLink } from 'react-router';
import axios from '../axios';
import { useAuth } from '../contexts/AuthContext';
import bannerDayLogo from '/banner-day.svg'
export default function ProtectedLayout() {
const { user, setUser } = useAuth();
// check if user is logged in or not from server
useEffect(() => {
(async () => {
try {
const response = await axios.get('/api/user');
if (response.status === 200) {
setUser(response.data);
}
} catch (error) {
if (error.response.status === 401) {
localStorage.removeItem('user');
window.location.href = '/';
}
}
})();
}, []);
// if user is not logged in, redirect to login page
if (!user) {
return <Navigate to="/login" />;
}
// logout user
const handleLogout = async () => {
try {
const resp = await axios.post('/api/logout');
if (resp.status === 200) {
localStorage.removeItem('user');
window.location.href = '/login';
}
} catch (error) {
console.log(error);
}
};
return (
<>
<nav className="bg-white border-gray-200 px-2 sm:px-4 py-2.5 dark:bg-gray-900 w-screen">
<div className="container flex flex-wrap items-center justify-between mx-auto">
<NavLink to="/dashboard">
<div className="float-left">
<img src={bannerDayLogo} className="w-12 inline" alt="Banner Day logo" />
<span className="text-xl font-semibold whitespace-nowrap dark:text-white">
Banner Day
</span>
</div>
</NavLink>
<div className="hidden w-full md:block md:w-auto" id="navbar-default">
<ul className="flex flex-col p-4 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:text-sm md:font-medium md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li>
<NavLink
to="/profile"
className={({ isActive }) =>
isActive
? 'block py-2 pl-3 pr-4 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 dark:text-white'
: 'block py-2 pl-3 pr-4 rounded md:bg-transparent md:p-0 dark:text-gray-400 md:dark:hover:text-white'
}>
Profile
</NavLink>
</li>
<li>
<NavLink
to="/about"
className={({ isActive }) =>
isActive
? 'block py-2 pl-3 pr-4 text-white bg-blue-700 rounded md:bg-transparent md:text-blue-700 md:p-0 dark:text-white'
: 'block py-2 pl-3 pr-4 rounded md:bg-transparent md:p-0 dark:text-gray-400 md:dark:hover:text-white'
}>
About
</NavLink>
</li>
<li>
<a
onClick={handleLogout}
href="#"
className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-gray-400 md:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">
Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
<main className="container flex justify-center flex-col items-center mt-10 mx-auto">
<Outlet />
</main>
</>
);
}

15
src/main.jsx Normal file
View File

@ -0,0 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
//import App from './App.jsx'
import { RouterProvider } from 'react-router'
import { AuthProvider } from './contexts/AuthContext.jsx'
import router from './router.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</StrictMode>,
)

33
src/pages/About.jsx Normal file
View File

@ -0,0 +1,33 @@
import bannerDayLogo from '/banner-day.svg'
function About() {
return (
<>
<div className="container">
<img src={bannerDayLogo} className="logo mx-auto" alt="Banner Day logo" />
</div>
<h1>Banner Day</h1>
<h4>Easily manage and track banner ads across affiliate websites</h4>
<div className="card">
<p>
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
</p>
<p>
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
Banner day makes it easy to manage your own ad campaigns on affiliate websites.
It's focused on banner ads that can display any image and link to any target.
</p>
</div>
</>
)
}
export default About

35
src/pages/Dashboard.jsx Normal file
View File

@ -0,0 +1,35 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router'
import axios from '../axios';
function Dashboard() {
const [usersCount, setUsersCount] = useState(0);
useEffect(() => {
console.log("at dashboard");
axios.get('/api/num-users').then((response) => {
console.log("got users");
setUsersCount(response.data.num_users);
}).catch(error => console.log(error));
//(async () => {
// try {
// const response = await axios.get('/api/num-users');
// if (response.status === 200) {
// setUsersCount(response.data.num_users);
// }
// } catch (error) {
// console.log(error);
// }
//})
}, [setUsersCount])
return (
<>
<h1>Dashboard</h1>
<p>Number of users: {usersCount}</p>
</>
)
}
export default Dashboard

22
src/pages/Home.jsx Normal file
View File

@ -0,0 +1,22 @@
import bannerDayLogo from '/banner-day.svg'
import '../App.css'
import { Link } from 'react-router'
function Home() {
return (
<>
<div className="container mx-auto">
<img src={bannerDayLogo} className="logo mx-auto" alt="Banner Day logo" />
</div>
<h1>Banner Day</h1>
<h4>Easily manage and track banner ads across affiliate websites</h4>
<div className="card">
<Link to="/login">
<button>Go to Login</button>
</Link>
</div>
</>
)
}
export default Home

117
src/pages/Login.jsx Normal file
View File

@ -0,0 +1,117 @@
import axios from '../axios';
import { useState } from "react";
import { useNavigate } from "react-router"
import bannerDayLogo from '/banner-day.svg'
import { useAuth } from '../contexts/AuthContext.jsx';
function Login() {
const [form, setForm] = useState({
email: "",
password: "",
});
const navigate = useNavigate();
const { setUser, csrfToken } = useAuth();
const handleLogin = async (event) => {
event.preventDefault();
await csrfToken();
const body = {
email: form.email,
password: form.password,
};
try {
const response = await axios.post('/api/login', body);
if (response.status === 200) {
setUser(response.data.user);
return navigate("/dashboard");
}
} catch (error) {
if (error.response.status === 401) {
console.log(error);
}
}
}
return (
<>
<section className="bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<div className="mx-auto">
<img src={bannerDayLogo} className="w-12 inline" alt="Banner Day logo" />
<span className="text-xl font-semibold whitespace-nowrap dark:text-white">
Banner Day
</span>
</div>
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Login to your account
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleLogin}>
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Your email
</label>
<input
type="email"
name="email"
id="email"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="name@company.com"
required
value={form.email}
onChange={event => {
setForm({
...form,
email: event.target.value
});
}}
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Password
</label>
<input
type="password"
name="password"
id="password"
placeholder="••••••••"
className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required
value={form.password}
onChange={event => {
setForm({
...form,
password: event.target.value
});
}}
/>
</div>
<button
type="submit"
className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Log in
</button>
</form>
</div>
</div>
</div>
</section>
</>
)
}
export default Login

40
src/router.jsx Normal file
View File

@ -0,0 +1,40 @@
import { createBrowserRouter } from 'react-router';
import Login from './pages/Login';
import Home from './pages/Home';
import About from './pages/About';
import Dashboard from './pages/Dashboard';
import ProtectedLayout from './layouts/ProtectedLayout';
import GuestLayout from './layouts/GuestLayout';
const router = createBrowserRouter([
{
path: '/',
element: <GuestLayout />,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/login',
element: <Login />,
},
],
},
{
path: '/',
element: <ProtectedLayout />,
children: [
{
path: '/dashboard',
element: <Dashboard />,
},
{
path: '/about',
element: <About />,
},
],
},
]);
export default router;

12
tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

7
vite.config.js Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})