pages_core 3.13.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/app/assets/builds/pages_core/admin-dist.js +19 -8
  4. data/app/assets/builds/pages_core/admin-dist.js.map +4 -4
  5. data/app/assets/builds/pages_core/admin.css +704 -388
  6. data/app/assets/fonts/Inter-Black.woff2 +0 -0
  7. data/app/assets/fonts/Inter-BlackItalic.woff2 +0 -0
  8. data/app/assets/fonts/Inter-Bold.woff2 +0 -0
  9. data/app/assets/fonts/Inter-BoldItalic.woff2 +0 -0
  10. data/app/assets/fonts/Inter-ExtraBold.woff2 +0 -0
  11. data/app/assets/fonts/Inter-ExtraBoldItalic.woff2 +0 -0
  12. data/app/assets/fonts/Inter-ExtraLight.woff2 +0 -0
  13. data/app/assets/fonts/Inter-ExtraLightItalic.woff2 +0 -0
  14. data/app/assets/fonts/Inter-Italic.woff2 +0 -0
  15. data/app/assets/fonts/Inter-Light.woff2 +0 -0
  16. data/app/assets/fonts/Inter-LightItalic.woff2 +0 -0
  17. data/app/assets/fonts/Inter-Medium.woff2 +0 -0
  18. data/app/assets/fonts/Inter-MediumItalic.woff2 +0 -0
  19. data/app/assets/fonts/Inter-Regular.woff2 +0 -0
  20. data/app/assets/fonts/Inter-SemiBold.woff2 +0 -0
  21. data/app/assets/fonts/Inter-SemiBoldItalic.woff2 +0 -0
  22. data/app/assets/fonts/Inter-Thin.woff2 +0 -0
  23. data/app/assets/fonts/Inter-ThinItalic.woff2 +0 -0
  24. data/app/assets/fonts/InterDisplay-Black.woff2 +0 -0
  25. data/app/assets/fonts/InterDisplay-BlackItalic.woff2 +0 -0
  26. data/app/assets/fonts/InterDisplay-Bold.woff2 +0 -0
  27. data/app/assets/fonts/InterDisplay-BoldItalic.woff2 +0 -0
  28. data/app/assets/fonts/InterDisplay-ExtraBold.woff2 +0 -0
  29. data/app/assets/fonts/InterDisplay-ExtraBoldItalic.woff2 +0 -0
  30. data/app/assets/fonts/InterDisplay-ExtraLight.woff2 +0 -0
  31. data/app/assets/fonts/InterDisplay-ExtraLightItalic.woff2 +0 -0
  32. data/app/assets/fonts/InterDisplay-Italic.woff2 +0 -0
  33. data/app/assets/fonts/InterDisplay-Light.woff2 +0 -0
  34. data/app/assets/fonts/InterDisplay-LightItalic.woff2 +0 -0
  35. data/app/assets/fonts/InterDisplay-Medium.woff2 +0 -0
  36. data/app/assets/fonts/InterDisplay-MediumItalic.woff2 +0 -0
  37. data/app/assets/fonts/InterDisplay-Regular.woff2 +0 -0
  38. data/app/assets/fonts/InterDisplay-SemiBold.woff2 +0 -0
  39. data/app/assets/fonts/InterDisplay-SemiBoldItalic.woff2 +0 -0
  40. data/app/assets/fonts/InterDisplay-Thin.woff2 +0 -0
  41. data/app/assets/fonts/InterDisplay-ThinItalic.woff2 +0 -0
  42. data/app/assets/fonts/InterVariable-Italic.woff2 +0 -0
  43. data/app/assets/fonts/InterVariable.woff2 +0 -0
  44. data/app/assets/stylesheets/pages_core/admin/components/archive.css +1 -1
  45. data/app/assets/stylesheets/pages_core/admin/components/attachments.css +22 -34
  46. data/app/assets/stylesheets/pages_core/admin/components/base.css +1 -68
  47. data/app/assets/stylesheets/pages_core/admin/components/forms.css +107 -48
  48. data/app/assets/stylesheets/pages_core/admin/components/header.css +56 -58
  49. data/app/assets/stylesheets/pages_core/admin/components/image_editor.css +35 -24
  50. data/app/assets/stylesheets/pages_core/admin/components/image_grid.css +28 -27
  51. data/app/assets/stylesheets/pages_core/admin/components/image_uploader.css +5 -5
  52. data/app/assets/stylesheets/pages_core/admin/components/layout.css +7 -1
  53. data/app/assets/stylesheets/pages_core/admin/components/list_table.css +24 -15
  54. data/app/assets/stylesheets/pages_core/admin/components/page_tree.css +63 -104
  55. data/app/assets/stylesheets/pages_core/admin/components/pagination.css +12 -13
  56. data/app/assets/stylesheets/pages_core/admin/components/search.css +1 -16
  57. data/app/assets/stylesheets/pages_core/admin/components/sidebar.css +5 -11
  58. data/app/assets/stylesheets/pages_core/admin/components/tag_editor.css +22 -36
  59. data/app/assets/stylesheets/pages_core/admin/components/toast.css +1 -2
  60. data/app/assets/stylesheets/pages_core/admin/components/toolbar.css +10 -10
  61. data/app/assets/stylesheets/pages_core/admin/components/totp.css +26 -0
  62. data/app/assets/stylesheets/pages_core/admin/controllers/pages.css +37 -51
  63. data/app/assets/stylesheets/pages_core/admin/global/fonts.css +271 -0
  64. data/app/assets/stylesheets/pages_core/admin/global/typography.css +109 -0
  65. data/app/assets/stylesheets/pages_core/admin/vars.css +1 -3
  66. data/app/assets/stylesheets/pages_core/admin.postcss.css +1 -0
  67. data/app/controllers/admin/account_recoveries_controller.rb +87 -0
  68. data/app/controllers/admin/invites_controller.rb +3 -2
  69. data/app/controllers/admin/otp_secrets_controller.rb +45 -0
  70. data/app/controllers/admin/pages_controller.rb +22 -42
  71. data/app/controllers/admin/recovery_codes_controller.rb +32 -0
  72. data/app/controllers/admin/sessions_controller.rb +65 -0
  73. data/app/controllers/admin/users_controller.rb +2 -8
  74. data/app/controllers/concerns/pages_core/authentication.rb +12 -10
  75. data/app/controllers/concerns/pages_core/error_reporting.rb +1 -1
  76. data/app/controllers/concerns/pages_core/page_parameters.rb +29 -0
  77. data/app/controllers/concerns/pages_core/policies_helper.rb +1 -1
  78. data/app/controllers/concerns/pages_core/preview_pages_controller.rb +20 -20
  79. data/app/controllers/pages_core/admin_controller.rb +1 -3
  80. data/app/controllers/pages_core/frontend/pages_controller.rb +2 -6
  81. data/app/formatters/pages_core/html_formatter.rb +2 -4
  82. data/app/helpers/admin/menu_helper.rb +5 -4
  83. data/app/helpers/admin/pages_helper.rb +1 -21
  84. data/app/helpers/pages_core/admin/admin_helper.rb +13 -3
  85. data/app/helpers/pages_core/admin/content_tabs_helper.rb +1 -2
  86. data/app/helpers/pages_core/admin/labelled_field_helper.rb +1 -1
  87. data/app/helpers/pages_core/frontend_helper.rb +1 -1
  88. data/app/helpers/pages_core/images_helper.rb +10 -8
  89. data/app/helpers/pages_core/labelled_form_builder.rb +2 -7
  90. data/app/helpers/pages_core/page_path_helper.rb +1 -1
  91. data/app/javascript/components/Attachments/Attachment.tsx +20 -18
  92. data/app/javascript/components/Attachments/AttachmentEditor.tsx +11 -9
  93. data/app/javascript/components/{Attachments.jsx → Attachments/List.tsx} +58 -63
  94. data/app/javascript/components/Attachments/useAttachments.ts +15 -0
  95. data/app/javascript/components/Attachments.tsx +14 -0
  96. data/app/javascript/components/DateRangeSelect.tsx +105 -0
  97. data/app/javascript/components/DateTimeSelect.tsx +136 -0
  98. data/app/javascript/components/EditableImage.tsx +11 -9
  99. data/app/javascript/components/FileUploadButton.tsx +7 -7
  100. data/app/javascript/components/ImageCropper/FocalPoint.tsx +9 -12
  101. data/app/javascript/components/ImageCropper/Image.tsx +10 -8
  102. data/app/javascript/components/ImageCropper/Toolbar.tsx +11 -12
  103. data/app/javascript/components/ImageCropper/useCrop.ts +24 -53
  104. data/app/javascript/components/ImageCropper.tsx +10 -15
  105. data/app/javascript/components/ImageEditor/Form.tsx +12 -8
  106. data/app/javascript/components/ImageEditor.tsx +12 -7
  107. data/app/javascript/components/ImageGrid/DragElement.tsx +9 -12
  108. data/app/javascript/components/{ImageGrid.jsx → ImageGrid/Grid.tsx} +62 -71
  109. data/app/javascript/components/ImageGrid/GridImage.tsx +22 -23
  110. data/app/javascript/components/ImageGrid/Placeholder.tsx +2 -2
  111. data/app/javascript/components/ImageGrid/useImageGrid.ts +26 -0
  112. data/app/javascript/components/ImageGrid.tsx +15 -0
  113. data/app/javascript/components/ImageUploader.tsx +35 -22
  114. data/app/javascript/components/LabelledField.tsx +34 -0
  115. data/app/javascript/components/Modal.tsx +2 -2
  116. data/app/javascript/components/PageForm/Block.tsx +81 -0
  117. data/app/javascript/components/PageForm/Content.tsx +54 -0
  118. data/app/javascript/components/PageForm/Dates.tsx +66 -0
  119. data/app/javascript/components/PageForm/Files.tsx +28 -0
  120. data/app/javascript/components/PageForm/Form.tsx +41 -0
  121. data/app/javascript/components/PageForm/Images.tsx +28 -0
  122. data/app/javascript/components/PageForm/LocaleLinks.tsx +36 -0
  123. data/app/javascript/components/PageForm/Metadata.tsx +67 -0
  124. data/app/javascript/components/PageForm/Options.tsx +180 -0
  125. data/app/javascript/components/PageForm/PageDescription.tsx +48 -0
  126. data/app/javascript/components/PageForm/PathSegment.tsx +65 -0
  127. data/app/javascript/components/PageForm/TabPanel.tsx +21 -0
  128. data/app/javascript/components/PageForm/Tabs.tsx +33 -0
  129. data/app/javascript/components/PageForm/UnconfiguredContent.tsx +42 -0
  130. data/app/javascript/components/PageForm/pageParams.ts +95 -0
  131. data/app/javascript/components/PageForm/preview.ts +23 -0
  132. data/app/javascript/components/PageForm/usePage.ts +169 -0
  133. data/app/javascript/components/PageForm/useTabs.ts +46 -0
  134. data/app/javascript/components/PageForm.tsx +163 -0
  135. data/app/javascript/components/PageImages.tsx +7 -9
  136. data/app/javascript/components/PageTree/Draggable.tsx +40 -39
  137. data/app/javascript/components/PageTree/Node.tsx +62 -56
  138. data/app/javascript/components/PageTree/PageName.tsx +28 -0
  139. data/app/javascript/components/PageTree.tsx +65 -53
  140. data/app/javascript/components/{RichTextArea.jsx → RichTextArea.tsx} +98 -79
  141. data/app/javascript/components/RichTextToolbarButton.tsx +4 -6
  142. data/app/javascript/components/TagEditor/AddTagForm.tsx +19 -12
  143. data/app/javascript/components/TagEditor/Editor.tsx +32 -0
  144. data/app/javascript/components/TagEditor/Tag.tsx +6 -4
  145. data/app/javascript/components/TagEditor/useTags.ts +58 -0
  146. data/app/javascript/components/TagEditor.tsx +8 -58
  147. data/app/javascript/components/Toast.tsx +3 -3
  148. data/app/javascript/components/drag/draggedOrder.ts +22 -14
  149. data/app/javascript/components/drag/useDragCollection.ts +35 -30
  150. data/app/javascript/components/drag/useDragUploader.ts +32 -21
  151. data/app/javascript/components/drag/useDraggable.ts +7 -6
  152. data/app/javascript/components/drag.ts +0 -1
  153. data/app/javascript/components.ts +1 -3
  154. data/app/javascript/features/RichText.tsx +2 -3
  155. data/app/javascript/features/contentTabs.ts +79 -0
  156. data/app/javascript/index.ts +5 -14
  157. data/app/javascript/lib/Tree.ts +31 -45
  158. data/app/javascript/lib/request.ts +11 -11
  159. data/app/javascript/stores/useToastStore.ts +1 -1
  160. data/app/javascript/types/Attachments.ts +29 -0
  161. data/app/javascript/types/Crop.ts +36 -0
  162. data/app/javascript/types/Drag.ts +34 -0
  163. data/app/javascript/types/Images.ts +47 -0
  164. data/app/javascript/types/PageEditor.ts +26 -0
  165. data/app/javascript/types/Pages.ts +75 -0
  166. data/app/javascript/types/Tags.ts +9 -0
  167. data/app/javascript/types/Template.ts +24 -0
  168. data/app/javascript/types/Trees.ts +19 -0
  169. data/app/javascript/types.ts +2 -25
  170. data/app/mailers/admin_mailer.rb +2 -2
  171. data/app/models/attachment.rb +1 -1
  172. data/app/models/concerns/pages_core/authenticable_user.rb +63 -0
  173. data/app/models/concerns/pages_core/emailable.rb +16 -0
  174. data/app/models/concerns/pages_core/page_model/templateable.rb +2 -16
  175. data/app/models/invite.rb +2 -6
  176. data/app/models/otp_secret.rb +101 -0
  177. data/app/models/page.rb +0 -3
  178. data/app/models/user.rb +2 -68
  179. data/app/policies/page_policy.rb +6 -2
  180. data/app/policies/user_policy.rb +4 -0
  181. data/app/resources/admin/page_resource.rb +95 -0
  182. data/app/resources/admin/page_tree_resource.rb +27 -0
  183. data/app/resources/admin/template_configuration_resource.rb +50 -0
  184. data/app/views/admin/account_recoveries/new.html.erb +22 -0
  185. data/app/views/admin/account_recoveries/show.html.erb +37 -0
  186. data/app/views/admin/invites/show.html.erb +1 -1
  187. data/app/views/admin/news/_sidebar.html.erb +2 -4
  188. data/app/views/admin/news/index.html.erb +0 -1
  189. data/app/views/admin/otp_secrets/create.html.erb +7 -0
  190. data/app/views/admin/otp_secrets/new.html.erb +60 -0
  191. data/app/views/admin/pages/_form.html.erb +10 -30
  192. data/app/views/admin/pages/_search_bar.html.erb +1 -1
  193. data/app/views/admin/pages/edit.html.erb +1 -57
  194. data/app/views/admin/pages/index.html.erb +1 -1
  195. data/app/views/admin/pages/new.html.erb +1 -44
  196. data/app/views/admin/recovery_codes/_codes.html.erb +14 -0
  197. data/app/views/admin/recovery_codes/create.html.erb +7 -0
  198. data/app/views/admin/recovery_codes/new.html.erb +11 -0
  199. data/app/views/admin/sessions/_otp_form.html.erb +13 -0
  200. data/app/views/admin/sessions/new.html.erb +31 -0
  201. data/app/views/admin/sessions/verify_otp.html.erb +19 -0
  202. data/app/views/admin/users/_access_control.html.erb +5 -1
  203. data/app/views/admin/users/_list.html.erb +12 -7
  204. data/app/views/admin/users/edit.html.erb +31 -1
  205. data/app/views/admin/users/new.html.erb +1 -1
  206. data/app/views/admin_mailer/account_recovery.text.erb +10 -0
  207. data/app/views/layouts/admin/_header.html.erb +3 -5
  208. data/app/views/layouts/admin/_page_header.html.erb +1 -2
  209. data/app/views/layouts/admin/_toast.html.erb +12 -0
  210. data/app/views/layouts/admin.html.erb +2 -2
  211. data/config/locales/en.yml +11 -7
  212. data/config/routes.rb +13 -12
  213. data/db/migrate/20240126160700_add_2fa_fields.rb +26 -0
  214. data/db/migrate/20240129201300_remove_password_reset_tokens.rb +13 -0
  215. data/db/migrate/20240131140700_change_email_to_citext.rb +18 -0
  216. data/db/migrate/20240201160700_remove_persistent_data.rb +7 -0
  217. data/db/migrate/20240508145300_remove_categories.rb +21 -0
  218. data/lib/pages_core/configuration/base.rb +2 -2
  219. data/lib/pages_core/templates/configuration.rb +1 -1
  220. data/lib/pages_core/templates/configuration_proxy.rb +2 -2
  221. data/lib/pages_core/templates/template_configuration.rb +11 -1
  222. data/lib/pages_core/templates.rb +6 -4
  223. data/lib/pages_core/version.rb +1 -1
  224. data/lib/pages_core.rb +6 -0
  225. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/gridOverlay.ts +6 -7
  226. data/lib/rails/generators/pages_core/frontend/templates/javascript/lib/responsiveEmbeds.ts +17 -12
  227. data/lib/rails/generators/pages_core/rspec/rspec_generator.rb +0 -2
  228. data/lib/rails/generators/pages_core/rspec/templates/rails_helper.rb +3 -4
  229. metadata +143 -35
  230. data/app/assets/stylesheets/pages_core/admin/components/login.css +0 -33
  231. data/app/controllers/admin/categories_controller.rb +0 -56
  232. data/app/controllers/admin/password_resets_controller.rb +0 -85
  233. data/app/controllers/concerns/pages_core/admin/persistent_params.rb +0 -75
  234. data/app/controllers/sessions_controller.rb +0 -27
  235. data/app/helpers/pages_core/admin/page_blocks_helper.rb +0 -66
  236. data/app/helpers/pages_core/admin/page_json_helper.rb +0 -23
  237. data/app/javascript/components/DateRangeSelect.jsx +0 -225
  238. data/app/javascript/components/PageDates.jsx +0 -73
  239. data/app/javascript/components/PageFiles.jsx +0 -25
  240. data/app/javascript/components/PageTree/types.ts +0 -15
  241. data/app/javascript/components/drag/types.ts +0 -28
  242. data/app/javascript/controllers/EditPageController.ts +0 -22
  243. data/app/javascript/controllers/LoginController.ts +0 -32
  244. data/app/javascript/controllers/MainController.ts +0 -74
  245. data/app/javascript/controllers/PageOptionsController.js +0 -67
  246. data/app/models/category.rb +0 -22
  247. data/app/models/page_category.rb +0 -6
  248. data/app/models/password_reset_token.rb +0 -34
  249. data/app/views/admin/pages/_edit_content.html.erb +0 -19
  250. data/app/views/admin/pages/_edit_files.html.erb +0 -4
  251. data/app/views/admin/pages/_edit_images.html.erb +0 -4
  252. data/app/views/admin/pages/_edit_metadata.html.erb +0 -35
  253. data/app/views/admin/pages/_edit_options.html.erb +0 -91
  254. data/app/views/admin/password_resets/show.html.erb +0 -21
  255. data/app/views/admin/users/login.html.erb +0 -65
  256. data/app/views/admin_mailer/password_reset.text.erb +0 -11
  257. data/lib/rails/generators/pages_core/rspec/templates/mailer_macros.rb +0 -11
