More work?
This commit is contained in:
parent
add6489e40
commit
640f81a79f
30 changed files with 5082 additions and 405 deletions
22
backend/.env
Normal file
22
backend/.env
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Environment
|
||||
NODE_ENV=development # Change to 'production' for live environment
|
||||
|
||||
# Database Configuration - development localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_NAME=tower_test
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
|
||||
# Live Database Configuration (used only in production)
|
||||
DB_USER_PROD=your_live_database_user
|
||||
DB_PASSWORD_PROD=your_live_database_password
|
||||
DB_NAME_PROD=your_database_name_live
|
||||
DB_HOST_PROD=your_live_database_host
|
||||
DB_PORT_PROD=5432
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your_jwt_secret
|
||||
|
||||
# Server Configuration
|
||||
PORT=3000
|
59
backend/controllers/authController.js
Normal file
59
backend/controllers/authController.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret";
|
||||
|
||||
export const register = async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ message: "Username and password are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
"INSERT INTO users (username, password) VALUES ($1, $2)",
|
||||
[username, hashedPassword]
|
||||
);
|
||||
res.status(201).json({ message: "User registered successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error registering user" });
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ message: "Username and password are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query("SELECT * FROM users WHERE username = $1", [username]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ message: "Invalid username or password" });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ message: "Invalid username or password" });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, {
|
||||
expiresIn: "1h",
|
||||
});
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error logging in" });
|
||||
}
|
||||
};
|
47
backend/controllers/recipeController.js
Normal file
47
backend/controllers/recipeController.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
export const getAllRecipes = async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT * FROM recipes");
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error fetching recipes" });
|
||||
}
|
||||
};
|
||||
|
||||
export const addRecipe = async (req, res) => {
|
||||
const { title, description, ingredients, tags, image_url } = req.body;
|
||||
|
||||
if (!title || !description || !ingredients) {
|
||||
return res.status(400).json({ message: "Title, description, and ingredients are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
"INSERT INTO recipes (title, description, ingredients, tags, image_url) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[title, description, ingredients, tags, image_url]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error adding recipe" });
|
||||
}
|
||||
};
|
||||
|
||||
export const searchRecipesByIngredient = async (req, res) => {
|
||||
const { ingredient } = req.query;
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
"SELECT * FROM recipes WHERE $1 = ANY (ingredients)",
|
||||
[ingredient]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error searching recipes" });
|
||||
}
|
||||
};
|
13
backend/controllers/userController.js
Normal file
13
backend/controllers/userController.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Pool } from "pg";
|
||||
|
||||
const pool = new Pool();
|
||||
|
||||
export const getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT id, username FROM users");
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error fetching users" });
|
||||
}
|
||||
};
|
19
backend/middlewares/authMiddleware.js
Normal file
19
backend/middlewares/authMiddleware.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret";
|
||||
|
||||
export const authenticateToken = (req, res, next) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = jwt.verify(token, JWT_SECRET);
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error("Invalid token:", error.message); // Log the error for debugging
|
||||
res.status(403).json({ message: "Invalid token" });
|
||||
}
|
||||
};
|
28
backend/models/db.js
Normal file
28
backend/models/db.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Sequelize } from "sequelize";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const sequelize = new Sequelize(
|
||||
isProduction ? process.env.DB_NAME_PROD : process.env.DB_NAME,
|
||||
isProduction ? process.env.DB_USER_PROD : process.env.DB_USER,
|
||||
isProduction ? process.env.DB_PASSWORD_PROD : process.env.DB_PASSWORD,
|
||||
{
|
||||
host: isProduction ? process.env.DB_HOST_PROD : process.env.DB_HOST,
|
||||
dialect: "postgres",
|
||||
port: isProduction ? process.env.DB_PORT_PROD : process.env.DB_PORT,
|
||||
logging: false, // Disable logging for cleaner output
|
||||
}
|
||||
);
|
||||
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log("Connection to the database has been established successfully.");
|
||||
} catch (error) {
|
||||
console.error("Unable to connect to the database:", error);
|
||||
}
|
||||
};
|
||||
|
||||
testConnection();
|
||||
|
||||
export default sequelize;
|
2175
backend/package-lock.json
generated
Normal file
2175
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
backend/package.json
Normal file
25
backend/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "hub-site-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for the Hub-Site project",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.0",
|
||||
"body-parser": "2.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "5.0.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"pg": "^8.10.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.32.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.7"
|
||||
}
|
||||
}
|
67
backend/routes/auth.js
Normal file
67
backend/routes/auth.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Router } from "express";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import pkg from "pg"; // Importing the whole CommonJS module
|
||||
|
||||
const { Pool } = pkg; // Destructuring Pool from the imported CommonJS module
|
||||
|
||||
const router = Router();
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your_jwt_secret";
|
||||
|
||||
// Register
|
||||
router.post("/register", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
"INSERT INTO users (username, password) VALUES ($1, $2)",
|
||||
[username, hashedPassword]
|
||||
);
|
||||
res.status(201).json({ message: "User registered successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error registering user" });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post("/login", async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const result = await pool.query("SELECT * FROM users WHERE username = $1", [
|
||||
username,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ message: "Invalid username or password" });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ message: "Invalid username or password" });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, {
|
||||
expiresIn: "1h",
|
||||
});
|
||||
|
||||
res.json({ token });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error logging in" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
0
backend/routes/index.js
Normal file
0
backend/routes/index.js
Normal file
59
backend/routes/recipes.js
Normal file
59
backend/routes/recipes.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import express from "express";
|
||||
import pkg from "pg";
|
||||
|
||||
const { Pool } = pkg; // Destructure the Pool object from the CommonJS module
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
// Get all recipes
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT * FROM recipes");
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error fetching recipes" });
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new recipe
|
||||
router.post("/", async (req, res) => {
|
||||
const { title, description, ingredients, tags, image_url } = req.body;
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
"INSERT INTO recipes (title, description, ingredients, tags, image_url) VALUES ($1, $2, $3, $4, $5) RETURNING *",
|
||||
[title, description, ingredients, tags, image_url]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error adding recipe" });
|
||||
}
|
||||
});
|
||||
|
||||
// Search recipes by ingredients
|
||||
router.get("/search", async (req, res) => {
|
||||
const { ingredient } = req.query;
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
"SELECT * FROM recipes WHERE $1 = ANY (ingredients)",
|
||||
[ingredient]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error searching recipes" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
27
backend/routes/users.js
Normal file
27
backend/routes/users.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import express from "express";
|
||||
import pkg from "pg";
|
||||
|
||||
const { Pool } = pkg; // Destructure Pool from the CommonJS module
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: process.env.DB_PORT,
|
||||
});
|
||||
|
||||
// Get all users (Admin-only functionality)
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query("SELECT id, username FROM users");
|
||||
res.json(result.rows);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: "Error fetching users" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
33
backend/server.js
Normal file
33
backend/server.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import dotenv from "dotenv";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
|
||||
import authRoutes from "./routes/auth.js";
|
||||
import recipeRoutes from "./routes/recipes.js";
|
||||
import userRoutes from "./routes/users.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Routes
|
||||
app.use("/auth", authRoutes);
|
||||
app.use("/recipes", recipeRoutes);
|
||||
app.use("/users", userRoutes);
|
||||
|
||||
// Fallback Route
|
||||
app.get("/", (req, res) => {
|
||||
res.send("Welcome to the Hub-Site backend API!");
|
||||
});
|
||||
|
||||
// Server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
});
|
||||
|
1311
package-lock.json
generated
1311
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
@ -13,11 +13,19 @@
|
|||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"start:backend": "cd backend && npm run dev",
|
||||
"start:frontend": "cd ./ && npm run dev --host",
|
||||
"dev-all": "concurrently \"npm run start:backend\" \"npm run start:frontend\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@tsparticles/slim": "^3.5.0",
|
||||
"@tsparticles/vue3": "^3.0.1",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
"express": "^4.21.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.1",
|
||||
"pinia": "^2.2.6",
|
||||
"vue": "^3.5.12",
|
||||
"vue-router": "^4.4.5"
|
||||
|
@ -33,6 +41,7 @@
|
|||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"concurrently": "^9.1.0",
|
||||
"cypress": "^13.15.1",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
|
|
80
src/App.vue
80
src/App.vue
|
@ -1,21 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
// Authentication state
|
||||
const isAuthenticated = ref(false);
|
||||
const username = ref("");
|
||||
|
||||
// Theme state and available themes
|
||||
const theme = ref(localStorage.getItem("theme") || "mocha");
|
||||
const availableThemes = [
|
||||
"mocha",
|
||||
"latte",
|
||||
"yule-night",
|
||||
"yule-day",
|
||||
"midsummer-twilight",
|
||||
"midsummer-daylight",
|
||||
"fireworks-night",
|
||||
"parade-day",
|
||||
"harvest-twilight",
|
||||
"golden-hour",
|
||||
"stargazer",
|
||||
"daydreamer",
|
||||
];
|
||||
|
||||
// Apply theme and check authentication state
|
||||
onMounted(() => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
isAuthenticated.value = true;
|
||||
username.value = localStorage.getItem("username") || "";
|
||||
}
|
||||
document.documentElement.setAttribute("data-theme", theme.value);
|
||||
});
|
||||
|
||||
// Logout function
|
||||
const logout = () => {
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("username");
|
||||
isAuthenticated.value = false;
|
||||
window.location.href = "/";
|
||||
};
|
||||
|
||||
// Change theme function
|
||||
const changeTheme = (newTheme: string) => {
|
||||
theme.value = newTheme;
|
||||
document.documentElement.setAttribute("data-theme", newTheme);
|
||||
localStorage.setItem("theme", newTheme);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<!-- Header and Navigation -->
|
||||
<header>
|
||||
<nav>
|
||||
<nav class="nav-bar">
|
||||
<div class="nav-links">
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/servers">Servers</RouterLink>
|
||||
<RouterLink to="/projects">Projects</RouterLink>
|
||||
<a href="" target="_blank">Foundry VTT</a>
|
||||
<RouterLink v-if="isAuthenticated" to="/recipes">Recipes</RouterLink>
|
||||
<RouterLink v-if="isAuthenticated" to="/calendar">Calendar</RouterLink>
|
||||
<RouterLink v-if="isAuthenticated" to="/gallery">Gallery</RouterLink>
|
||||
<a href="https://foundryvtt.example.com" target="_blank">Foundry VTT</a>
|
||||
<a href="https://oekaki.smgames.club" target="_blank">Oekaki</a>
|
||||
<a href="https://social.smgames.club/" target="_blank">Social</a>
|
||||
<a href="https://madstar.studio" target="_blank">Shop</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Authentication & Theme Controls -->
|
||||
<section class="controls">
|
||||
<div class="theme-selector">
|
||||
<label for="theme-switcher">Theme:</label>
|
||||
<select id="theme-switcher" v-model="theme" @change="changeTheme(theme)">
|
||||
<option v-for="t in availableThemes" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="auth-controls">
|
||||
<span v-if="isAuthenticated">
|
||||
Welcome, {{ username }}!
|
||||
<button @click="logout" class="logout-button">Logout</button>
|
||||
</span>
|
||||
<span v-else>
|
||||
<RouterLink to="/login">Login</RouterLink> |
|
||||
<RouterLink to="/register">Register</RouterLink>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
|
|
|
@ -1,11 +1,154 @@
|
|||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-surface0: #1e1e2e;
|
||||
--color-surface1: #313244;
|
||||
--color-surface2: #45475a;
|
||||
--color-text: #cdd6f4;
|
||||
--color-accent: #a6e3a1;
|
||||
--color-accent-hover: #89d58d;
|
||||
--color-border: #585b70;
|
||||
/* Catppuccin Mocha */
|
||||
:root[data-theme="mocha"] {
|
||||
--color-surface0: #1e1e2e; /* Base */
|
||||
--color-surface1: #313244; /* Mantle */
|
||||
--color-surface2: #45475a; /* Surface */
|
||||
--color-text: #cdd6f4; /* Text */
|
||||
--color-accent: #a6e3a1; /* Green */
|
||||
--color-accent-hover: #89d58d; /* Hover Green */
|
||||
--color-border: #585b70; /* Overlay */
|
||||
--color-rosewater: #f5e0dc;
|
||||
--color-flamingo: #f2cdcd;
|
||||
--color-pink: #f5c2e7;
|
||||
--color-mauve: #cba6f7;
|
||||
--color-red: #f38ba8;
|
||||
--color-maroon: #eba0ac;
|
||||
--color-peach: #fab387;
|
||||
--color-yellow: #f9e2af;
|
||||
--color-teal: #94e2d5;
|
||||
--color-sky: #89dceb;
|
||||
--color-sapphire: #74c7ec;
|
||||
--color-blue: #89b4fa;
|
||||
--color-lavender: #b4befe;
|
||||
--color-overlay0: #6c7086;
|
||||
--color-overlay1: #7f849c;
|
||||
--color-overlay2: #9399b2;
|
||||
}
|
||||
|
||||
/* Catppuccin Latte */
|
||||
:root[data-theme="latte"] {
|
||||
--color-surface0: #eff1f5; /* Base */
|
||||
--color-surface1: #e6e9ef; /* Mantle */
|
||||
--color-surface2: #ccd0da; /* Surface */
|
||||
--color-text: #4c4f69; /* Text */
|
||||
--color-accent: #d7827e; /* Peach */
|
||||
--color-accent-hover: #e04f58; /* Hover Peach */
|
||||
--color-border: #9ca0b0; /* Overlay */
|
||||
--color-rosewater: #dc8a78;
|
||||
--color-flamingo: #dd7878;
|
||||
--color-pink: #ea76cb;
|
||||
--color-mauve: #8839ef;
|
||||
--color-red: #d20f39;
|
||||
--color-maroon: #e64553;
|
||||
--color-peach: #fe640b;
|
||||
--color-yellow: #df8e1d;
|
||||
--color-teal: #40a02b;
|
||||
--color-sky: #04a5e5;
|
||||
--color-sapphire: #209fb5;
|
||||
--color-blue: #1e66f5;
|
||||
--color-lavender: #7287fd;
|
||||
--color-overlay0: #6c6f85;
|
||||
--color-overlay1: #8c8fa1;
|
||||
--color-overlay2: #9ca0b0;
|
||||
}
|
||||
|
||||
:root[data-theme="yule-night"] {
|
||||
--color-surface0: #1b1d28; /* Deep midnight */
|
||||
--color-surface1: #252936; /* Frosty steel */
|
||||
--color-surface2: #343a48; /* Snow shadow */
|
||||
--color-text: #d4e6f4; /* Pale moonlight */
|
||||
--color-accent: #a3cf8e; /* Pine green */
|
||||
--color-accent-hover: #7fb36a; /* Mistletoe */
|
||||
--color-border: #475266; /* Frosty edges */
|
||||
}
|
||||
|
||||
:root[data-theme="yule-day"] {
|
||||
--color-surface0: #f5f3ed; /* Fresh snow */
|
||||
--color-surface1: #ece7df; /* Frosty beige */
|
||||
--color-surface2: #dcd3c3; /* Hearth ash */
|
||||
--color-text: #4e4b43; /* Warm bark */
|
||||
--color-accent: #7ea86a; /* Pine green */
|
||||
--color-accent-hover: #577a46; /* Darker pine */
|
||||
--color-border: #9d9684; /* Frosted wood */
|
||||
}
|
||||
|
||||
:root[data-theme="midsummer-twilight"] {
|
||||
--color-surface0: #241f36; /* Starry violet */
|
||||
--color-surface1: #2e2746; /* Dusky purple */
|
||||
--color-surface2: #403659; /* Twilight shadow */
|
||||
--color-text: #f6d8e8; /* Fading pink */
|
||||
--color-accent: #ffd983; /* Sunflower gold */
|
||||
--color-accent-hover: #f4be5c; /* Saffron yellow */
|
||||
--color-border: #6b5a89; /* Lavender dusk */
|
||||
}
|
||||
|
||||
:root[data-theme="midsummer-daylight"] {
|
||||
--color-surface0: #faf8eb; /* Bright sunlight */
|
||||
--color-surface1: #f2e7c4; /* Sunlit field */
|
||||
--color-surface2: #e6d399; /* Wheat gold */
|
||||
--color-text: #3b3a24; /* Tree bark */
|
||||
--color-accent: #f5c34e; /* Summer gold */
|
||||
--color-accent-hover: #d69a30; /* Sunset orange */
|
||||
--color-border: #a38a5b; /* Golden shadows */
|
||||
}
|
||||
|
||||
:root[data-theme="fireworks-night"] {
|
||||
--color-surface0: #0a0e1a; /* Starry sky */
|
||||
--color-surface1: #121b32; /* Midnight blue */
|
||||
--color-surface2: #1f2945; /* Smoke cloud */
|
||||
--color-text: #ffffff; /* Brilliant white */
|
||||
--color-accent: #ff4c4c; /* Firework red */
|
||||
--color-accent-hover: #ff726f; /* Flaming red */
|
||||
--color-border: #3b4e7e; /* Steel blue */
|
||||
}
|
||||
|
||||
:root[data-theme="parade-day"] {
|
||||
--color-surface0: #fafafa; /* White fabric */
|
||||
--color-surface1: #eaeaea; /* Pale silver */
|
||||
--color-surface2: #c9d3e3; /* Cerulean mist */
|
||||
--color-text: #2b2b2b; /* Midnight blue */
|
||||
--color-accent: #ff3b3b; /* Firework red */
|
||||
--color-accent-hover: #cc2a2a; /* Deep crimson */
|
||||
--color-border: #8795b4; /* Cloud blue */
|
||||
}
|
||||
|
||||
:root[data-theme="harvest-twilight"] {
|
||||
--color-surface0: #1d1b13; /* Shadowed wheat field */
|
||||
--color-surface1: #29231a; /* Earthen soil */
|
||||
--color-surface2: #4b3b27; /* Golden dusk */
|
||||
--color-text: #f2e5ce; /* Pale harvest moon */
|
||||
--color-accent: #e4a672; /* Pumpkin orange */
|
||||
--color-accent-hover: #c88752; /* Rusted leaves */
|
||||
--color-border: #5d4633; /* Bark brown */
|
||||
}
|
||||
|
||||
:root[data-theme="golden-hour"] {
|
||||
--color-surface0: #fef6e6; /* Golden wheat */
|
||||
--color-surface1: #fdecc8; /* Honey glow */
|
||||
--color-surface2: #fcd399; /* Pumpkin yellow */
|
||||
--color-text: #533c24; /* Harvest bark */
|
||||
--color-accent: #e78a4b; /* Autumn orange */
|
||||
--color-accent-hover: #d06b34; /* Deep amber */
|
||||
--color-border: #a88a5f; /* Field shadows */
|
||||
}
|
||||
|
||||
:root[data-theme="stargazer"] {
|
||||
--color-surface0: #0d1321; /* Midnight sky */
|
||||
--color-surface1: #1c2533; /* Cloudy night */
|
||||
--color-surface2: #283142; /* Subtle twilight */
|
||||
--color-text: #d6e0f5; /* Starlight */
|
||||
--color-accent: #62b6cb; /* Cool cyan */
|
||||
--color-accent-hover: #89d3ed; /* Soft teal */
|
||||
--color-border: #3e506a; /* Lunar blue */
|
||||
}
|
||||
|
||||
:root[data-theme="daydreamer"] {
|
||||
--color-surface0: #f9f9fc; /* Light paper */
|
||||
--color-surface1: #eceef3; /* Morning mist */
|
||||
--color-surface2: #d7dcea; /* Overcast sky */
|
||||
--color-text: #2e3440; /* Quiet gray */
|
||||
--color-accent: #5e81ac; /* Blue-gray calm */
|
||||
--color-accent-hover: #81a1c1; /* Brighter sky blue */
|
||||
--color-border: #b2c4d4; /* Subtle frost */
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,56 @@ header nav a.router-link-exact-active {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Controls Section */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--color-surface2);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-selector select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-surface0);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.theme-selector select:hover {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
background: none;
|
||||
border: 1px solid var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
main {
|
||||
margin-top: 2rem;
|
||||
|
@ -69,20 +119,20 @@ main {
|
|||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, #313244, #45475a); /* Catppuccin Mocha colors */
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, var(--color-surface1), var(--color-surface2));
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
padding: 1rem 0.5rem; /* Reduced padding for smaller height */
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2rem; /* Smaller font size for the heading */
|
||||
margin-bottom: 0.5rem; /* Reduced bottom margin */
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.25rem; /* Smaller font size for the paragraph */
|
||||
margin: 0; /* No extra margin */
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
|
@ -91,17 +141,17 @@ main {
|
|||
}
|
||||
|
||||
.section-box {
|
||||
margin-bottom: 2rem; /* Reduced bottom margin for sections */
|
||||
padding: 1rem; /* Reduced padding inside the section box */
|
||||
background-color: #313244;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-surface1);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-box h2 {
|
||||
font-size: 1.5rem; /* Slightly smaller font size for headers */
|
||||
margin: 0.5rem 0 1rem; /* Reduced top margin and kept space below */
|
||||
color: #a6e3a1;
|
||||
font-size: 1.5rem;
|
||||
margin: 0.5rem 0 1rem;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
|
@ -110,8 +160,9 @@ main {
|
|||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background-color: #45475a;
|
||||
background-color: var(--color-surface2);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
|
@ -137,79 +188,525 @@ main {
|
|||
}
|
||||
|
||||
.link {
|
||||
color: #a6e3a1;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #313244;
|
||||
background-color: var(--color-surface1);
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
background-color: #89d58d;
|
||||
color: #1e1e2e;
|
||||
background-color: var(--color-accent-hover);
|
||||
color: var(--color-surface0);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background-color: #1e1e2e;
|
||||
background-color: var(--color-surface2);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: #cdd6f4;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #a6e3a1;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
color: #89d58d;
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* Responsive Navbar */
|
||||
@media (max-width: 768px) {
|
||||
header nav {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
/* Filters and Project Styles */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
header nav a {
|
||||
padding: 0.5rem;
|
||||
.filter-bar button {
|
||||
background: var(--color-surface1);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.filter-bar button:hover,
|
||||
.filter-bar button.active {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
}
|
||||
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: var(--color-surface1);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.project-card p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Player Icon */
|
||||
.players-icon {
|
||||
color: #89b4fa;
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-surface1);
|
||||
color: var(--color-text);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Servers View */
|
||||
.servers {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.server-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.server-card {
|
||||
background-color: var(--color-surface1);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.server-card:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.server-card h3 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.server-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Status Icons */
|
||||
.online {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: #f7768e; /* Matches Catppuccin Mocha */
|
||||
}
|
||||
|
||||
.unknown {
|
||||
color: #cba6f7; /* Matches Catppuccin Mocha */
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-surface1);
|
||||
color: var(--color-text);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.modal-content .banner {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.ip {
|
||||
background: var(--color-surface2);
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Copy Icon */
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
color: #89b4fa;
|
||||
margin-left: 0.5rem;
|
||||
color: #89b4fa; /* Matches Catppuccin Mocha */
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.copy-icon:hover {
|
||||
color: #74a8e0;
|
||||
color: #74a8e0; /* Matches Catppuccin Mocha */
|
||||
}
|
||||
|
||||
/* Status Icons */
|
||||
.online-icon {
|
||||
color: #a6e3a1;
|
||||
margin-right: 0.5rem;
|
||||
.close-button {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
color: #f38ba8;
|
||||
margin-right: 0.5rem;
|
||||
.close-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.unknown-icon {
|
||||
color: #cba6f7;
|
||||
margin-right: 0.5rem;
|
||||
.close-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Projects View */
|
||||
.projects {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.faq-button {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.faq-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.faq-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Filter Buttons */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filter-bar button {
|
||||
background: var(--color-surface1);
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.filter-bar button:hover,
|
||||
.filter-bar button.active {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
}
|
||||
|
||||
/* Project Grid */
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: var(--color-surface1);
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.project-card p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--color-surface2);
|
||||
color: var(--color-text);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* FAQ Modal */
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
overflow-y: auto; /* Ensure overlay scrolls if content overflows */
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.modal-content {
|
||||
background: var(--color-surface1);
|
||||
color: var(--color-text);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh; /* Limit the modal's height to 80% of the viewport */
|
||||
overflow-y: auto; /* Enable vertical scrolling if content overflows */
|
||||
text-align: left;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Header in Modal */
|
||||
.modal-content h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
list-style: none; /* Remove default bullet points */
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.faq-list li {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.faq-list strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.faq-list p {
|
||||
margin: 0.5rem 0 0;
|
||||
}
|
||||
|
||||
/* Close Button */
|
||||
.close-button {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Links Section */
|
||||
.links {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.links p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
color: var(--color-accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Login Page Styles */
|
||||
.login {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--color-surface1);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login h1 {
|
||||
font-size: 2rem;
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login .form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.login input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-surface0);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.login input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 4px var(--color-accent);
|
||||
}
|
||||
|
||||
.login .btn-login {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-surface0);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.login .btn-login:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.login .btn-login:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.login .error {
|
||||
color: #f7768e;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login .register-link {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login .register-link a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login .register-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
21
src/authService.ts
Normal file
21
src/authService.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import axios from "axios";
|
||||
|
||||
const API_BASE = "http://localhost:3000"; // Update with your backend address
|
||||
|
||||
export const register = async (username: string, password: string) => {
|
||||
return axios.post(`${API_BASE}/register`, { username, password });
|
||||
};
|
||||
|
||||
export const login = async (username: string, password: string) => {
|
||||
const response = await axios.post(`${API_BASE}/login`, { username, password });
|
||||
const token = response.data.token;
|
||||
localStorage.setItem("token", token); // Save token for authenticated requests
|
||||
return token;
|
||||
};
|
||||
|
||||
export const getProtectedData = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
return axios.get(`${API_BASE}/protected`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
};
|
|
@ -9,13 +9,6 @@ const router = createRouter({
|
|||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
/* Rename name to the page name to add another page
|
||||
{
|
||||
path: '/name',
|
||||
name: 'name',
|
||||
component: () => import('../views/NameView.vue'),
|
||||
},
|
||||
*/
|
||||
{
|
||||
path: '/servers',
|
||||
name: 'servers',
|
||||
|
@ -26,6 +19,39 @@ const router = createRouter({
|
|||
name: 'projects',
|
||||
component: () => import('../views/ProjectsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('../views/LoginView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: () => import('../views/RegisterView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/gallery',
|
||||
name: 'gallery',
|
||||
component: () => import('../views/GalleryView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/calendar',
|
||||
name: 'calendar',
|
||||
component: () => import('../views/CalendarView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/recipes',
|
||||
name: 'recipes',
|
||||
component: () => import('../views/RecipesView.vue'),
|
||||
},
|
||||
// Example for adding a new page
|
||||
/*
|
||||
{
|
||||
path: '/your-new-page',
|
||||
name: 'yourNewPage',
|
||||
component: () => import('../views/YourNewPageView.vue'),
|
||||
},
|
||||
*/
|
||||
],
|
||||
})
|
||||
|
||||
|
|
0
src/views/CalendarView.vue
Normal file
0
src/views/CalendarView.vue
Normal file
0
src/views/GalleryView.vue
Normal file
0
src/views/GalleryView.vue
Normal file
|
@ -14,28 +14,31 @@
|
|||
<div class="section-box">
|
||||
<h2>Family Resources</h2>
|
||||
<div class="section-content">
|
||||
<!-- Sharkey Section (Always Visible) -->
|
||||
<div class="card">
|
||||
<h3>Sharkey</h3>
|
||||
<p>
|
||||
Twitter alternative without a flood of Nazis.<br><br>
|
||||
We manually approve all members + verify them outside of the net, too.<br><br>
|
||||
Twitter alternative without a flood of Nazis.<br /><br />
|
||||
We manually approve all members + verify them outside of the net, too.<br /><br />
|
||||
Fam & friends only to make an account in our space, but you can use any Sharkey/Mastodon/etc instance to register elsewhere and talk to us.
|
||||
</p>
|
||||
<a href="https://social.smgames.club/" class="link">Go to Platform</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
||||
<!-- Conditional Sections (Visible only if logged in) -->
|
||||
<div v-if="isAuthenticated" class="card">
|
||||
<h3>Discord</h3>
|
||||
<p>
|
||||
Connect with others on our private Discord... which is by invite only!<br><br>
|
||||
Connect with others on our private Discord... which is by invite only!<br /><br />
|
||||
Sorry, you won't actually find the link here! If you're related to Starr, ask her about it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div v-if="isAuthenticated" class="card">
|
||||
<h3>Photo Gallery</h3>
|
||||
<p>Explore our collection of family photos, preserved for generations.</p>
|
||||
<a href="/photos" class="link">View Photos</a>
|
||||
<a href="/gallery" class="link">View Photos</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div v-if="isAuthenticated" class="card">
|
||||
<h3>Recipe Collection</h3>
|
||||
<p>Browse our curated family recipes, shared across generations.</p>
|
||||
<a href="/recipes" class="link">Explore Recipes</a>
|
||||
|
@ -49,16 +52,12 @@
|
|||
<div class="section-content">
|
||||
<div class="card">
|
||||
<h3>Game Servers</h3>
|
||||
<p>Privately hosted servers for Minecraft, Garry's Mod, TF2, Terraria, and more. Clicking each game shows instructions to get on.</p>
|
||||
<p>
|
||||
Privately hosted servers for Minecraft, Garry's Mod, TF2, Terraria, and more.
|
||||
Clicking each game shows instructions to get on.
|
||||
</p>
|
||||
<a href="/servers" class="link">See Our Game Servers</a>
|
||||
</div>
|
||||
<!--
|
||||
<div class="card">
|
||||
<h3>Server Dashboard</h3>
|
||||
<p>Manage and monitor our hosted game servers in one place.</p>
|
||||
<a href="/server-dashboard" class="link">Open Dashboard</a>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -68,7 +67,9 @@
|
|||
<div class="section-content">
|
||||
<div class="card">
|
||||
<h3>Forgejo Repository</h3>
|
||||
<p>Access our code repositories and collaborate on development with our Forgejo instance.</p>
|
||||
<p>
|
||||
Access our code repositories and collaborate on development with our Forgejo instance.
|
||||
</p>
|
||||
<a href="https://git.smgames.club/" class="link" target="_blank">Visit Forgejo</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
|
@ -78,7 +79,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
// Authentication state from App.vue
|
||||
const isAuthenticated = ref(false); // Replace this with a global state or a store (e.g., Pinia/Vuex) if necessary
|
||||
</script>
|
||||
|
|
44
src/views/LoginView.vue
Normal file
44
src/views/LoginView.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div class="login">
|
||||
<h1>Login</h1>
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<input v-model="username" type="text" placeholder="Username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn-login">Login</button>
|
||||
</form>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p class="register-link">
|
||||
Don't have an account? <router-link to="/register">Register here</router-link>.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { login } from "@/authService"; // Matches the renamed file
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
username: "",
|
||||
password: "",
|
||||
error: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
try {
|
||||
await login(this.username, this.password);
|
||||
this.$router.push("/protected"); // Redirect to protected route
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = "Invalid username or password";
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -175,171 +175,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.links {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.links p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: #89b4fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
color: #b4befe;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.projects {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filter-bar button {
|
||||
background: #313244;
|
||||
color: #cdd6f4;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.filter-bar button:hover,
|
||||
.filter-bar button.active {
|
||||
background: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.project-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background: #313244;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.project-card h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.project-card p {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #45475a;
|
||||
color: #cdd6f4;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh; /* Limits height to 80% of the viewport */
|
||||
overflow-y: auto; /* Enables vertical scrolling if content overflows */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.faq-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.faq-list li {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.faq-list strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: #f7768e;
|
||||
color: #1e1e2e;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #f38ba8;
|
||||
}
|
||||
|
||||
.faq-button {
|
||||
background: #89b4fa;
|
||||
color: #1e1e2e;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.faq-button:hover {
|
||||
background: #74a8e0;
|
||||
}
|
||||
</style>
|
||||
|
|
253
src/views/RecipesView.vue
Normal file
253
src/views/RecipesView.vue
Normal file
|
@ -0,0 +1,253 @@
|
|||
<template>
|
||||
<div class="recipes">
|
||||
<h1>Recipes</h1>
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<label for="type">Filter by Type:</label>
|
||||
<select v-model="selectedType" @change="filterRecipes">
|
||||
<option value="">All</option>
|
||||
<option value="appetizer">Appetizers</option>
|
||||
<option value="drink">Drinks</option>
|
||||
<option value="alcoholic">Alcoholic Drinks</option>
|
||||
<option value="non-alcoholic">Non-Alcoholic Drinks</option>
|
||||
<option value="found">Found Recipes</option>
|
||||
<option value="user">User Recipes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-group">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
placeholder="Search recipes..."
|
||||
@input="filterRecipes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recipes-grid">
|
||||
<div
|
||||
v-for="recipe in filteredRecipes"
|
||||
:key="recipe.id"
|
||||
class="recipe-card"
|
||||
>
|
||||
<img v-if="recipe.image" :src="recipe.image" alt="Recipe Image" />
|
||||
<h3>{{ recipe.name }}</h3>
|
||||
<p><strong>Type:</strong> {{ recipe.type }}</p>
|
||||
<p><strong>Made By:</strong> {{ recipe.madeBy }}</p>
|
||||
<p>{{ recipe.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Recipe Modal -->
|
||||
<div v-if="showAddModal" class="modal-overlay" @click="closeModal">
|
||||
<div class="modal-content" @click.stop>
|
||||
<h2>Add Recipe</h2>
|
||||
<form @submit.prevent="addRecipe">
|
||||
<input v-model="newRecipe.name" type="text" placeholder="Recipe Name" required />
|
||||
<textarea
|
||||
v-model="newRecipe.description"
|
||||
placeholder="Recipe Description"
|
||||
required
|
||||
></textarea>
|
||||
<select v-model="newRecipe.type" required>
|
||||
<option value="">Select Type</option>
|
||||
<option value="appetizer">Appetizer</option>
|
||||
<option value="drink">Drink</option>
|
||||
<option value="alcoholic">Alcoholic Drink</option>
|
||||
<option value="non-alcoholic">Non-Alcoholic Drink</option>
|
||||
<option value="found">Found Recipe</option>
|
||||
<option value="user">User Recipe</option>
|
||||
</select>
|
||||
<input
|
||||
v-model="newRecipe.madeBy"
|
||||
type="text"
|
||||
placeholder="Made By (or Found)"
|
||||
/>
|
||||
<input type="file" @change="onFileChange" />
|
||||
<button type="submit">Add Recipe</button>
|
||||
</form>
|
||||
<button @click="closeModal" class="close-button">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Recipe Button -->
|
||||
<button v-if="canAddRecipe" @click="showAddModal = true" class="add-button">
|
||||
Add Recipe
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { getRecipes, addRecipeToDb } from "@/recipeService"; // Replace with actual service paths
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
recipes: [],
|
||||
filteredRecipes: [],
|
||||
selectedType: "",
|
||||
searchQuery: "",
|
||||
showAddModal: false,
|
||||
newRecipe: {
|
||||
name: "",
|
||||
description: "",
|
||||
type: "",
|
||||
madeBy: "",
|
||||
image: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canAddRecipe() {
|
||||
// Replace with actual permission check logic
|
||||
return localStorage.getItem("token"); // Simple check if the user is logged in
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async fetchRecipes() {
|
||||
try {
|
||||
const response = await getRecipes();
|
||||
this.recipes = response.data;
|
||||
this.filteredRecipes = this.recipes;
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch recipes:", err);
|
||||
}
|
||||
},
|
||||
filterRecipes() {
|
||||
this.filteredRecipes = this.recipes.filter((recipe) => {
|
||||
const matchesType =
|
||||
!this.selectedType || recipe.type === this.selectedType;
|
||||
const matchesSearch =
|
||||
!this.searchQuery ||
|
||||
recipe.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
||||
recipe.description
|
||||
.toLowerCase()
|
||||
.includes(this.searchQuery.toLowerCase());
|
||||
return matchesType && matchesSearch;
|
||||
});
|
||||
},
|
||||
onFileChange(event: Event) {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
this.newRecipe.image = file;
|
||||
}
|
||||
},
|
||||
async addRecipe() {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("name", this.newRecipe.name);
|
||||
formData.append("description", this.newRecipe.description);
|
||||
formData.append("type", this.newRecipe.type);
|
||||
formData.append("madeBy", this.newRecipe.madeBy || "Unknown");
|
||||
if (this.newRecipe.image) {
|
||||
formData.append("image", this.newRecipe.image);
|
||||
}
|
||||
await addRecipeToDb(formData);
|
||||
this.showAddModal = false;
|
||||
this.newRecipe = {
|
||||
name: "",
|
||||
description: "",
|
||||
type: "",
|
||||
madeBy: "",
|
||||
image: null,
|
||||
};
|
||||
await this.fetchRecipes();
|
||||
} catch (err) {
|
||||
console.error("Failed to add recipe:", err);
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.showAddModal = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchRecipes();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recipes {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.search-group input {
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.recipes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.recipe-card {
|
||||
background: #313244;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recipe-card img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1e1e2e;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: #89b4fa;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
background: #74a8e0;
|
||||
}
|
||||
</style>
|
52
src/views/RegisterView.vue
Normal file
52
src/views/RegisterView.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div class="register">
|
||||
<h1>Register</h1>
|
||||
<form @submit.prevent="handleRegister">
|
||||
<input v-model="username" type="text" placeholder="Username" required />
|
||||
<input v-model="password" type="password" placeholder="Password" required />
|
||||
<input v-model="confirmPassword" type="password" placeholder="Confirm Password" required />
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-if="success" class="success">{{ success }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { register } from "@/authService";
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
error: "",
|
||||
success: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async handleRegister() {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
this.error = "Passwords do not match";
|
||||
this.success = "";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await register(this.username, this.password);
|
||||
this.success = "Registration successful! You can now log in.";
|
||||
this.error = "";
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
this.confirmPassword = "";
|
||||
} catch (err) {
|
||||
this.error = "Registration failed. Please try again.";
|
||||
this.success = "";
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -313,90 +313,3 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.servers {
|
||||
padding: 2rem;
|
||||
}
|
||||
.server-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.server-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.server-card {
|
||||
background-color: #313244;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.server-card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.online {
|
||||
color: #a6e3a1;
|
||||
}
|
||||
.offline {
|
||||
color: #f38ba8;
|
||||
}
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
.ip-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ip {
|
||||
background: #313244;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
color: #89b4fa;
|
||||
}
|
||||
.copy-icon:hover {
|
||||
color: #74a8e0;
|
||||
}
|
||||
.close-button {
|
||||
background: #89b4fa; /* Blue tone for the primary button */
|
||||
color: #1e1e2e; /* Text color matches the modal content background */
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #74a8e0; /* Slightly lighter blue for hover effect */
|
||||
transform: scale(1.05); /* Subtle hover animation for interaction */
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background: #89b4fa; /* Restore original color for active state */
|
||||
transform: scale(0.95); /* Slight press-down effect */
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "Node",
|
||||
"types": ["vite/client"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext"
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue