Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 7fdb35e

Browse files
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)

AltStyle によって変換されたページ (->オリジナル) /