Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866e0c05b7 | ||
|
|
390ee703a3 | ||
|
|
f2fd1d30f2 | ||
|
|
e0d7dd4af9 |
67
admin/add.php
Normal file
67
admin/add.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$title = $_POST['title'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$image_url = $_POST['image_url'] ?? '';
|
||||
|
||||
if ($title) {
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$sql = "INSERT INTO projects (title, description, image_url) VALUES (:title, :description, :image_url)";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(['title' => $title, 'description' => $description, 'image_url' => $image_url]);
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Add Project</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<div class="container mt-5">
|
||||
<h1>Add New Project</h1>
|
||||
<div class="card portfolio-item">
|
||||
<div class="card-body">
|
||||
<form action="add.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="image_url" class="form-label">Image URL</label>
|
||||
<input type="text" class="form-control" id="image_url" name="image_url">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent">Add Project</button>
|
||||
<a href="index.php" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
admin/delete.php
Normal file
26
admin/delete.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if ($id) {
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$sql = "DELETE FROM projects WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(['id' => $id]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
90
admin/edit.php
Normal file
90
admin/edit.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$id = $_GET['id'] ?? null;
|
||||
if (!$id) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$project = null;
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
$project = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
|
||||
if (!$project) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$title = $_POST['title'] ?? '';
|
||||
$description = $_POST['description'] ?? '';
|
||||
$image_url = $_POST['image_url'] ?? '';
|
||||
|
||||
if ($title) {
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$sql = "UPDATE projects SET title = :title, description = :description, image_url = :image_url WHERE id = :id";
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute(['title' => $title, 'description' => $description, 'image_url' => $image_url, 'id' => $id]);
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Edit Project</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<div class="container mt-5">
|
||||
<h1>Edit Project</h1>
|
||||
<div class="card portfolio-item">
|
||||
<div class="card-body">
|
||||
<form action="edit.php?id=<?php echo $id; ?>" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="<?php echo htmlspecialchars($project['title']); ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="5"><?php echo htmlspecialchars($project['description']); ?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="image_url" class="form-label">Image URL</label>
|
||||
<input type="text" class="form-control" id="image_url" name="image_url" value="<?php echo htmlspecialchars($project['image_url']); ?>">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent">Update Project</button>
|
||||
<a href="index.php" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
77
admin/index.php
Normal file
77
admin/index.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// If not logged in, redirect to login page
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
$projects = [];
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->query("SELECT * FROM projects ORDER BY created_at DESC");
|
||||
if ($stmt) {
|
||||
$projects = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<a href="logout.php" class="btn btn-danger">Logout</a>
|
||||
</div>
|
||||
|
||||
<div class="card portfolio-item">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Projects</h2>
|
||||
<a href="add.php" class="btn btn-accent mb-3">Add New Project</a>
|
||||
<table class="table table-dark table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($projects)): ?>
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">No projects found.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($projects as $project): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($project['title']); ?></td>
|
||||
<td><?php echo htmlspecialchars(substr($project['description'], 0, 100)); ?>...</td>
|
||||
<td>
|
||||
<a href="edit.php?id=<?php echo $project['id']; ?>" class="btn btn-sm btn-primary">Edit</a>
|
||||
<a href="delete.php?id=<?php echo $project['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure?')">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
58
admin/login.php
Normal file
58
admin/login.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// If already logged in, redirect to admin dashboard
|
||||
if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$login_error = '';
|
||||
$password = 'password123'; // Hardcoded password - NOT FOR PRODUCTION
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
if (isset($_POST['password']) && $_POST['password'] == $password) {
|
||||
$_SESSION['loggedin'] = true;
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$login_error = 'Invalid password.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../assets/css/custom.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center" style="height: 100vh;">
|
||||
<div class="col-md-4">
|
||||
<div class="card portfolio-item">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-center">Admin Login</h3>
|
||||
<form action="login.php" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<?php if ($login_error): ?>
|
||||
<div class="alert alert-danger"><?php echo $login_error; ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-accent">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
6
admin/logout.php
Normal file
6
admin/logout.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
252
assets/css/custom.css
Normal file
252
assets/css/custom.css
Normal file
@ -0,0 +1,252 @@
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--surface-color: #1E1E1E;
|
||||
--primary-text-color: #EAEAEA;
|
||||
--secondary-text-color: #B3B3B3;
|
||||
--accent-color: #3E8BFF;
|
||||
--accent-color-hover: #5A9BFF;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body.dark-theme {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--primary-text-color);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
/* Particles.js container */
|
||||
#particles-js {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1; /* Place it behind all content */
|
||||
}
|
||||
|
||||
/* --- Header & Nav --- */
|
||||
header.sticky-top .navbar {
|
||||
background-color: rgba(30, 30, 30, 0.5); /* More transparency */
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1); /* Subtle border */
|
||||
}
|
||||
|
||||
.navbar-brand, .nav-link {
|
||||
color: var(--primary-text-color) !important;
|
||||
font-weight: 400;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover, .navbar-brand:hover {
|
||||
color: var(--accent-color) !important;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(234, 234, 234, 0.8)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
|
||||
/* --- Hero Section --- */
|
||||
.hero {
|
||||
padding: 8rem 0;
|
||||
background: linear-gradient(135deg, var(--bg-color) 0%, #1a1a1a 100%);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hero p.lead {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* --- General Section --- */
|
||||
.section {
|
||||
padding: 5rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 3rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* --- Buttons --- */
|
||||
.btn-accent {
|
||||
background-color: var(--accent-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
background-color: var(--accent-color-hover);
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-outline-accent {
|
||||
color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
font-weight: 400;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-outline-accent:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
/* --- About Section --- */
|
||||
.profile-pic {
|
||||
border: 5px solid var(--surface-color);
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* --- Portfolio Section --- */
|
||||
.portfolio-item.card {
|
||||
background-color: rgba(30, 30, 30, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.portfolio-item.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.card-text {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
background-color: transparent;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- Contact Form --- */
|
||||
.form-control {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--primary-text-color);
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: var(--surface-color);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--primary-text-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(62, 139, 255, 0.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.form-control::placeholder {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* --- Footer --- */
|
||||
footer {
|
||||
background-color: var(--surface-color);
|
||||
color: var(--secondary-text-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- AI Chat --- */
|
||||
.ai-response {
|
||||
background-color: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.ai-response pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
color: var(--primary-text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tools Page */
|
||||
.tool-card {
|
||||
background-color: rgba(30, 30, 30, 0.5);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
color: var(--primary-text-color);
|
||||
margin-bottom: 2rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tool-card h2, .tool-card h3, .tool-card h4, .tool-card h5 {
|
||||
color: var(--primary-text-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-card p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.tool-card .form-label,
|
||||
.tool-card label {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.tool-card .form-control {
|
||||
background-color: var(--surface-color);
|
||||
border-color: var(--border-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.tool-card .form-control:focus {
|
||||
background-color: #2a2a2a;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-prompt,
|
||||
.ai-response {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-top: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.user-prompt strong,
|
||||
.ai-response strong {
|
||||
color: var(--accent-color);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
47
assets/js/main.js
Normal file
47
assets/js/main.js
Normal file
@ -0,0 +1,47 @@
|
||||
// Bootstrap form validation
|
||||
(() => {
|
||||
'use strict'
|
||||
|
||||
const forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.from(forms).forEach(form => {
|
||||
form.addEventListener('submit', event => {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})();
|
||||
|
||||
// Show toast notification for form submission status
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const status = urlParams.get('status');
|
||||
const message = urlParams.get('message');
|
||||
|
||||
if (status) {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
const toastHTML = `
|
||||
<div class="toast align-items-center text-white ${status === 'success' ? 'bg-success' : 'bg-danger'} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message || (status === 'success' ? 'Your message has been sent successfully!' : 'An error occurred. Please try again.')}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
toastContainer.innerHTML = toastHTML;
|
||||
const toastEl = toastContainer.querySelector('.toast');
|
||||
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
|
||||
toast.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Particles.js
|
||||
particlesJS.load('particles-js', 'assets/particles.json', function() {
|
||||
console.log('particles.js config loaded');
|
||||
});
|
||||
184
assets/js/simulator.js
Normal file
184
assets/js/simulator.js
Normal file
@ -0,0 +1,184 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('simulationCanvas');
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
const launchButton = document.getElementById('launchButton');
|
||||
const accelerationInput = document.getElementById('acceleration');
|
||||
const burnTimeInput = document.getElementById('burnTime');
|
||||
const angleInput = document.getElementById('angle');
|
||||
const resultMessage = document.getElementById('result-message');
|
||||
|
||||
// --- Параметры симуляции ---
|
||||
const GRAVITY = 9.8;
|
||||
const PIXELS_PER_METER = 0.01;
|
||||
const TIME_STEP = 1.0;
|
||||
|
||||
let rocket = {};
|
||||
let simulationState = {};
|
||||
|
||||
function initializeState() {
|
||||
return {
|
||||
animationFrameId: null,
|
||||
elapsedTime: 0,
|
||||
acceleration: 0,
|
||||
burnTime: 0,
|
||||
angleRad: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function initializeRocket() {
|
||||
return {
|
||||
x: 30,
|
||||
y: canvas.height - 45,
|
||||
width: 10,
|
||||
height: 25,
|
||||
x_meters: 0,
|
||||
y_meters: 0,
|
||||
vx: 0, // Скорость по X в м/с
|
||||
vy: 0, // Скорость по Y в м/с
|
||||
};
|
||||
}
|
||||
|
||||
function drawEarth() {
|
||||
ctx.fillStyle = '#2c5c23';
|
||||
ctx.beginPath();
|
||||
ctx.arc(canvas.width / 2, canvas.height + canvas.height - 50, canvas.height, 0, Math.PI, true);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
function drawRocket() {
|
||||
ctx.save();
|
||||
ctx.translate(rocket.x, rocket.y);
|
||||
|
||||
// Угол поворота ракеты зависит от вектора скорости
|
||||
// Но до старта (скорость 0) она стоит вертикально
|
||||
const angle = (rocket.vx === 0 && rocket.vy === 0)
|
||||
? -Math.PI / 2
|
||||
: Math.atan2(rocket.vy, rocket.vx);
|
||||
|
||||
// Поворачиваем ракету. Наша модель "смотрит" вправо, поэтому доп. вращение не нужно.
|
||||
ctx.rotate(angle);
|
||||
|
||||
// Рисуем пламя, если двигатель работает
|
||||
if (simulationState.elapsedTime > 0 && simulationState.elapsedTime <= simulationState.burnTime) {
|
||||
ctx.fillStyle = `rgba(255, ${Math.random() * 150 + 100}, 0, 0.8)`;
|
||||
ctx.beginPath();
|
||||
const flameLength = rocket.height * (1.5 + Math.random() * 0.5);
|
||||
ctx.moveTo(-rocket.width / 2, 0);
|
||||
ctx.lineTo(-rocket.width / 2 - flameLength, rocket.width / 2);
|
||||
ctx.lineTo(-rocket.width / 2 - flameLength, -rocket.width / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Корпус ракеты (перерисовываем, чтобы был поверх пламени)
|
||||
// Модель ракеты теперь "смотрит" вправо (по оси X)
|
||||
ctx.fillStyle = '#d0d0d0';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(rocket.height / 2, 0);
|
||||
ctx.lineTo(-rocket.height / 2, -rocket.width / 2);
|
||||
ctx.lineTo(-rocket.height / 2, rocket.width / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function resetSimulation() {
|
||||
if (simulationState.animationFrameId) {
|
||||
cancelAnimationFrame(simulationState.animationFrameId);
|
||||
}
|
||||
simulationState = initializeState();
|
||||
rocket = initializeRocket();
|
||||
resultMessage.innerHTML = ' ';
|
||||
resultMessage.className = 'alert alert-secondary';
|
||||
clearCanvas();
|
||||
drawEarth();
|
||||
drawRocket();
|
||||
}
|
||||
|
||||
function clearCanvas() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
simulationState.elapsedTime += TIME_STEP;
|
||||
|
||||
// 1. Обновляем физику
|
||||
// Если двигатель работает, применяем ускорение
|
||||
if (simulationState.elapsedTime <= simulationState.burnTime) {
|
||||
const currentAccelerationX = simulationState.acceleration * Math.cos(simulationState.angleRad);
|
||||
const currentAccelerationY = simulationState.acceleration * Math.sin(simulationState.angleRad);
|
||||
rocket.vx += currentAccelerationX * TIME_STEP;
|
||||
rocket.vy += currentAccelerationY * TIME_STEP;
|
||||
}
|
||||
|
||||
// Всегда применяем гравитацию (она действует на ось Y)
|
||||
rocket.vy -= GRAVITY * TIME_STEP;
|
||||
|
||||
// Обновляем позицию в метрах
|
||||
rocket.x_meters += rocket.vx * TIME_STEP;
|
||||
rocket.y_meters += rocket.vy * TIME_STEP;
|
||||
|
||||
// 2. Обновляем позицию для отрисовки в пикселях
|
||||
const groundY = canvas.height - 45;
|
||||
rocket.x = 30 + (rocket.x_meters * PIXELS_PER_METER);
|
||||
rocket.y = groundY - (rocket.y_meters * PIXELS_PER_METER);
|
||||
|
||||
// 3. Отрисовываем сцену
|
||||
clearCanvas();
|
||||
drawEarth();
|
||||
drawRocket();
|
||||
|
||||
// 4. Проверяем условия завершения
|
||||
const hasCrashed = rocket.y_meters < 0 && simulationState.elapsedTime > 1;
|
||||
const outOfBounds = rocket.x > canvas.width + rocket.height;
|
||||
|
||||
if (hasCrashed || outOfBounds) {
|
||||
rocket.y_meters = 0;
|
||||
rocket.y = groundY;
|
||||
clearCanvas();
|
||||
drawEarth();
|
||||
drawRocket();
|
||||
|
||||
resultMessage.innerHTML = outOfBounds
|
||||
? "<strong>Неудача.</strong> Ракета улетела за пределы видимости."
|
||||
: "<strong>Неудача.</strong> Ракета упала обратно на Землю.";
|
||||
resultMessage.className = 'alert alert-danger';
|
||||
|
||||
cancelAnimationFrame(simulationState.animationFrameId);
|
||||
simulationState.animationFrameId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
simulationState.animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
launchButton.addEventListener('click', () => {
|
||||
const accelValue = parseFloat(accelerationInput.value);
|
||||
const burnTimeValue = parseFloat(burnTimeInput.value);
|
||||
const angleValue = parseFloat(angleInput.value);
|
||||
|
||||
if (isNaN(accelValue) || isNaN(burnTimeValue) || isNaN(angleValue) || accelValue <= 0 || burnTimeValue <= 0 || angleValue < 0 || angleValue > 90) {
|
||||
resultMessage.innerHTML = 'Пожалуйста, введите корректные значения (ускорение > 0, время > 0, угол 0-90).';
|
||||
resultMessage.className = 'alert alert-warning';
|
||||
return;
|
||||
}
|
||||
|
||||
resetSimulation();
|
||||
|
||||
simulationState.acceleration = accelValue;
|
||||
simulationState.burnTime = burnTimeValue;
|
||||
simulationState.angleRad = angleValue * (Math.PI / 180);
|
||||
|
||||
resultMessage.innerHTML = "Запуск...";
|
||||
resultMessage.className = 'alert alert-info';
|
||||
|
||||
if (!simulationState.animationFrameId) {
|
||||
simulationState.animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
});
|
||||
|
||||
resetSimulation();
|
||||
});
|
||||
110
assets/particles.json
Normal file
110
assets/particles.json
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 80,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"value": "#ffffff"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
"stroke": {
|
||||
"width": 0,
|
||||
"color": "#000000"
|
||||
},
|
||||
"polygon": {
|
||||
"nb_sides": 5
|
||||
},
|
||||
"image": {
|
||||
"src": "img/github.svg",
|
||||
"width": 100,
|
||||
"height": 100
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 1,
|
||||
"opacity_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 3,
|
||||
"random": true,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 40,
|
||||
"size_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 6,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"bounce": false,
|
||||
"attract": {
|
||||
"enable": false,
|
||||
"rotateX": 600,
|
||||
"rotateY": 1200
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": true,
|
||||
"mode": "repulse"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": true,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 400,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"bubble": {
|
||||
"distance": 400,
|
||||
"size": 40,
|
||||
"duration": 2,
|
||||
"opacity": 8,
|
||||
"speed": 3
|
||||
},
|
||||
"repulse": {
|
||||
"distance": 200,
|
||||
"duration": 0.4
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
},
|
||||
"remove": {
|
||||
"particles_nb": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true
|
||||
}
|
||||
38
contact.php
Normal file
38
contact.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// Basic security: check if it's a POST request
|
||||
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
||||
header("Location: index.php?status=error&message=Invalid request method.");
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/mail/MailService.php';
|
||||
|
||||
// Sanitize and validate inputs
|
||||
$name = filter_var(trim($_POST['name'] ?? ''), FILTER_SANITIZE_STRING);
|
||||
$email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL);
|
||||
$message = filter_var(trim($_POST['message'] ?? ''), FILTER_SANITIZE_STRING);
|
||||
|
||||
if (empty($name) || !filter_var($email, FILTER_VALIDATE_EMAIL) || empty($message)) {
|
||||
header("Location: index.php?status=error&message=Please fill out all fields correctly.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Use MailService to send the email
|
||||
// The recipient is determined by the MAIL_TO environment variable in .env
|
||||
// The user's email is set as the Reply-To address.
|
||||
$res = MailService::sendContactMessage(
|
||||
$name,
|
||||
$email,
|
||||
$message,
|
||||
null, // Use default recipient from config
|
||||
'New Contact Form Submission'
|
||||
);
|
||||
|
||||
if (!empty($res['success'])) {
|
||||
header("Location: index.php?status=success&message=Your message has been sent successfully!");
|
||||
exit;
|
||||
} else {
|
||||
// In a real app, you would log the detailed error: error_log('MailService Error: ' . $res['error']);
|
||||
header("Location: index.php?status=error&message=Sorry, there was an error sending your message.");
|
||||
exit;
|
||||
}
|
||||
7
db/migrations/001_create_projects_table.sql
Normal file
7
db/migrations/001_create_projects_table.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
image_url VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
328
index.php
328
index.php
@ -1,150 +1,186 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO and Meta Tags -->
|
||||
<title>Имя Фамилия | Личное Портфолио</title>
|
||||
<meta name="description" content="Личный сайт-портфолио для демонстрации работ и получения контактных запросов.">
|
||||
<meta name="keywords" content="личное портфолио, веб-разработчик, дизайнер, форма обратной связи, проекты, резюме, flatlogic, php, lamp">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Имя Фамилия | Личное Портфолио">
|
||||
<meta property="og:description" content="Личный сайт-портфолио для демонстрации работ и получения контактных запросов.">
|
||||
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Имя Фамилия | Личное Портфолио">
|
||||
<meta name="twitter:description" content="Личный сайт-портфолио для демонстрации работ и получения контактных запросов.">
|
||||
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
<body class="dark-theme">
|
||||
|
||||
<!-- Particles.js container -->
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<!-- Toast container for notifications -->
|
||||
<div id="toast-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1100"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky-top">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="#">Имя Фамилия</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="#about">Обо мне</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="projects.php">Портфолио</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#contact">Контакт</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin">Админка</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="tools.php">Инструменты</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="simulator.php">Симулятор</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<section class="hero text-center">
|
||||
<div class="container">
|
||||
<h1 class="display-4">Креативный разработчик и дизайнер</h1>
|
||||
<p class="lead">Я создаю красивые и функциональные сайты. Добро пожаловать в мое личное пространство в сети.</p>
|
||||
<a href="#contact" class="btn btn-accent btn-lg mt-3">Связаться</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About Section -->
|
||||
<section id="about" class="section">
|
||||
<div class="container">
|
||||
<h2 class="text-center section-title">Обо мне</h2>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-4 text-center">
|
||||
<img src="https://i.pravatar.cc/300?u=a042581f4e29026704d" class="img-fluid rounded-circle mb-4 mb-md-0 profile-pic" alt="Портрет">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<p>Привет! Я увлеченный разработчик со склонностью к созданию элегантных решений в кратчайшие сроки. Я люблю чистый код, хороший дизайн и чашку кофе. Мой опыт охватывает различные технологии, что позволяет мне решать разнообразные задачи.</p>
|
||||
<p>Этот сайт — демонстрация моего пути и навыков. Он построен на классическом стеке LAMP, демонстрируя мощность и гибкость этой надежной технологии. Не стесняйтесь осмотреться и написать мне, если хотите сотрудничать!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$projects = [];
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->query("SELECT * FROM projects ORDER BY created_at DESC");
|
||||
if ($stmt) {
|
||||
$projects = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// Log error or handle it gracefully
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!-- Portfolio Section -->
|
||||
<section id="portfolio" class="section">
|
||||
<div class="container">
|
||||
<h2 class="text-center section-title">Мои работы</h2>
|
||||
<div class="row g-4">
|
||||
<?php if (empty($projects)): ?>
|
||||
<div class="col">
|
||||
<p class="text-center">Пока нет проектов для отображения. Зайдите позже!</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($projects as $project): ?>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card portfolio-item">
|
||||
<img src="<?php echo htmlspecialchars($project['image_url'] ?: 'https://picsum.photos/seed/'.htmlspecialchars($project['id']).'/400/300'); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($project['title']); ?>">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><?php echo htmlspecialchars($project['title']); ?></h5>
|
||||
<p class="card-text"><?php echo htmlspecialchars($project['description']); ?></p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="project.php?id=<?php echo $project['id']; ?>" class="btn btn-sm btn-outline-accent">Подробнее</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section id="contact" class="section">
|
||||
<div class="container">
|
||||
<h2 class="text-center section-title">Свяжитесь со мной</h2>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<form action="contact.php" method="POST" class="needs-validation" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Имя</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
<div class="invalid-feedback">Пожалуйста, введите ваше имя.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
<div class="invalid-feedback">Пожалуйста, введите корректный email.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Сообщение</label>
|
||||
<textarea class="form-control" id="message" name="message" rows="5" required></textarea>
|
||||
<div class="invalid-feedback">Пожалуйста, введите ваше сообщение.</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-accent btn-lg">Отправить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center p-4">
|
||||
<div class="container">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Имя Фамилия. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Particles.js -->
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
259
invoice.php
Normal file
259
invoice.php
Normal file
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
// Sanitize POST data
|
||||
$from_name = htmlspecialchars($_POST['from_name'] ?? 'N/A');
|
||||
$from_address = nl2br(htmlspecialchars($_POST['from_address'] ?? 'N/A'));
|
||||
$to_name = htmlspecialchars($_POST['to_name'] ?? 'N/A');
|
||||
$to_address = nl2br(htmlspecialchars($_POST['to_address'] ?? 'N/A'));
|
||||
|
||||
$invoice_number = htmlspecialchars($_POST['invoice_number'] ?? 'N/A');
|
||||
$invoice_date = htmlspecialchars($_POST['invoice_date'] ?? date('Y-m-d'));
|
||||
$invoice_due_date = htmlspecialchars($_POST['invoice_due_date'] ?? 'N/A');
|
||||
|
||||
$items = $_POST['items'] ?? [];
|
||||
$notes = nl2br(htmlspecialchars($_POST['notes'] ?? ''));
|
||||
$currency = htmlspecialchars($_POST['currency'] ?? '$');
|
||||
$tax_rate = floatval($_POST['tax_rate'] ?? 0);
|
||||
|
||||
// Server-side calculation
|
||||
$subtotal = 0;
|
||||
foreach ($items as $item) {
|
||||
$qty = floatval($item['qty'] ?? 0);
|
||||
$price = floatval($item['price'] ?? 0);
|
||||
$subtotal += $qty * $price;
|
||||
}
|
||||
$tax_amount = $subtotal * ($tax_rate / 100);
|
||||
$total = $subtotal + $tax_amount;
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Инвойс <?= $invoice_number ?></title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f9f9f9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.invoice-container {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 40px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.05);
|
||||
}
|
||||
.invoice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
.invoice-header .from-details,
|
||||
.invoice-header .invoice-meta {
|
||||
width: 48%;
|
||||
}
|
||||
.invoice-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5em;
|
||||
color: #000;
|
||||
}
|
||||
.invoice-meta table {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
.invoice-meta th, .invoice-meta td {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.invoice-meta th {
|
||||
color: #777;
|
||||
font-weight: normal;
|
||||
}
|
||||
.client-details {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.client-details h5 {
|
||||
font-size: 1.1em;
|
||||
color: #777;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.items-table th, .items-table td {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
.items-table th {
|
||||
background-color: #f9f9f9;
|
||||
font-weight: 600;
|
||||
}
|
||||
.items-table .text-right {
|
||||
text-align: right;
|
||||
}
|
||||
.totals {
|
||||
float: right;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
.totals table {
|
||||
width: 100%;
|
||||
}
|
||||
.totals th, .totals td {
|
||||
padding: 8px;
|
||||
}
|
||||
.totals th {
|
||||
text-align: left;
|
||||
font-weight: normal;
|
||||
color: #555;
|
||||
}
|
||||
.totals td {
|
||||
text-align: right;
|
||||
}
|
||||
.totals .grand-total th, .totals .grand-total td {
|
||||
font-size: 1.4em;
|
||||
font-weight: bold;
|
||||
padding-top: 15px;
|
||||
border-top: 2px solid #333;
|
||||
}
|
||||
.invoice-notes {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.invoice-notes h5 {
|
||||
margin-bottom: 5px;
|
||||
color: #777;
|
||||
}
|
||||
.print-button-container {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.print-button {
|
||||
padding: 12px 25px;
|
||||
font-size: 1em;
|
||||
background-color: #0d6efd;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.print-button:hover {
|
||||
background-color: #0b5ed7;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
.invoice-container {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.print-button-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="invoice-container">
|
||||
<div class="invoice-header">
|
||||
<div class="from-details">
|
||||
<h1>Инвойс</h1>
|
||||
<strong><?= $from_name ?></strong><br>
|
||||
<?= $from_address ?>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Номер #</th>
|
||||
<td><?= $invoice_number ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Дата</th>
|
||||
<td><?= $invoice_date ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Срок оплаты</th>
|
||||
<td><?= $invoice_due_date ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="client-details">
|
||||
<h5>Кому:</h5>
|
||||
<strong><?= $to_name ?></strong><br>
|
||||
<?= $to_address ?>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Описание</th>
|
||||
<th class="text-right">Кол-во</th>
|
||||
<th class="text-right">Цена</th>
|
||||
<th class="text-right">Сумма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($item['description'] ?? '') ?></td>
|
||||
<td class="text-right"><?= htmlspecialchars($item['qty'] ?? 0) ?></td>
|
||||
<td class="text-right"><?= $currency ?> <?= number_format(floatval($item['price'] ?? 0), 2) ?></td>
|
||||
<td class="text-right"><?= $currency ?> <?= number_format(floatval($item['qty'] ?? 0) * floatval($item['price'] ?? 0), 2) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="totals">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Подытог:</th>
|
||||
<td><?= $currency ?> <?= number_format($subtotal, 2) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Налог (<?= $tax_rate ?>%):</th>
|
||||
<td><?= $currency ?> <?= number_format($tax_amount, 2) ?></td>
|
||||
</tr>
|
||||
<tr class="grand-total">
|
||||
<th>Итого:</th>
|
||||
<td><?= $currency ?> <?= number_format($total, 2) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
|
||||
<?php if (!empty($notes)): ?>
|
||||
<div class="invoice-notes">
|
||||
<h5>Примечания:</h5>
|
||||
<p><?= $notes ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="print-button-container">
|
||||
<button class="print-button" onclick="window.print()">Печать / Сохранить в PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
120
project.php
Normal file
120
project.php
Normal file
@ -0,0 +1,120 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$project = null;
|
||||
$project_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
|
||||
|
||||
if ($project_id) {
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE id = ?");
|
||||
$stmt->execute([$project_id]);
|
||||
$project = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$page_title = $project ? htmlspecialchars($project['title']) . ' | Проект' : 'Проект не найден';
|
||||
$page_description = $project ? htmlspecialchars($project['description']) : 'Запрошенный проект не найден.';
|
||||
?>
|
||||
|
||||
<title><?php echo $page_title; ?></title>
|
||||
<meta name="description" content="<?php echo $page_description; ?>">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:title" content="<?php echo $page_title; ?>">
|
||||
<meta property="og:description" content="<?php echo $page_description; ?>">
|
||||
<meta property="og:image" content="<?php echo $project ? htmlspecialchars($project['image_url']) : htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:title" content="<?php echo $page_title; ?>">
|
||||
<meta name="twitter:description" content="<?php echo $page_description; ?>">
|
||||
<meta name="twitter:image" content="<?php echo $project ? htmlspecialchars($project['image_url']) : htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
|
||||
<!-- Particles.js container -->
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky-top">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="index.php">Имя Фамилия</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#about">Обо мне</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="projects.php">Портфолио</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#contact">Контакт</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin">Админка</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="tools.php">Инструменты</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="simulator.php">Симулятор</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10">
|
||||
<?php if ($project): ?>
|
||||
<div class="project-details">
|
||||
<h1 class="project-title text-center mb-4"><?php echo htmlspecialchars($project['title']); ?></h1>
|
||||
<img src="<?php echo htmlspecialchars($project['image_url'] ?: 'https://picsum.photos/seed/'.htmlspecialchars($project['id']).'/1200/800'); ?>" class="img-fluid rounded-3 mb-4 project-image" alt="<?php echo htmlspecialchars($project['title']); ?>">
|
||||
<div class="project-description">
|
||||
<p><?php echo nl2br(htmlspecialchars($project['description'])); ?></p>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="projects.php" class="btn btn-outline-accent">Назад к проектам</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-danger text-center" role="alert">
|
||||
<h4 class="alert-heading">Ошибка 404</h4>
|
||||
<p>К сожалению, проект с таким ID не найден.</p>
|
||||
<hr>
|
||||
<a href="projects.php" class="btn btn-primary">Вернуться к проектам</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center p-4 mt-auto">
|
||||
<div class="container">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Имя Фамилия. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Particles.js -->
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
118
projects.php
Normal file
118
projects.php
Normal file
@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Проекты | Личное Портфолио</title>
|
||||
<meta name="description" content="Галерея проектов в личном портфолио.">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:title" content="Проекты | Личное Портфолио">
|
||||
<meta property="og:description" content="Галерея проектов в личном портфолио.">
|
||||
<meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:title" content="Проекты | Личное Портфолио">
|
||||
<meta name="twitter:description" content="Галерея проектов в личном портфолио.">
|
||||
<meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
|
||||
<!-- Particles.js container -->
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky-top">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="index.php">Имя Фамилия</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#about">Обо мне</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="projects.php">Портфолио</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#contact">Контакт</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin">Админка</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="tools.php">Инструменты</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="simulator.php">Симулятор</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container py-5">
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
$projects = [];
|
||||
try {
|
||||
$pdo = db();
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->query("SELECT * FROM projects ORDER BY created_at DESC");
|
||||
if ($stmt) {
|
||||
$projects = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log("Database error: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
<!-- Portfolio Section -->
|
||||
<section id="portfolio" class="section">
|
||||
<div class="container">
|
||||
<h2 class="text-center section-title mb-5">Мои работы</h2>
|
||||
<div class="row g-4">
|
||||
<?php if (empty($projects)): ?>
|
||||
<div class="col">
|
||||
<p class="text-center">Пока нет проектов для отображения. Зайдите позже!</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($projects as $project): ?>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card portfolio-item h-100">
|
||||
<img src="<?php echo htmlspecialchars($project['image_url'] ?: 'https://picsum.photos/seed/'.htmlspecialchars($project['id']).'/400/300'); ?>" class="card-img-top" alt="<?php echo htmlspecialchars($project['title']); ?>">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title"><?php echo htmlspecialchars($project['title']); ?></h5>
|
||||
<p class="card-text flex-grow-1"><?php echo htmlspecialchars($project['description']); ?></p>
|
||||
<a href="project.php?id=<?php echo $project['id']; ?>" class="btn btn-sm btn-outline-accent mt-auto">Подробнее</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center p-4 mt-auto">
|
||||
<div class="container">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Имя Фамилия. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Particles.js -->
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
118
simulator.php
Normal file
118
simulator.php
Normal file
@ -0,0 +1,118 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- SEO and Meta Tags -->
|
||||
<title>Симулятор | Личное Портфолио</title>
|
||||
<meta name="description" content="Страница симулятора.">
|
||||
|
||||
<!-- Google Fonts: Poppins -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
|
||||
<!-- Particles.js container -->
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<header class="sticky-top">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="index.php">Имя Фамилия</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#about">Обо мне</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="projects.php">Портфолио</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="index.php#contact">Контакт</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin">Админка</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="tools.php">Инструменты</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="simulator.php">Симулятор</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h1 class="text-center section-title">Симулятор Запуска Ракеты</h1>
|
||||
<p class="text-center mb-4">Задайте параметры запуска: ускорение, время работы двигателя и угол.<br>Наблюдайте, как ракета набирает скорость и летит по траектории.</p>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card bg-dark-2 text-white">
|
||||
<div class="card-body">
|
||||
<!-- Форма теперь без перезагрузки страницы -->
|
||||
<form onsubmit="return false;">
|
||||
<div class="mb-3">
|
||||
<label for="acceleration" class="form-label">Ускорение (м/с²):</label>
|
||||
<input type="number" class="form-control" id="acceleration" name="acceleration" placeholder="Например, 100" value="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="burnTime" class="form-label">Время работы двигателя (сек):</label>
|
||||
<input type="number" class="form-control" id="burnTime" name="burnTime" placeholder="Например, 60" value="60" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="angle" class="form-label">Угол запуска (°):</label>
|
||||
<input type="number" class="form-control" id="angle" name="angle" placeholder="от 0 до 90" value="90" min="0" max="90" required>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<!-- Кнопка теперь запускает JS-функцию -->
|
||||
<button type="button" id="launchButton" class="btn btn-primary btn-lg">Запуск</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Область для анимации -->
|
||||
<div class="row justify-content-center mt-4">
|
||||
<div class="col-md-10 col-lg-8">
|
||||
<div class="card bg-dark-2 text-white">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-3">Траектория полёта</h5>
|
||||
<canvas id="simulationCanvas" width="600" height="400" style="background-color: #0c1021; border-radius: 5px;"></canvas>
|
||||
<div id="result-message" class="mt-3"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center p-4">
|
||||
<div class="container">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Имя Фамилия. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Particles.js -->
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
<script src="assets/js/simulator.js?v=<?php echo time(); ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
307
tools.php
Normal file
307
tools.php
Normal file
@ -0,0 +1,307 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/ai/LocalAIApi.php';
|
||||
|
||||
$ai_response = '';
|
||||
$user_prompt = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['user_prompt'])) {
|
||||
$user_prompt = htmlspecialchars($_POST['user_prompt']);
|
||||
|
||||
if (!empty($user_prompt)) {
|
||||
$response = LocalAIApi::createResponse([
|
||||
'input' => [
|
||||
['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
||||
['role' => 'user', 'content' => $user_prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
if (!empty($response['success'])) {
|
||||
$ai_response = LocalAIApi::extractText($response);
|
||||
} else {
|
||||
$ai_response = 'Ошибка: Не удалось получить ответ от AI.';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Инструменты - Ваше Имя</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
</head>
|
||||
<body class="dark-theme">
|
||||
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="index.php">Имя Фамилия</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="index.php#about">Обо мне</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="projects.php">Портфолио</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="index.php#contact">Контакт</a>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/admin">Админка</a></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="tools.php">Инструменты</a>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="simulator.php">Симулятор</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container mt-5 pt-5">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4">Инструменты</h1>
|
||||
<p class="lead">Полезные инструменты для автоматизации ваших задач.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card tool-card">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">AI-чат</h3>
|
||||
<p>Задайте любой вопрос и получите ответ от искусственного интеллекта.</p>
|
||||
<form action="tools.php" method="post">
|
||||
<div class="form-group mb-3">
|
||||
<label for="user_prompt">Ваш вопрос:</label>
|
||||
<input type="text" id="user_prompt" name="user_prompt" class="form-control" required value="<?php echo $user_prompt; ?>">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Отправить</button>
|
||||
</form>
|
||||
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($user_prompt)): ?>
|
||||
<div class="mt-4">
|
||||
<div class="user-prompt">
|
||||
<strong>Вы:</strong>
|
||||
<p><?php echo $user_prompt; ?></p>
|
||||
</div>
|
||||
<div class="ai-response">
|
||||
<strong>AI:</strong>
|
||||
<p><?php echo nl2br($ai_response); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card tool-card">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Генератор инвойсов</h2>
|
||||
<p>Создайте профессиональный инвойс и сохраните его в PDF.</p>
|
||||
<form action="invoice.php" method="POST" target="_blank">
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">От кого</h5>
|
||||
<div class="mb-2">
|
||||
<input type="text" class="form-control" name="from_name" placeholder="Ваше имя / Название компании" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<textarea class="form-control" name="from_address" placeholder="Ваш адрес" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5 class="mb-3">Кому</h5>
|
||||
<div class="mb-2">
|
||||
<input type="text" class="form-control" name="to_name" placeholder="Имя клиента / Название компании" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<textarea class="form-control" name="to_address" placeholder="Адрес клиента" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<label for="invoice_number" class="form-label">Номер инвойса</label>
|
||||
<input type="text" class="form-control" id="invoice_number" name="invoice_number" value="INV-001">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="invoice_date" class="form-label">Дата</label>
|
||||
<input type="date" class="form-control" id="invoice_date" name="invoice_date">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="invoice_due_date" class="form-label">Срок оплаты</label>
|
||||
<input type="date" class="form-control" id="invoice_due_date" name="invoice_due_date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered" id="invoice-items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Описание</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Цена за ед.</th>
|
||||
<th>Сумма</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" name="items[0][description]" placeholder="Название услуги или товара" required></td>
|
||||
<td><input type="number" class="form-control item-qty" name="items[0][qty]" value="1" min="1" required></td>
|
||||
<td><input type="number" class="form-control item-price" name="items[0][price]" value="0.00" step="0.01" min="0" required></td>
|
||||
<td><input type="text" class="form-control item-total" readonly></td>
|
||||
<td><button type="button" class="btn btn-danger btn-sm remove-item">×</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button type="button" class="btn btn-secondary mb-3" id="add-item">Добавить позицию</button>
|
||||
|
||||
<div class="row justify-content-end">
|
||||
<div class="col-md-5">
|
||||
<div class="row mb-2">
|
||||
<div class="col">Валюта:</div>
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control" name="currency" value="$">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col">Подытог:</div>
|
||||
<div class="col-auto" id="subtotal">0.00</div>
|
||||
</div>
|
||||
<div class="row mb-2 align-items-center">
|
||||
<div class="col">Налог (%):</div>
|
||||
<div class="col-auto">
|
||||
<input type="number" class="form-control" id="tax-rate" name="tax_rate" value="0" min="0" step="1">
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row fw-bold fs-5">
|
||||
<div class="col">Итого:</div>
|
||||
<div class="col-auto" id="total">0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="notes" class="form-label">Примечания</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3" placeholder="Например: Спасибо за ваш бизнес!"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Сгенерировать инвойс</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="text-center p-4">
|
||||
<div class="container">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Имя Фамилия. Все права защищены.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
<script>
|
||||
particlesJS.load('particles-js', 'assets/particles.json', function() {
|
||||
console.log('callback - particles.js config loaded');
|
||||
});
|
||||
</script>
|
||||
<script src="assets/js/main.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Set default dates
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const invoiceDate = document.getElementById('invoice_date');
|
||||
const dueDate = document.getElementById('invoice_due_date');
|
||||
if (invoiceDate) {
|
||||
invoiceDate.value = today;
|
||||
}
|
||||
if (dueDate) {
|
||||
// Set due date to 30 days from now
|
||||
const thirtyDaysFromNow = new Date();
|
||||
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
|
||||
dueDate.value = thirtyDaysFromNow.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
const itemsTable = document.getElementById('invoice-items');
|
||||
if (!itemsTable) return;
|
||||
|
||||
let itemIndex = 1;
|
||||
|
||||
// Function to update totals
|
||||
function updateTotals() {
|
||||
let subtotal = 0;
|
||||
document.querySelectorAll('#invoice-items tbody tr').forEach(row => {
|
||||
const qty = parseFloat(row.querySelector('.item-qty').value) || 0;
|
||||
const price = parseFloat(row.querySelector('.item-price').value) || 0;
|
||||
const total = qty * price;
|
||||
row.querySelector('.item-total').value = total.toFixed(2);
|
||||
subtotal += total;
|
||||
});
|
||||
|
||||
const taxRate = parseFloat(document.getElementById('tax-rate').value) || 0;
|
||||
const taxAmount = subtotal * (taxRate / 100);
|
||||
const total = subtotal + taxAmount;
|
||||
|
||||
const currency = document.querySelector('input[name="currency"]').value || '$';
|
||||
|
||||
document.getElementById('subtotal').textContent = currency + ' ' + subtotal.toFixed(2);
|
||||
document.getElementById('total').textContent = currency + ' ' + total.toFixed(2);
|
||||
}
|
||||
|
||||
// Add new item row
|
||||
document.getElementById('add-item').addEventListener('click', function () {
|
||||
const newRow = document.createElement('tr');
|
||||
newRow.innerHTML = `
|
||||
<td><input type="text" class="form-control" name="items[${itemIndex}][description]" placeholder="Название услуги или товара" required></td>
|
||||
<td><input type="number" class="form-control item-qty" name="items[${itemIndex}][qty]" value="1" min="1" required></td>
|
||||
<td><input type="number" class="form-control item-price" name="items[${itemIndex}][price]" value="0.00" step="0.01" min="0" required></td>
|
||||
<td><input type="text" class="form-control item-total" readonly></td>
|
||||
<td><button type="button" class="btn btn-danger btn-sm remove-item">×</button></td>
|
||||
`;
|
||||
itemsTable.querySelector('tbody').appendChild(newRow);
|
||||
itemIndex++;
|
||||
updateTotals();
|
||||
});
|
||||
|
||||
// Remove item row and update totals on click
|
||||
itemsTable.addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('remove-item')) {
|
||||
e.target.closest('tr').remove();
|
||||
updateTotals();
|
||||
}
|
||||
});
|
||||
|
||||
// Update totals on input change
|
||||
itemsTable.addEventListener('input', function (e) {
|
||||
if (e.target.classList.contains('item-qty') || e.target.classList.contains('item-price')) {
|
||||
updateTotals();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('tax-rate').addEventListener('input', updateTotals);
|
||||
document.querySelector('input[name="currency"]').addEventListener('input', updateTotals);
|
||||
|
||||
|
||||
// Initial calculation
|
||||
updateTotals();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user