5 Commits
lab2 ... lab_5

17 changed files with 547 additions and 42 deletions

26
App.jsx Normal file
View File

@@ -0,0 +1,26 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navbar from './components/navbar.jsx';
import HomePage from './pages/HomePage.jsx';
import OutcomPage from './pages/OutcomPage.jsx';
import SpamPage from './pages/SpamPage.jsx';
import IncomPage from './pages/IncomPage.jsx';
import AboutPage from './pages/AboutPage.jsx';
import Footer from './components/footer.jsx';
export default function App() {
return (
<BrowserRouter>
<Navbar/>
<Routes>
<Route path="/" element={<HomePage/>}/>
<Route path="/outcom" element={<OutcomPage/>}/>
<Route path="/spam" element={<SpamPage/>}/>
<Route path="/incom" element={<IncomPage/>}/>
<Route path="/about" element={<AboutPage/>}/>
{/* <Route path="/form" element={<FormPage/>}/>
<Route path="/form/:id" element={<FormPage/>}/> */}
</Routes>
<Footer/>
</BrowserRouter>
);
}

86
OutcomPage.jsx Normal file
View File

@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import MailList from '../components/mailList';
import MailForm from '../components/mailForm';
import { Modal } from 'bootstrap';
import { useMailes } from '../hooks/useMailes';
export default function OutcomPage() {
const { mailes, remove, save, moveToSpam } = useMailes();
const [editingMovie, setEditingMovie] = useState(null);
function handleEdit(movie) {
setEditingMovie(movie);
const modal = new Modal(document.getElementById('movieModal'));
modal.show();
}
function handleSuccess() {
const modal = Modal.getInstance(document.getElementById('movieModal'));
modal.hide();
setEditingMovie(null);
}
function showImageModal(src) {
const img = document.getElementById("modalImage");
img.src = src;
const modal = new Modal(document.getElementById("imageModal"));
modal.show();
}
return (
<main className="flex-grow-1 pt-4">
<div className="container my-5">
<h2>Исходящие сообщения</h2>
<button
className="btn btn-success my-3"
onClick={() => handleEdit(null)}
>
Добавить
</button>
<h3>Сообщения</h3>
<div id="mailList" className="row g-4 mb-5 mt-1">
<MailList
mailes={[...mailes].reverse()} // Добавьте reverse() для отображения в обратном порядке
onEdit={handleEdit}
onDelete={remove}
onImageClick={showImageModal}
onSpam={moveToSpam}
/>
</div>
</div>
{/* форма */}
<div className="modal fade" id="movieModal" tabIndex="-1">
<div className="modal-dialog modal-lg modal-dialog-centered">
<div className="modal-body">
<MailForm
movie={editingMovie}
onSuccess={handleSuccess}
onSave={save}
/>
</div>
</div>
</div>
{/* форма картинки на карточке */}
<div className="modal fade" id="imageModal" tabIndex="-1" aria-hidden="true">
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content">
<div className="modal-body p-0">
<img
id="modalImage"
src=""
alt="movie"
className="img-fluid w-100"
style={{ maxHeight: '90vh', objectFit: 'contain' }}
/>
</div>
</div>
</div>
</div>
</main>
);
}

22
db.backup.json Normal file

File diff suppressed because one or more lines are too long

52
db.json Normal file
View File

@@ -0,0 +1,52 @@
{
"mailes": [
{
"id": "3099",
"title": "43",
"description": "423423",
"img": "",
"userId": 1,
"createdAt": "2023-11-15T10:00:00Z"
}
],
"users": [
{
"id": "1",
"name": "vasilyiZ"
},
{
"id": "2",
"name": "Nagibator228"
},
{
"id": "3",
"name": "Anastasia-Egorova"
}
],
"spam": [
{
"id": "9e04",
"title": "3232",
"description": "123123",
"img": "",
"userId": 1,
"createdAt": "2025-05-28T15:44:52.889Z",
"director": {
"id": "1",
"name": "vasilyiZ"
}
},
{
"id": "faec",
"title": "43242gdgdz11112",
"description": "tgt",
"img": "",
"userId": 2,
"createdAt": "2025-05-28T11:03:33.331Z",
"director": {
"id": "2",
"name": "Nagibator228"
}
}
]
}

26
footer.jsx Normal file
View File

