More work?

This commit is contained in:
DesertMermaid 2024-11-16 14:14:49 -08:00
parent add6489e40
commit 640f81a79f
30 changed files with 5082 additions and 405 deletions

22
backend/.env Normal file
View 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

View 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" });
}
};

View 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" });
}
};

View 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" });
}
};

View 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
View 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

File diff suppressed because it is too large Load diff

25
backend/package.json Normal file
View 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
View 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
View file

59
backend/routes/recipes.js Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -13,11 +13,19 @@
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build --force", "type-check": "vue-tsc --build --force",
"lint": "eslint . --fix", "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": { "dependencies": {
"@tsparticles/slim": "^3.5.0", "@tsparticles/slim": "^3.5.0",
"@tsparticles/vue3": "^3.0.1", "@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", "pinia": "^2.2.6",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
@ -33,6 +41,7 @@
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"concurrently": "^9.1.0",
"cypress": "^13.15.1", "cypress": "^13.15.1",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-plugin-cypress": "^4.1.0", "eslint-plugin-cypress": "^4.1.0",

View file

@ -1,21 +1,95 @@
<script setup lang="ts"> <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> </script>
<template> <template>
<div id="app"> <div id="app">
<!-- Header and Navigation -->
<header> <header>
<nav> <nav class="nav-bar">
<div class="nav-links">
<RouterLink to="/">Home</RouterLink> <RouterLink to="/">Home</RouterLink>
<RouterLink to="/servers">Servers</RouterLink> <RouterLink to="/servers">Servers</RouterLink>
<RouterLink to="/projects">Projects</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://oekaki.smgames.club" target="_blank">Oekaki</a>
<a href="https://social.smgames.club/" target="_blank">Social</a> <a href="https://social.smgames.club/" target="_blank">Social</a>
<a href="https://madstar.studio" target="_blank">Shop</a> <a href="https://madstar.studio" target="_blank">Shop</a>
</div>
</nav> </nav>
</header> </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>&nbsp;&nbsp;|&nbsp;
<RouterLink to="/register">Register</RouterLink>
</span>
</div>
</section>
<!-- Main Content -->
<main> <main>
<RouterView /> <RouterView />
</main> </main>

View file

@ -1,11 +1,154 @@
@media (prefers-color-scheme: dark) { /* Catppuccin Mocha */
:root { :root[data-theme="mocha"] {
--color-surface0: #1e1e2e; --color-surface0: #1e1e2e; /* Base */
--color-surface1: #313244; --color-surface1: #313244; /* Mantle */
--color-surface2: #45475a; --color-surface2: #45475a; /* Surface */
--color-text: #cdd6f4; --color-text: #cdd6f4; /* Text */
--color-accent: #a6e3a1; --color-accent: #a6e3a1; /* Green */
--color-accent-hover: #89d58d; --color-accent-hover: #89d58d; /* Hover Green */
--color-border: #585b70; --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 */
}

View file

@ -62,6 +62,56 @@ header nav a.router-link-exact-active {
font-weight: bold; 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 Content */
main { main {
margin-top: 2rem; margin-top: 2rem;
@ -69,20 +119,20 @@ main {
/* Hero Section */ /* Hero Section */
.hero { .hero {
background: linear-gradient(135deg, #313244, #45475a); /* Catppuccin Mocha colors */ background: linear-gradient(135deg, var(--color-surface1), var(--color-surface2));
color: #ffffff; color: var(--color-text);
text-align: center; text-align: center;
padding: 1rem 0.5rem; /* Reduced padding for smaller height */ padding: 1rem 0.5rem;
} }
.hero h1 { .hero h1 {
font-size: 2rem; /* Smaller font size for the heading */ font-size: 2rem;
margin-bottom: 0.5rem; /* Reduced bottom margin */ margin-bottom: 0.5rem;
} }
.hero p { .hero p {
font-size: 1.25rem; /* Smaller font size for the paragraph */ font-size: 1.25rem;
margin: 0; /* No extra margin */ margin: 0;
} }
/* Sections */ /* Sections */
@ -91,17 +141,17 @@ main {
} }
.section-box { .section-box {
margin-bottom: 2rem; /* Reduced bottom margin for sections */ margin-bottom: 2rem;
padding: 1rem; /* Reduced padding inside the section box */ padding: 1rem;
background-color: #313244; background-color: var(--color-surface1);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
.section-box h2 { .section-box h2 {
font-size: 1.5rem; /* Slightly smaller font size for headers */ font-size: 1.5rem;
margin: 0.5rem 0 1rem; /* Reduced top margin and kept space below */ margin: 0.5rem 0 1rem;
color: #a6e3a1; color: var(--color-accent);
} }
.section-content { .section-content {
@ -110,8 +160,9 @@ main {
gap: 1.5rem; gap: 1.5rem;
} }
/* Cards */
.card { .card {
background-color: #45475a; background-color: var(--color-surface2);
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
@ -137,79 +188,525 @@ main {
} }
.link { .link {
color: #a6e3a1; color: var(--color-accent);
text-decoration: none; text-decoration: none;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
background-color: #313244; background-color: var(--color-surface1);
border-radius: 6px; border-radius: 6px;
transition: background-color 0.3s; transition: background-color 0.3s;
} }
.link:hover { .link:hover {
background-color: #89d58d; background-color: var(--color-accent-hover);
color: #1e1e2e; color: var(--color-surface0);
} }
/* Footer */ /* Footer */
.footer { .footer {
background-color: #1e1e2e; background-color: var(--color-surface2);
text-align: center; text-align: center;
padding: 1rem; padding: 1rem;
color: #cdd6f4; color: var(--color-text);
} }
.footer a { .footer a {
color: #a6e3a1; color: var(--color-accent);
text-decoration: none; text-decoration: none;
} }
.footer a:hover { .footer a:hover {
color: #89d58d; color: var(--color-accent-hover);
} }
/* Responsive Navbar */ /* Filters and Project Styles */
@media (max-width: 768px) { .filter-bar {
header nav { display: flex;
flex-direction: column; justify-content: center;
text-align: center; gap: 1rem;
margin-bottom: 2rem;
} }
header nav a { .filter-bar button {
padding: 0.5rem; 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; font-size: 1rem;
} margin-bottom: 1rem;
} }
/* Player Icon */ /* Modal Styles */
.players-icon { .modal-overlay {
color: #89b4fa; 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; margin-right: 0.5rem;
} }
/* Copy Icon */
.copy-icon { .copy-icon {
cursor: pointer; cursor: pointer;
color: #89b4fa; color: #89b4fa; /* Matches Catppuccin Mocha */
margin-left: 0.5rem;
transition: color 0.2s; transition: color 0.2s;
} }
.copy-icon:hover { .copy-icon:hover {
color: #74a8e0; color: #74a8e0; /* Matches Catppuccin Mocha */
} }
/* Status Icons */ .close-button {
.online-icon { background: var(--color-accent);
color: #a6e3a1; color: var(--color-surface0);
margin-right: 0.5rem; padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
} }
.offline-icon { .close-button:hover {
color: #f38ba8; background: var(--color-accent-hover);
margin-right: 0.5rem; transform: scale(1.05);
} }
.unknown-icon { .close-button:active {
color: #cba6f7; transform: scale(0.95);
margin-right: 0.5rem; }
/* 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
View 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}` },
});
};

View file

@ -9,13 +9,6 @@ const router = createRouter({
name: 'home', name: 'home',
component: HomeView, component: HomeView,
}, },
/* Rename name to the page name to add another page
{
path: '/name',
name: 'name',
component: () => import('../views/NameView.vue'),
},
*/
{ {
path: '/servers', path: '/servers',
name: 'servers', name: 'servers',
@ -26,6 +19,39 @@ const router = createRouter({
name: 'projects', name: 'projects',
component: () => import('../views/ProjectsView.vue'), 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'),
},
*/
], ],
}) })

View file

View file

View file

@ -14,28 +14,31 @@
<div class="section-box"> <div class="section-box">
<h2>Family Resources</h2> <h2>Family Resources</h2>
<div class="section-content"> <div class="section-content">
<!-- Sharkey Section (Always Visible) -->
<div class="card"> <div class="card">
<h3>Sharkey</h3> <h3>Sharkey</h3>
<p> <p>
Twitter alternative without a flood of Nazis.<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> 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. 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> </p>
<a href="https://social.smgames.club/" class="link">Go to Platform</a> <a href="https://social.smgames.club/" class="link">Go to Platform</a>
</div> </div>
<div class="card">
<!-- Conditional Sections (Visible only if logged in) -->
<div v-if="isAuthenticated" class="card">
<h3>Discord</h3> <h3>Discord</h3>
<p> <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. Sorry, you won't actually find the link here! If you're related to Starr, ask her about it.
</p> </p>
</div> </div>
<div class="card"> <div v-if="isAuthenticated" class="card">
<h3>Photo Gallery</h3> <h3>Photo Gallery</h3>
<p>Explore our collection of family photos, preserved for generations.</p> <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>
<div class="card"> <div v-if="isAuthenticated" class="card">
<h3>Recipe Collection</h3> <h3>Recipe Collection</h3>
<p>Browse our curated family recipes, shared across generations.</p> <p>Browse our curated family recipes, shared across generations.</p>
<a href="/recipes" class="link">Explore Recipes</a> <a href="/recipes" class="link">Explore Recipes</a>
@ -49,16 +52,12 @@
<div class="section-content"> <div class="section-content">
<div class="card"> <div class="card">
<h3>Game Servers</h3> <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> <a href="/servers" class="link">See Our Game Servers</a>
</div> </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>
</div> </div>
@ -68,7 +67,9 @@
<div class="section-content"> <div class="section-content">
<div class="card"> <div class="card">
<h3>Forgejo Repository</h3> <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> <a href="https://git.smgames.club/" class="link" target="_blank">Visit Forgejo</a>
</div> </div>
<div class="card"> <div class="card">
@ -78,7 +79,13 @@
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
</template> </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
View 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>

View file

@ -175,171 +175,3 @@ export default {
}, },
}; };
</script> </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
View 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>

View 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>

View file

@ -313,90 +313,3 @@ export default defineComponent({
}, },
}); });
</script> </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>

View file

@ -9,6 +9,12 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} },
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"moduleResolution": "Node",
"types": ["vite/client"]
} }
} }

View file

@ -12,6 +12,7 @@
} }
], ],
"compilerOptions": { "compilerOptions": {
"module": "NodeNext" "module": "NodeNext",
"moduleResolution": "NodeNext"
} }
} }