Skip to content

Commit 7fdb35e

Browse files
authored
Merge pull request #19 from maxchistt/image
Image
2 parents 8be6f81 + 1c53883 commit 7fdb35e

File tree

20 files changed

+563
-44
lines changed

20 files changed

+563
-44
lines changed

client/src/Hooks/http.hook.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const useHttp = () => {
2222
if (body) {
2323
body = JSON.stringify(body)
2424
headers['Content-Type'] = 'application/json'
25+
headers['Content-Length'] = String(Number(String(body).length) + 100)
2526
}
2627
/**отправка запроса */
2728
const response = await fetch(url, { method, body, headers })

client/src/Hooks/useFetchNotes.hook.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ function useFetchNotes(token) {
1818
*/
1919
async function fetchNotes(url = "", method = "GET", body = null, resCallback = () => { }) {
2020
try {
21-
/**запрос к серверу с определенными параметрами*/
21+
/**запрос к серверу о заметках с определенными параметрами*/
2222
const fetched = await request(`/api/notes${url ? ("/" + url) : ""}`, method, body, { Authorization: `Bearer ${token}` })
2323
resCallback(tryParce(fetched))
2424
} catch (e) { }
2525
}
2626

27+
async function fetchMedia(url = "", method = "GET", body = null, resCallback = () => { }) {
28+
try {
29+
/**запрос к серверу о медиа с определенными параметрами*/
30+
const fetched = await request(`/api/media${url ? ("/" + url) : ""}`, method, body, { Authorization: `Bearer ${token}` })
31+
resCallback(tryParce(fetched))
32+
} catch (e) { }
33+
}
34+
2735
function tryParce(str) {
2836
try {
2937
return JSON.parse(str);
@@ -32,7 +40,7 @@ function useFetchNotes(token) {
3240
}
3341
}
3442

35-
return { loading, fetchNotes, error, clearError }
43+
return { loading, fetchNotes, fetchMedia, error, clearError }
3644
}
3745

3846
export default useFetchNotes

client/src/NoteComponents/ModalNoteEdit.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import NotesContext from '../Context/NotesContext'
66
import TextareaAutosize from 'react-textarea-autosize'
77
import Modal, { ModalProps } from "../Shared/Components/Modal/Modal"
88
import Palette from './palette/palette'
9+
import Media from './media/media'
910

1011
/**расчет числа строк */
1112
function calcMaxRows() {
@@ -25,7 +26,7 @@ function calcMaxRows() {
2526
*/
2627
function ModalNoteEdit() {
2728
/**получение контекста */
28-
const { removeNote, changeNoteColor, unsetEditNoteId, editNoteContent, getNoteById, editNoteId } = React.useContext(NotesContext)
29+
const { removeNote, changeNoteColor, unsetEditNoteId, editNoteMedia, editNoteContent, getNoteById, editNoteId } = React.useContext(NotesContext)
2930

3031
/** обьект заметки */
3132
const note = getNoteById(editNoteId)
@@ -76,6 +77,14 @@ function ModalNoteEdit() {
7677
changeNoteColor(editNoteId, color)
7778
}
7879

80+
/**
81+
* Изменение медиа заметки
82+
* @param {*} media
83+
*/
84+
function trySetNoteMedia(media) {
85+
editNoteMedia(editNoteId, media)
86+
}
87+
7988
/**
8089
* удаление
8190
*/
@@ -93,10 +102,12 @@ function ModalNoteEdit() {
93102
close()
94103
}
95104

105+
const sizeRef = React.useRef()
106+
96107
/**рендер */
97108
return (
98109
<Modal {...modalProps.bind()}>
99-
<div className="container p-2">
110+
<div ref={sizeRef} className="container p-2">
100111
{/**Блок редактирования контента */}
101112
<div>
102113
{note ? (
@@ -144,6 +155,15 @@ function ModalNoteEdit() {
144155
disabled={!note}
145156
setColor={tryChangeColor}
146157
></Palette>
158+
<Media
159+
className="btn btn-light mx-1"
160+
style={{ boxShadow: "none" }}
161+
disabled={!note}
162+
mediaList={note ? note.media || [] : []}
163+
setNoteMedia={trySetNoteMedia}
164+
noteId={note ? note.id : null}
165+
sizeData={sizeRef}
166+
></Media>
147167
<button
148168
className="btn btn-light"
149169
style={{ boxShadow: "none" }}
@@ -153,7 +173,7 @@ function ModalNoteEdit() {
153173
</div>
154174
{/**Индикатор номера заметки */}
155175
<div className="mx-auto">
156-
<span style={{ color: "lightgray", fontWeight: "400" }}>{note && note.order}</span>
176+
<span style={{ color: "lightgray", fontWeight: "400" }}>{note && String(note.order)}</span>
157177
</div>
158178
{/**Зактрытие окна */}
159179
<div>

client/src/NoteComponents/NoteItem.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ function fixLineBreaks(mdStr) {
1919
*/
2020
function NoteItem({ note }) {
2121
/**Подключение контекста */
22-
const { setEditNoteId, editNoteOrder } = useContext(NotesContext)
22+
const { setEditNoteId, editNoteOrder, getMediaById } = useContext(NotesContext)
2323

2424
const lineClip = 12
25-
const bgColor = note.color
25+
const bgColor = note.color || "#f8f9fa"
26+
const mediaList = note.media ? note.media || [] : []
2627

2728
const footerBtn = {
2829
className: `btn btn-light p-0 text-secondary item-footer-btn`,
@@ -37,6 +38,14 @@ function NoteItem({ note }) {
3738
return (
3839
<div className="p-1" >
3940
<div className="card" style={{ backgroundColor: bgColor }} >
41+
{/**Изображение заметки*/}
42+
{Array.isArray(mediaList) ? (mediaList.map((imgId) => {
43+
const media = getMediaById(imgId)
44+
const src = typeof media === "object" && media && media.data
45+
return src && (
46+
<img key={imgId} onClick={() => setEditNoteId(note.id)} className="card-img-top" src={src} alt="note img"></img>
47+
)
48+
})) : null}
4049
{/**Заголовок и текст заметки с обработчиками отображения markdown*/}
4150
<div className="card-body" onClick={() => setEditNoteId(note.id)} >
4251
<div
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.media-btn:focus {
2+
box-shadow: 0 0 0 0.2rem rgb(216 217 219 / 50%) !important;
3+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @file media.js
3+
*/
4+
import React, { useContext } from "react"
5+
import PropTypes from 'prop-types'
6+
import "./media.css"
7+
import NotesContext from "../../Context/NotesContext"
8+
import Modal, { ModalProps } from "../../Shared/Components/Modal/Modal"
9+
import { downscaleImage } from "../../Shared/downscaleImage"
10+
11+
const MAX_PAYLOAD_SIZE = 100 * 1024
12+
13+
/**
14+
* Сжатие url изображения c проверкой размера
15+
* @param {String} uncompressed
16+
* @param {String} type
17+
*/
18+
async function getCompressed(uncompressed, type) {
19+
if (uncompressed.length < MAX_PAYLOAD_SIZE) return uncompressed
20+
const smallcompressedRes = await downscaleImage(uncompressed, type, 480)
21+
if (smallcompressedRes.length < MAX_PAYLOAD_SIZE) return smallcompressedRes
22+
const mediumcompressedRes = await downscaleImage(uncompressed, type, 360)
23+
if (mediumcompressedRes.length < MAX_PAYLOAD_SIZE) return mediumcompressedRes
24+
const extracompressedRes = await downscaleImage(uncompressed, type, 240)
25+
if (extracompressedRes.length < MAX_PAYLOAD_SIZE) return extracompressedRes
26+
console.error("compressed unsuc, too long url")
27+
return null
28+
}
29+
30+
/**
31+
* компонент палитры
32+
* @param {object} props
33+
* @param {void} props.setNoteMedia
34+
* @param {Array<String>} props.mediaList
35+
* @param {{}} props.style
36+
* @param {String} props.className
37+
* @param {Boolean} props.disabled
38+
* @param {String} props.noteId
39+
* @param {{}} props.sizeData
40+
*/
41+
function Media({ setNoteMedia, mediaList = [], style, className, disabled, noteId, sizeData }) {
42+
const { addMedia, removeMedia, getMediaById, getNoteById } = useContext(NotesContext)
43+
44+
const limited = getNoteById(noteId).media.length >= 1
45+
46+
/**хук состояния формы */
47+
const [showForm, setShowForm] = React.useState(false)
48+
49+
/**создание параметров модального окна*/
50+
const modalProps = new ModalProps()
51+
modalProps.isOpen = showForm
52+
modalProps.setOpenState = setShowForm
53+
modalProps.sideClose = true
54+
55+
/**открытие окна */
56+
function open() {
57+
setShowForm(true)
58+
}
59+
60+
/**закрытие окна */
61+
function close() {
62+
setShowForm(false)
63+
}
64+
65+
function encodeImageFileAsURLAndPost(e) {
66+
var file = e.target.files[0]
67+
var reader = new FileReader()
68+
69+
reader.onloadend = async function () {
70+
const uncompressedReaderRes = reader.result
71+
const compressedRes = await getCompressed(uncompressedReaderRes, file.type)
72+
73+
if (compressedRes) {
74+
const mediaId = addMedia(compressedRes, noteId)
75+
Array.isArray(mediaList) ? mediaList.push(mediaId) : (mediaList = [mediaId])
76+
setNoteMedia(mediaList)
77+
}
78+
79+
e.target.value = null
80+
}
81+
82+
if (file !== undefined) reader.readAsDataURL(file)
83+
}
84+
85+
function delImg(imgId, index) {
86+
removeMedia(imgId)
87+
mediaList.splice(index, 1)
88+
setNoteMedia(mediaList)
89+
}
90+
91+
return (
92+
<React.Fragment>
93+
{/**Кнопка вызова media */}
94+
<button disabled={disabled} className={`btn ${className}`} style={style} type="button" onClick={open} >
95+
<i className="bi bi-image" ></i>
96+
</button>
97+
98+
{/**Форма media */}
99+
<Modal {...modalProps.bind()} >
100+
<div style={{ minHeight: `${sizeData.current ? sizeData.current.parentElement.clientHeight : 100}px` }} className="p-1 d-flex flex-wrap align-content-between align-items-center justify-content-center">
101+
102+
<div className="form-group container d-flex flex-row flex-wrap align-items-start justify-content-around mb-0">
103+
{Array.isArray(mediaList) && mediaList.length ? (mediaList.map((imgId, index) => {
104+
const media = getMediaById(imgId)
105+
const src = typeof media === "object" && media && media.data
106+
return (
107+
<div className="card p-1 m-1" key={imgId} style={{ position: "relative" }}>
108+
<img className="img-fluid" style={{ maxWidth: "35em", maxHeight: "15em" }} src={src} alt="note img"></img>
109+
<button
110+
style={{ position: "absolute", bottom: "0", right: "0", lineHeight: "1em", padding: "0.05em" }}
111+
className={`btn btn-danger m-1`}
112+
onClick={() => delImg(imgId, index)}
113+
>&#10007;</button>
114+
</div>
115+
)
116+
})) : (
117+
<div className="p-1 m-1 text-center" style={{ minWidth: '100%' }}>
118+
No images
119+
</div>
120+
)}
121+
</div>
122+
123+
<div className="form-group container row mb-0">
124+
<div className="custom-file p-0 col m-1" style={{ minWidth: "7.6em" }}>
125+
<input style={{ cursor: "pointer" }} disabled={limited} onChange={encodeImageFileAsURLAndPost} type="file" className="custom-file-input" id="noteImgFile" accept=".jpg, .jpeg, .png" />
126+
<label style={{ boxShadow: "none", border: "lightgray 1px solid" }} className="custom-file-label" htmlFor="noteImgFile">{"Img"}</label>
127+
</div>
128+
<button className="btn btn-light col-3 col-sm-2 m-1 ml-auto" style={{ boxShadow: "none", minWidth: "4em" }} onClick={close} >
129+
Close
130+
</button>
131+
</div>
132+
133+
</div>
134+
</Modal>
135+
</React.Fragment>
136+
);
137+
}
138+
139+
// Валидация
140+
Media.propTypes = {
141+
setNoteMedia: PropTypes.func,
142+
style: PropTypes.object,
143+
className: PropTypes.string,
144+
}
145+
146+
export default Media;
147+
148+
149+
150+
151+
152+

client/src/NoteComponents/palette/palette.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ function Palette({ setColor, style, className, disabled }) {
3232
className={`btn ${className}`}
3333
style={style}
3434
type="button"
35-
id="dropdownMenuButton"
35+
id="dropdownMenuButtonPalette"
3636
data-toggle="dropdown"
3737
aria-haspopup="true"
3838
aria-expanded="false"
3939
>
4040
<i className="bi bi-palette" ></i>
4141
</button>
4242
{/**Форма выбора цвета */}
43-
<div className="dropdown-menu tab-content mt-1" aria-labelledby="dropdownMenuButton">
43+
<div className="dropdown-menu tab-content mt-1" aria-labelledby="dropdownMenuButtonPalette">
4444
<form>
4545
<div className="d-flex flex-row flex-wrap justify-content-center">
4646
{colors.map((color, key) => (

0 commit comments

Comments
 (0)