@@ -0,0 +1,26 @@
export default function Footer() {
return (
<footer className="bg-dark text-white mt-auto">
<div className="container py-3 d-flex flex-column flex-md-row justify-content-between align-items-center">
<div>
<p className="mb-1">
<i className="bi bi-telephone-fill me-1"></i>
+7&nbsp;123&nbsp;456&nbsp;7890
</p>
<p className="mb-0">
<i className="bi bi-geo-alt-fill me-1"></i>
г. Москва, ул. Примерная, 1
</p>
</div>
<div className="mt-2 mt-md-0">
<a href="#" className="text-white me-3">
<i className="bi bi-telegram fs-4"></i>
</a>
<a href="#" className="text-white">
<i className="bi bi-youtube fs-4"></i>
</a>
</div>
</div>
</footer>
);
}

BIN
genres.js Normal file

Binary file not shown.

View File

@@ -1,47 +1,19 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title> MailmanPet </title>
<link rel="icon" href="PostmanPat.png" type="image/png">
<link rel="stylesheet" href="style.css"/>
<meta charset="UTF-8">
<title>Почтовый клиент</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="node_modules/bootstrap-icons/icons/PostmanPat.svg" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link href="/style.min.css" rel="stylesheet" />
</head>
<body>
<h1> MailmanPet </h1>
<p> <strong>Почтальон пэт - это многофункциональная иновационная современная почта,
которая может сравниться с привычными вам остальными почтовыми клиентами. </strong> </p>
<h3> Добро пожаловать! </h3>
<div class="abstract">
<p> Что я могу? </p>
<ul>
<li> Отправить письмо </li>
<li> Прочитать письмо </li>
<li> Посмотреть черновики </li>
</ul>
</div>
<div class="logotype">
<img src="PostmanPat.png" width = "200"/>
</div>
<h2>Управление</h2>
<div class="button">
<a href="page2.html"> Входящие сообщения </a>
</div>
<div class="button">
<a href="test/page-test.html"> Исходящие сообщения </a>
</div>
<div class="button">
<a href="page3.html"> Черновики </a>
</div>
<div class="button">
<a href="page4.html"> Спам </a>
</div>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

46
mailCard.jsx Normal file
View File

@@ -0,0 +1,46 @@
import { useState, useEffect } from 'react';
export default function MailCard({ mail, onEdit, onDelete, onAddToSpam }) {
// Удаляем состояние currentDateTime и его обновление
// Получаем дату из props.mail.createdAt
const formattedDateTime = mail.createdAt
? new Date(mail.createdAt).toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: 'Дата не указана';
return (
<div className="card h-100">
<div className="card-body">
<div className="d-flex justify-content-between align-items-start">
<div>
<h5>Тема:</h5>
<h5>{mail.title}</h5>
</div>
<small className="text-muted">{formattedDateTime}</small> {/* Используем сохраненную дату */}
</div>
<h6 className="mt-3">Сообщение:</h6>
<h6>{mail.description}</h6>
<p className="mt-3">Отправитель: <em>{mail.director?.name}@patmail.ru</em></p>
<div className="mt-3">
<button className="btn btn-warning me-2" onClick={onEdit}>
Редактировать
</button>
<button className="btn btn-danger" onClick={onDelete}>
Удалить
</button>
<button className="btn btn-secondary me-2" onClick={onAddToSpam}>
В спам
</button>
</div>
</div>
</div>
);
}

121
mailForm.jsx Normal file
View File

@@ -0,0 +1,121 @@
import { useState, useEffect } from 'react';
import * as API from '../api/mailes';
import * as userAPI from '../api/users';
import { Modal } from 'bootstrap';
export default function MailForm({ movie, onSuccess, onSave }) {
const [title, setTitle] = useState('');
const [description, setDesc] = useState('');
const [img, setImage] = useState('');
const [userId, setUser] = useState('');
const [dirs, setDirs] = useState([]);
useEffect(() => {
console.log('Movie:', movie);
userAPI.fetchUsers().then(setDirs);
if (movie) {
setTitle(movie.title);
setDesc(movie.description);
setImage(movie.img);
setUser(movie.userId);
} else {
setTitle('');
setDesc('');
setImage('');
setUser('');
}
}, [movie]);
function handleFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onloadend = () => {
setImage(reader.result);
};
reader.readAsDataURL(file);
}
async function handleSubmit(e) {
e.preventDefault();
const newMovie = {
title,
description,
img,
userId,
createdAt: movie?.createdAt || new Date().toISOString()
};
await onSave(movie?.id ? { ...newMovie, id: movie.id } : newMovie);
const modal = Modal.getInstance(document.getElementById('movieModal'));
modal.hide();
onSuccess();
}
return (
<form className="modal-content" onSubmit={handleSubmit}>
<div className="modal-header">
<h5 className="modal-title">
{movie ? 'Редактировать сообщение' : 'Добавить сообщение'}
</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Закрыть" />
</div>
<div className="modal-body">
<div className="mb-3">
<label className="form-label">Тема</label>
<input
type="text"
className="form-control"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label className="form-label">Сообщение</label>
<textarea
className="form-control"
value={description}
onChange={(e) => setDesc(e.target.value)}
required
/>
</div>
<div className="mb-3">
<label className="form-label">От кого</label>
<select
className="form-select"
value={userId}
onChange={(e) => setUser(Number(e.target.value))}
required
>
<option value="">-- выбрать пользователя --</option>
{dirs.map((d) => (
<option key={d.id} value={d.id}>
{d.name}
</option>
))}
</select>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">
Отмена
</button>
<button type="submit" className="btn btn-primary">
{movie ? 'Сохранить' : 'Создать'}
</button>
</div>
</form>
);
}