@@ -1,20 +1,22 @@
1
- import React, { useState } from "react";
2
- import ImageEditor from "./ImageEditor";
1
+ import React, { MouseEvent, useState } from "react";
2
+
3
3
  import useModalStore from "../stores/useModalStore";
4
+ import * as Images from "../types/Images";
5
+ import { Locale } from "../types";
4
6
 
5
- import { Locale, ImageResource } from "../types";
7
+ import ImageEditor from "./ImageEditor";
6
8
 
7
- interface EditableImageProps {
8
- image: ImageResource;
9
+ interface Props {
10
+ image: Images.Resource;
9
11
  src: string;
10
12
  caption: boolean;
11
13
  locale: string;
12
14
  locales: Record<string, Locale>;
13
15
  width: number;
14
- onUpdate?: (newImage: ImageResource, src: string) => void;
16
+ onUpdate?: (newImage: Images.Resource, src: string) => void;
15
17
  }
16
18
 
17
- export default function EditableImage(props: EditableImageProps) {
19
+ export default function EditableImage(props: Props) {
18
20
  const [image, setImage] = useState(props.image);
19
21
  const [src, setSrc] = useState(props.src);
20
22
 
@@ -26,7 +28,7 @@ export default function EditableImage(props: EditableImageProps) {
26
28
  return Math.round((height / width) * props.width);
27
29
  };
28
30
 
29
- const updateImage = (updatedImage: ImageResource, src: string) => {
31
+ const updateImage = (updatedImage: Images.Resource, src: string) => {
30
32
  const newImage = { ...image, ...updatedImage };
31
33
  setSrc(src);
32
34
  setImage(newImage);
@@ -35,7 +37,7 @@ export default function EditableImage(props: EditableImageProps) {
35
37
  }
36
38
  };
37
39
 
38
- const handleClick = (evt: Event) => {
40
+ const handleClick = (evt: MouseEvent) => {
39
41
  evt.preventDefault();
40
42
  openModal(
41
43
  <ImageEditor
@@ -1,13 +1,13 @@
1
- import React, { ChangeEvent, useRef } from "react";
1
+ import React, { ChangeEvent, MouseEvent, useRef } from "react";
2
2
 
3
- interface FileUploadButtonProps {
3
+ interface Props {
4
4
  callback: (files: File[]) => void;
5
- type: string;
6
- multiple: boolean;
7
- multiline: boolean;
5
+ type?: string;
6
+ multiline?: boolean;
7
+ multiple?: boolean;
8
8
  }
9
9
 
10
- export default function FileUploadButton(props: FileUploadButtonProps) {
10
+ export default function FileUploadButton(props: Props) {
11
11
  const inputRef = useRef<HTMLInputElement>();
12
12
 
13
13
  const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
@@ -21,7 +21,7 @@ export default function FileUploadButton(props: FileUploadButtonProps) {
21
21
  }
22
22
  };
23
23
 
24
- const triggerDialog = (evt: Event) => {
24
+ const triggerDialog = (evt: MouseEvent) => {
25
25
  evt.preventDefault();
26
26
  inputRef.current.click();
27
27
  };
@@ -1,14 +1,11 @@
1
- import React, { useRef, useState } from "react";
1
+ import React, { MouseEvent, TouchEvent, useRef, useState } from "react";
2
2
 
3
- interface Position {
4
- x: number;
5
- y: number;
6
- }
3
+ import * as Crop from "../../types/Crop";
7
4
 
8
- interface FocalPointProps {
5
+ interface Props {
9
6
  x: number;
10
7
  y: number;
11
- onChange: (pos: Position) => void;
8
+ onChange: (pos: Crop.Position) => void;
12
9
  width: number;
13
10
  height: number;
14
11
  }
@@ -23,11 +20,11 @@ function clamp(val: number, min: number, max: number): number {
23
20
  }
24
21
  }
25
22
 
26
- export default function FocalPoint(props: FocalPointProps) {
23
+ export default function FocalPoint(props: Props) {
27
24
  const { width, height, onChange } = props;
28
25
 
29
26
  const [dragging, setDragging] = useState(false);
30
- const [position, setPosition] = useState<Position>({
27
+ const [position, setPosition] = useState<Crop.Position>({
31
28
  x: props.x,
32
29
  y: props.y
33
30
  });
@@ -35,7 +32,7 @@ export default function FocalPoint(props: FocalPointProps) {
35
32
  const containerRef = useRef<HTMLDivElement>();
36
33
  const pointRef = useRef<HTMLDivElement>();
37
34
 
38
- const dragStart = (evt: Event) => {
35
+ const dragStart = (evt: MouseEvent | TouchEvent) => {
39
36
  evt.preventDefault();
40
37
  evt.stopPropagation();
41
38
  if (evt.target == pointRef.current) {
@@ -50,7 +47,7 @@ export default function FocalPoint(props: FocalPointProps) {
50
47
  }
51
48
  };
52
49
 
53
- const drag = (evt: TouchEvent | MouseEvent) => {
50
+ const drag = (evt: MouseEvent | TouchEvent) => {
54
51
  if (dragging) {
55
52
  let x: number, y: number;
56
53
  const containerSize = containerRef.current.getBoundingClientRect();
@@ -59,7 +56,7 @@ export default function FocalPoint(props: FocalPointProps) {
59
56
  if ("touches" in evt && evt.type == "touchmove") {
60
57
  x = evt.touches[0].clientX - (containerSize.x || containerSize.left);
61
58
  y = evt.touches[0].clientY - (containerSize.y || containerSize.top);
62
- } else {
59
+ } else if ("clientX" in evt) {
63
60
  x = evt.clientX - (containerSize.x || containerSize.left);
64
61
  y = evt.clientY - (containerSize.y || containerSize.top);
65
62
  }
@@ -1,19 +1,21 @@
1
1
  import React from "react";
2
2
  import ReactCrop from "react-image-crop";
3
3
 
4
- import { cropSize, CropSize, CropState, Position, Size } from "./useCrop";
4
+ import * as Crop from "../../types/Crop";
5
+
6
+ import { cropSize } from "./useCrop";
5
7
  import FocalPoint from "./FocalPoint";
6
8
 
7
- interface ImageProps {
8
- containerSize: Size;
9
+ interface Props {
10
+ containerSize: Crop.Size;
9
11
  croppedImage: string;
10
- cropState: CropState;
11
- focalPoint: Position;
12
- setCrop: (crop: CropSize) => void;
13
- setFocal: (focal: Position) => void;
12
+ cropState: Crop.State;
13
+ focalPoint: Crop.Position;
14
+ setCrop: (crop: Crop.CropSize) => void;
15
+ setFocal: (focal: Crop.Position) => void;
14
16
  }
15
17
 
16
- export default function Image(props: ImageProps) {
18
+ export default function Image(props: Props) {
17
19
  const imageSize = () => {
18
20
  const { image, cropping, crop_width, crop_height } = props.cropState;
19
21
  if (cropping) {
@@ -1,22 +1,22 @@
1
- import React from "react";
1
+ import React, { MouseEvent } from "react";
2
2
 
3
- import { ImageResource } from "../../types";
4
- import { CropState } from "./useCrop";
3
+ import * as Crop from "../../types/Crop";
4
+ import * as Images from "../../types/Images";
5
5
 
6
6
  type Ratio = number | null;
7
7
 
8
- interface ToolbarProps {
9
- cropState: CropState;
10
- image: ImageResource;
8
+ interface Props {
9
+ cropState: Crop.State;
10
+ image: Images.Resource;
11
11
  setAspect: (Ratio) => void;
12
- toggleCrop: (evt: Event) => void;
13
- toggleFocal: (evt: Event) => void;
12
+ toggleCrop: (evt: MouseEvent) => void;
13
+ toggleFocal: (evt: MouseEvent) => void;
14
14
  }
15
15
 
16
- export default function Toolbar(props: ToolbarProps) {
16
+ export default function Toolbar(props: Props) {
17
17
  const { cropping } = props.cropState;
18
18
 
19
- const aspectRatios = [
19
+ const aspectRatios: Array<[string, Ratio]> = [
20
20
  ["Free", null],
21
21
  ["1:1", 1],
22
22
  ["3:2", 3 / 2],
@@ -28,7 +28,7 @@ export default function Toolbar(props: ToolbarProps) {
28
28
  ["16:9", 16 / 9]
29
29
  ];
30
30
 
31
- const updateAspect = (ratio: Ratio) => (evt: Event) => {
31
+ const updateAspect = (ratio: Ratio) => (evt: MouseEvent) => {
32
32
  evt.preventDefault();
33
33
  props.setAspect(ratio);
34
34
  };
@@ -61,7 +61,6 @@ export default function Toolbar(props: ToolbarProps) {
61
61
  href={props.image.original_url}
62
62
  className="button"
63
63
  title="Download original image"
64
- disabled={cropping}
65
64
  download={props.image.filename}
66
65
  onClick={(evt) => cropping && evt.preventDefault()}>
67
66
  <i className="fa-solid fa-download" />
@@ -1,46 +1,9 @@
1
1
  import { useEffect, useReducer, useState } from "react";
2
2
 
3
- import { ImageResource } from "../../types";
3
+ import * as Crop from "../../types/Crop";
4
+ import * as Images from "../../types/Images";
4
5
 
5
- export interface Position {
6
- x: number;
7
- y: number;
8
- }
9
-
10
- export interface Size {
11
- width: number;
12
- height: number;
13
- }
14
-
15
- interface CropParams {
16
- crop_start_x: number;
17
- crop_start_y: number;
18
- crop_width: number;
19
- crop_height: number;
20
- crop_gravity_x: number;
21
- crop_gravity_y: number;
22
- }
23
-
24
- export interface CropState extends CropParams {
25
- aspect: number | null;
26
- cropping: boolean;
27
- image: ImageResource;
28
- }
29
-
30
- export interface CropSize {
31
- x: number;
32
- y: number;
33
- width: number;
34
- height: number;
35
- aspect?: number;
36
- }
37
-
38
- export interface CropAction {
39
- type: string;
40
- payload?: CropSize | Position;
41
- }
42
-
43
- function applyAspect(state: CropState, aspect: number | null) {
6
+ function applyAspect(state: Crop.State, aspect: number | null) {
44
7
  const crop = cropSize(state);
45
8
  const image = state.image;
46
9
  const imageAspect = image.real_width / image.real_height;
@@ -65,7 +28,7 @@ function applyAspect(state: CropState, aspect: number | null) {
65
28
  return applyCrop(state, crop);
66
29
  }
67
30
 
68
- function applyCrop(state: CropState, crop: CropSize) {
31
+ function applyCrop(state: Crop.State, crop: Crop.CropSize) {
69
32
  const { image } = state;
70
33
 
71
34
  // Don't crop if dimensions are below the threshold
@@ -86,7 +49,14 @@ function applyCrop(state: CropState, crop: CropSize) {
86
49
  };
87
50
  }
88
51
 
89
- function cropReducer(state: CropState, action: CropAction): CropState {
52
+ function setFocal(state: Crop.State, position: Crop.Position) {
53
+ return {
54
+ crop_gravity_x: state.crop_width * (position.x / 100) + state.crop_start_x,
55
+ crop_gravity_y: state.crop_height * (position.y / 100) + state.crop_start_y
56
+ };
57
+ }
58
+
59
+ function cropReducer(state: Crop.State, action: Crop.Action): Crop.State {
90
60
  const {
91
61
  crop_start_x,
92
62
  crop_start_y,
@@ -119,11 +89,7 @@ function cropReducer(state: CropState, action: CropAction): CropState {
119
89
  case "setAspect":
120
90
  return { ...state, ...applyAspect(state, action.payload) };
121
91
  case "setFocal":
122
- return {
123
- ...state,
124
- crop_gravity_x: crop_width * (action.payload.x / 100) + crop_start_x,
125
- crop_gravity_y: crop_height * (action.payload.y / 100) + crop_start_y
126
- };
92
+ return { ...state, ...setFocal(state, action.payload) };
127
93
  case "startCrop":
128
94
  return { ...state, cropping: true };
129
95
  case "toggleFocal":
@@ -140,7 +106,10 @@ function cropReducer(state: CropState, action: CropAction): CropState {
140
106
  }
141
107
  }
142
108
 
143
- function croppedImageCanvas(img: HTMLImageElement, crop: CropSize) {
109
+ function croppedImageCanvas(
110
+ img: HTMLImageElement,
111
+ crop: Crop.CropSize
112
+ ): [HTMLCanvasElement, CanvasRenderingContext2D] {
144
113
  const canvas = document.createElement("canvas");
145
114
  canvas.width = img.naturalWidth * (crop.width / 100);
146
115
  canvas.height = img.naturalHeight * (crop.height / 100);
@@ -172,13 +141,13 @@ function imageDataUrl(
172
141
  return canvas.toDataURL("image/jpeg");
173
142
  }
174
143
 
175
- export function cropParams(state: CropState): CropParams {
144
+ export function cropParams(state: Crop.State): Crop.Params {
176
145
  const maybe = (func: (number) => number) => (val: number | null) =>
177
146
  val === null ? val : func(val);
178
147
  const maybeRound = maybe(Math.round);
179
148
  const maybeCeil = maybe(Math.ceil);
180
149
 
181
- const crop: CropParams = {
150
+ const crop: Crop.Params = {
182
151
  crop_start_x: maybeRound(state.crop_start_x),
183
152
  crop_start_y: maybeRound(state.crop_start_y),
184
153
  crop_width: maybeCeil(state.crop_width),
@@ -198,7 +167,7 @@ export function cropParams(state: CropState): CropParams {
198
167
  return crop;
199
168
  }
200
169
 
201
- export function cropSize(state: CropState): CropSize {
170
+ export function cropSize(state: Crop.State): Crop.CropSize {
202
171
  const { image, aspect, crop_start_x, crop_start_y, crop_width, crop_height } =
203
172
  state;
204
173
  const imageAspect = image.real_width / image.real_height;
@@ -221,8 +190,10 @@ export function cropSize(state: CropState): CropSize {
221
190
  }
222
191
  }
223
192
 
224
- export default function useCrop(image: ImageResource) {
225
- const initialState: CropState = {
193
+ export default function useCrop(
194
+ image: Images.Resource
195
+ ): [Crop.State, (action: Crop.Action) => void, string] {
196
+ const initialState: Crop.State = {
226
197
  aspect: null,
227
198
  cropping: false,
228
199
  crop_start_x: image.crop_start_x || 0,
@@ -1,24 +1,19 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
 
3
+ import * as Crop from "../types/Crop";
4
+
3
5
  import Image from "./ImageCropper/Image";
4
6
  import Toolbar from "./ImageCropper/Toolbar";
5
7
 
6
- import {
7
- CropAction,
8
- CropSize,
9
- CropState,
10
- Position
11
- } from "./ImageCropper/useCrop";
12
-
13
8
  export { default as useCrop, cropParams } from "./ImageCropper/useCrop";
14
9
 
15
- interface ImageCropperProps {
10
+ interface Props {
16
11
  croppedImage: string;
17
- cropState: CropState;
18
- dispatch: (action: CropAction) => void;
12
+ cropState: Crop.State;
13
+ dispatch: (action: Crop.Action) => void;
19
14
  }
20
15
 
21
- function focalPoint(state: CropState): Position {
16
+ function focalPoint(state: Crop.State): Crop.Position {
22
17
  if (state.crop_gravity_x === null || state.crop_gravity_y === null) {
23
18
  return null;
24
19
  } else {
@@ -29,9 +24,9 @@ function focalPoint(state: CropState): Position {
29
24
  }
30
25
  }
31
26
 
32
- export default function ImageCropper(props: ImageCropperProps) {
27
+ export default function ImageCropper(props: Props) {
33
28
  const containerRef = useRef<HTMLDivElement>();
34
- const [containerSize, setContainerSize] = useState();
29
+ const [containerSize, setContainerSize] = useState<Crop.Size>();
35
30
 
36
31
  const handleResize = () => {
37
32
  const elem = containerRef.current;
@@ -56,11 +51,11 @@ export default function ImageCropper(props: ImageCropperProps) {
56
51
  props.dispatch({ type: "setAspect", payload: aspect });
57
52
  };
58
53
 
59
- const setCrop = (crop: CropSize) => {
54
+ const setCrop = (crop: Crop.CropSize) => {
60
55
  props.dispatch({ type: "setCrop", payload: crop });
61
56
  };
62
57
 
63
- const setFocal = (focal: Position) => {
58
+ const setFocal = (focal: Crop.Position) => {
64
59
  props.dispatch({ type: "setFocal", payload: focal });
65
60
  };
66
61
 
@@ -1,28 +1,30 @@
1
- import React, { ChangeEvent } from "react";
1
+ import React, { ChangeEvent, MouseEvent } from "react";
2
+
2
3
  import useModalStore from "../../stores/useModalStore";
3
4
  import useToastStore from "../../stores/useToastStore";
4
- import { Locale, ImageResource } from "../../types";
5
5
  import copyToClipboard, { copySupported } from "../../lib/copyToClipboard";
6
+ import * as Images from "../../types/Images";
7
+ import { Locale } from "../../types";
6
8
 
7
- interface FormProps {
9
+ interface Props {
8
10
  alternative: Record<string, string>;
9
11
  caption: Record<string, string>;
10
- image: ImageResource;
12
+ image: Images.Resource;
11
13
  locale: string;
12
14
  locales: Record<string, Locale>;
13
15
  setLocale: (locale: string) => void;
14
- save: (evt: Event) => void;
16
+ save: (evt: MouseEvent) => void;
15
17
  showCaption: boolean;
16
18
  updateLocalization: (name: "alternative" | "caption", value: string) => void;
17
19
  }
18
20
 
19
- export default function Form(props: FormProps) {
21
+ export default function Form(props: Props) {
20
22
  const { alternative, caption, image, locale, locales } = props;
21
23
 
22
24
  const closeModal = useModalStore((state) => state.close);
23
25
  const notice = useToastStore((state) => state.notice);
24
26
 
25
- const copyEmbedCode = (evt: Event) => {
27
+ const copyEmbedCode = (evt: MouseEvent) => {
26
28
  evt.preventDefault();
27
29
  copyToClipboard(`[image:${image.id}]`);
28
30
  notice("Embed code copied to clipboard");
@@ -86,7 +88,9 @@ export default function Form(props: FormProps) {
86
88
  </div>
87
89
  )}
88
90
  <div className="buttons">
89
- <button onClick={props.save}>Save</button>
91
+ <button className="primary" onClick={props.save}>
92
+ Save
93
+ </button>
90
94
  <button onClick={closeModal}>Cancel</button>
91
95
  </div>
92
96
  </form>
@@ -1,20 +1,25 @@
1
- import React, { useState } from "react";
1
+ import React, { MouseEvent, useState } from "react";
2
+
2
3
  import useModalStore from "../stores/useModalStore";
3
4
  import { putJson } from "../lib/request";
5
+ import * as Images from "../types/Images";
6
+ import { Locale } from "../types";
4
7
 
5
- import { Locale, ImageResource } from "../types";
6
8
  import ImageCropper, { useCrop, cropParams } from "./ImageCropper";
7
9
  import Form from "./ImageEditor/Form";
8
10
 
9
- interface ImageEditorProps {
10
- image: ImageResource;
11
+ interface Props {
12
+ image: Images.Resource;
11
13
  caption: boolean;
12
14
  locale: string;
13
15
  locales: Record<string, Locale>;
14
- onUpdate?: (data: ImageResource, croppedImage: string | null) => void;
16
+ onUpdate?: (
17
+ data: Partial<Images.Resource>,
18
+ croppedImage: string | null
19
+ ) => void;
15
20
  }
16
21
 
17
- export default function ImageEditor(props: ImageEditorProps) {
22
+ export default function ImageEditor(props: Props) {
18
23
  const [cropState, dispatch, croppedImage] = useCrop(props.image);
19
24
  const [locale, setLocale] = useState(props.locale);
20
25
  const [localizations, setLocalizations] = useState({
@@ -34,7 +39,7 @@ export default function ImageEditor(props: ImageEditorProps) {
34
39
  });
35
40
  };
36
41
 
37
- const save = (evt: Event) => {
42
+ const save = (evt: MouseEvent) => {
38
43
  evt.preventDefault();
39
44
  evt.stopPropagation();
40
45
 
@@ -1,20 +1,21 @@
1
1
  import React, { RefObject } from "react";
2
2
 
3
- import { ImageResource } from "../../types";
4
- import { DragState } from "../drag";
3
+ import * as Drag from "../../types/Drag";
4
+ import * as Images from "../../types/Images";
5
5
 
6
- interface DragElementProps {
6
+ interface Props {
7
7
  container: RefObject<HTMLDivElement>;
8
- draggable: string | { record: { image: ImageResource; src?: string } };
9
- dragState: DragState;
8
+ draggable: Drag.Item<Images.Record>;
9
+ dragState: Drag.State;
10
10
  }
11
11
 
12
- export default function DragElement(props: DragElementProps) {
12
+ export default function DragElement(props: Props) {
13
13
  const { draggable, dragState, container } = props;
14
14
 
15
15
  if (draggable === "Files") {
16
16
  return "";
17
- } else {
17
+ } else if (typeof draggable !== "string") {
18
+ const record = draggable.record;
18
19
  const containerSize = container.current.getBoundingClientRect();
19
20
  const x = dragState.x - (containerSize.x || containerSize.left);
20
21
  const y = dragState.y - (containerSize.y || containerSize.top);
@@ -23,11 +24,7 @@ export default function DragElement(props: DragElementProps) {
23
24
  };
24
25
  return (
25
26
  <div className="drag-image" style={translateStyle}>
26
- {"record" in draggable && draggable.record.image && (
27
- <img
28
- src={draggable.record.src || draggable.record.image.thumbnail_url}
29
- />
30
- )}
27
+ {record.image && <img src={record.src || record.image.thumbnail_url} />}
31
28
  </div>
32
29
  );
33
30
  }