Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19f52c7c8c | |||
| 454b986dd9 | |||
| 86b812c646 | |||
| 978b8172f8 | |||
| 11770cefb1 |
26
App.jsx
Normal file
26
App.jsx
Normal 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
86
OutcomPage.jsx
Normal 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
22
db.backup.json
Normal file
File diff suppressed because one or more lines are too long
52
db.json
Normal file
52
db.json
Normal 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
26
footer.jsx
Normal 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 123 456 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>
|
||||
);
|
||||
}
|
||||
56
index.html
56
index.html
@@ -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>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
46
mailCard.jsx
Normal file
46
mailCard.jsx
Normal 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
121
mailForm.jsx
Normal 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
18
mailList.jsx
Normal 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
22
mailes.js
Normal 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
12
main.jsx
Normal 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
43
navbar.jsx
Normal 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
44
package.json
Normal 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
15
spam.js
Normal 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
BIN
Отчёт1.docx
Normal file
Binary file not shown.
BIN
Отчёт5.docx
Normal file
BIN
Отчёт5.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user