18
mailList.jsx Normal file
View File

@@ -0,0 +1,18 @@
import MailCard from "./mailCard";
export default function MailList({ mailes, onEdit, onDelete, onImageClick, onSpam }) {
return (
<div className="row g-4">
{mailes.map(m => (
<div key={m.id}>
<MailCard
mail={m}
onEdit={() => onEdit(m)}
onDelete={() => onDelete(m.id)}
onImageClick={() => onImageClick(m.img)}
onAddToSpam={() => onSpam(m)}
/>
</div>
))}
</div>
);
}

22
mailes.js Normal file
View File

@@ -0,0 +1,22 @@
const BASE = "/api/mailes";
export const fetchMailes = () =>
fetch(`${BASE}`).then((r) => r.json());
export const fetchMaile = (id) => fetch(`${BASE}/${id}`).then((r) => r.json());
export const createMaile = (m) =>
fetch(BASE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(m),
}).then((r) => r.json());
export const updateMaile = (id, m) =>
fetch(`${BASE}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(m),
}).then((r) => r.json());
export const deleteMaile = (id) => fetch(`${BASE}/${id}`, { method: "DELETE" });

12
main.jsx Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
import 'bootstrap-icons/font/bootstrap-icons.css';
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

43
navbar.jsx Normal file
View File

@@ -0,0 +1,43 @@
import { Link } from 'react-router-dom';
export default function Navbar() {
return (
<nav className="navbar navbar-expand-md navbar-dark bg-primary">
<div className="container">
<Link to="/" className="navbar-brand d-flex align-items-center">
<span className="fs-4">MailmanPat</span>
</Link>
{/* Кнопка-бургер */}
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#mainNavbar"
>
<span className="navbar-toggler-icon"></span>
</button>
{/* Выпадающее меню */}
<div className="collapse navbar-collapse" id="mainNavbar">
<ul className="navbar-nav ms-auto">
<li className="nav-item">
<Link to="/spam" className="nav-link text-white">Спам</Link>
</li>
<li className="nav-item">
<Link to="/incom" className="nav-link text-white">Входящие сообщения</Link>
</li>
<li className="nav-item">
<Link to="/outcom" className="nav-link text-white">Исходящие сообщения</Link>
</li>
<li className="nav-item">
<Link to="/about" className="nav-link text-white">
<i className="bi bi-info-circle"></i> О нас
</Link>
</li>
</ul>
</div>
</div>
</nav>
);
}

44
package.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "lab-1",
"version": "1.0.0",
"description": "online-cinema",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "vite",
"serve": "http-server -p 3000 ./dist/",
"server": "json-server --watch ./db.json --port 3000",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint .",
"format": "prettier --write ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/strwbrry1/Lab-1.git"
},
"author": "strwbrr",
"license": "ISC",
"bugs": {
"url": "https://github.com/strwbrry1/Lab-1/issues"
},
"homepage": "https://github.com/strwbrry1/Lab-1#readme",
"devDependencies": {
"@eslint/js": "^9.25.1",
"@vitejs/plugin-react": "^4.5.0",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"globals": "^16.0.0",
"json-server": "^1.0.0-beta.3",
"prettier": "^3.5.3",
"vite": "^6.3.5"
},
"dependencies": {
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.11.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
}
}

15
spam.js Normal file
View File

@@ -0,0 +1,15 @@
const BASE_URL = 'http://localhost:3000/spam';
export async function fetchSpam() {
const res = await fetch(BASE_URL);
return res.json();
}
export async function addSpam(mail) {
const res = await fetch(BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mail),
});
return res.json();
}

BIN
Отчёт1.docx Normal file

Binary file not shown.

BIN
Отчёт5.docx Normal file

Binary file not shown.