Initial commit
This commit is contained in:
42
Client/Css/InMemoriam/scss/bootstrap.scss
vendored
Normal file
42
Client/Css/InMemoriam/scss/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
||||
// Custom.scss
|
||||
// Option B: Include parts of Bootstrap
|
||||
|
||||
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
|
||||
@import "../../../../node_modules/bootstrap/scss/functions";
|
||||
|
||||
// 2. Include any default variable overrides here
|
||||
// $body-bg: #383E42;
|
||||
// $color-contrast-dark: #fff;
|
||||
// $color-contrast-light: #383E42;
|
||||
// $light: #383E42;
|
||||
// $dark: #fff;
|
||||
|
||||
// 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
|
||||
@import "../../../../node_modules/bootstrap/scss/variables";
|
||||
@import "../../../../node_modules/bootstrap/scss/variables-dark";
|
||||
|
||||
// 4. Include any default map overrides here
|
||||
|
||||
// 5. Include remainder of required parts
|
||||
@import "../../../../node_modules/bootstrap/scss/maps";
|
||||
@import "../../../../node_modules/bootstrap/scss/mixins";
|
||||
@import "../../../../node_modules/bootstrap/scss/root";
|
||||
|
||||
// 6. Optionally include any other parts as needed
|
||||
@import "../../../../node_modules/bootstrap/scss/utilities";
|
||||
@import "../../../../node_modules/bootstrap/scss/reboot";
|
||||
@import "../../../../node_modules/bootstrap/scss/type";
|
||||
@import "../../../../node_modules/bootstrap/scss/images";
|
||||
@import "../../../../node_modules/bootstrap/scss/containers";
|
||||
@import "../../../../node_modules/bootstrap/scss/grid";
|
||||
@import "../../../../node_modules/bootstrap/scss/helpers";
|
||||
|
||||
// 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
|
||||
@import "../../../../node_modules/bootstrap/scss/utilities/api";
|
||||
|
||||
// 8. Add additional custom code here
|
||||
[data-bs-theme="dark"] {
|
||||
--bs-body-bg: #383E42;
|
||||
}
|
||||
|
||||
98
Client/Css/InMemoriam/scss/inpictures.scss
Normal file
98
Client/Css/InMemoriam/scss/inpictures.scss
Normal file
@@ -0,0 +1,98 @@
|
||||
.pictures {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 222px);
|
||||
column-gap: 20px;
|
||||
width: 100%;
|
||||
.picture {
|
||||
border: 1px solid #ccc;
|
||||
min-height: 300px;
|
||||
padding: 10px;
|
||||
background-color: rgb(96 139 168 / 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #777;
|
||||
padding-top: 6px;
|
||||
.buttons {
|
||||
a {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picture-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
transition: opacity 0.3s;
|
||||
z-index: 1000;
|
||||
padding: 3vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.top {
|
||||
height: 3em;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.close {
|
||||
cursor: pointer;
|
||||
font-size: 2em;
|
||||
color: white;
|
||||
}
|
||||
.title {
|
||||
color: white;
|
||||
font-size: 2em;
|
||||
height: 2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.overlay-container {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-columns: 70% 30%;
|
||||
.picture {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottom {
|
||||
height: 3em;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.previous {
|
||||
cursor: pointer;
|
||||
font-size: 2em;
|
||||
color: white;
|
||||
}
|
||||
.next {
|
||||
cursor: pointer;
|
||||
font-size: 2em;
|
||||
color: white;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Client/Css/InMemoriam/scss/instories.scss
Normal file
29
Client/Css/InMemoriam/scss/instories.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
.stories {
|
||||
width: 100%;
|
||||
.story {
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
.title {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
.date {
|
||||
font-size: 0.8em;
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #777;
|
||||
padding-top: 6px;
|
||||
.buttons {
|
||||
a {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
height: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Client/Css/InMemoriam/scss/messages.scss
Normal file
29
Client/Css/InMemoriam/scss/messages.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
.messages {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 350px);
|
||||
column-gap: 20px;
|
||||
width: 100%;
|
||||
.message {
|
||||
border: 1px solid #ccc;
|
||||
min-height: 300px;
|
||||
padding: 10px;
|
||||
background-color: rgb(96 139 168 / 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.content {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-style: italic;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #777;
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Client/Css/InMemoriam/scss/styles.scss
Normal file
98
Client/Css/InMemoriam/scss/styles.scss
Normal file
@@ -0,0 +1,98 @@
|
||||
@import "./bootstrap";
|
||||
@import "./inpictures.scss";
|
||||
@import "./instories.scss";
|
||||
@import "./messages.scss";
|
||||
|
||||
body {
|
||||
font-family: "Quicksand", serif !important;
|
||||
font-optical-sizing: auto;
|
||||
}
|
||||
|
||||
.home-dates {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
h2 {
|
||||
margin-top: auto;
|
||||
font-size: 4em;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
#dnn_MenuPane,
|
||||
header > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
ul.homemenu,
|
||||
ul.mainmenu,
|
||||
ul.rightmenu {
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
padding-left: 0;
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
li.active {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ul.rightmenu {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
div.language-object {
|
||||
display: inline;
|
||||
li {
|
||||
a {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#loginContainer,
|
||||
#registrationContainer,
|
||||
.doublePane,
|
||||
.triplePane {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#loginContainer {
|
||||
padding-top: 200px;
|
||||
> div {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
#registrationContainer {
|
||||
padding-top: 100px;
|
||||
> div {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
.dnnHelperTip {
|
||||
display: none;
|
||||
}
|
||||
.password-strength-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.dnnFormMessage.dnnFormError {
|
||||
background-color: transparent;
|
||||
color: red;
|
||||
padding: 0;
|
||||
}
|
||||
ul.dnnActions {
|
||||
list-style-type: none;
|
||||
li {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
div.defaultContainer {
|
||||
padding: 20px;
|
||||
}
|
||||
36
Client/Css/InMemoriam/webpack.config.js
Normal file
36
Client/Css/InMemoriam/webpack.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
var path = require("path"),
|
||||
MiniCssExtractPlugin = require("mini-css-extract-plugin"),
|
||||
FileManagerPlugin = require("filemanager-webpack-plugin");
|
||||
|
||||
var outPath = path.resolve(__dirname, "../../../Themes/Skins/InMemoriamSkin");
|
||||
|
||||
var inmemoriamAppConfig = {
|
||||
context: path.join(__dirname, "."),
|
||||
entry: "./scss/styles.scss",
|
||||
output: {
|
||||
path: outPath,
|
||||
filename: "skin.css.js"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "skin.css"
|
||||
}),
|
||||
new FileManagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
delete: [outPath + "/skin.css.js"],
|
||||
},
|
||||
},
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = inmemoriamAppConfig;
|
||||
9
Client/Js/InMemoriam/App.tsx
Normal file
9
Client/Js/InMemoriam/App.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { AppManager } from "./AppManager";
|
||||
import { ComponentLoader } from "./ComponentLoader";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
AppManager.loadData();
|
||||
ComponentLoader.load();
|
||||
});
|
||||
52
Client/Js/InMemoriam/AppManager.ts
Normal file
52
Client/Js/InMemoriam/AppManager.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { AppModule, IAppModule } from "./Models/IAppModule";
|
||||
import { KeyedCollection } from "./Models/IKeyedCollection";
|
||||
import DataService from "./Service";
|
||||
|
||||
declare global {
|
||||
interface Element {
|
||||
dataInt: (prop: string) => number;
|
||||
dataString: (prop: string, defaultValue: string) => string;
|
||||
dataObject: (prop: string) => any;
|
||||
}
|
||||
}
|
||||
|
||||
Element.prototype.dataInt = function (this: Element, prop: string): number {
|
||||
if (this.getAttribute("data-" + prop) == null) return 0;
|
||||
return parseInt(this.getAttribute("data-" + prop) as string);
|
||||
};
|
||||
|
||||
Element.prototype.dataString = function (
|
||||
this: Element,
|
||||
prop: string,
|
||||
defaultValue: string
|
||||
): string {
|
||||
if (this.getAttribute("data-" + prop) == null) return defaultValue;
|
||||
return this.getAttribute("data-" + prop) as string;
|
||||
};
|
||||
|
||||
Element.prototype.dataObject = function (this: Element, prop: string): any {
|
||||
if (this.getAttribute("data-" + prop) == null) return null;
|
||||
return JSON.parse(this.getAttribute("data-" + prop) as string);
|
||||
};
|
||||
|
||||
export class AppManager {
|
||||
public static Modules = new KeyedCollection<IAppModule>();
|
||||
|
||||
public static loadData(): void {
|
||||
document.querySelectorAll(".Bring2mindInMemoriam").forEach((el) => {
|
||||
var moduleId = el.dataInt("moduleid");
|
||||
AppManager.Modules.Add(
|
||||
moduleId.toString(),
|
||||
new AppModule(
|
||||
moduleId,
|
||||
el.dataInt("tabid"),
|
||||
el.dataString("locale", "en-US"),
|
||||
el.dataObject("resources"),
|
||||
el.dataObject("common"),
|
||||
el.dataObject("security"),
|
||||
new DataService(moduleId)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
33
Client/Js/InMemoriam/ComponentLoader.tsx
Normal file
33
Client/Js/InMemoriam/ComponentLoader.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { AppManager } from "./AppManager";
|
||||
import InPicturesPage from "./Components/InPictures/InPicturesPage";
|
||||
import InStoriesPage from "./Components/InStories/InStoriesPage";
|
||||
import MessagesPage from "./Components/Messages/MessagesPage";
|
||||
|
||||
export class ComponentLoader {
|
||||
public static load(): void {
|
||||
document.querySelectorAll(".InPictures").forEach((el) => {
|
||||
const root = createRoot(el);
|
||||
const moduleId = el.dataInt("moduleid");
|
||||
root.render(
|
||||
<InPicturesPage module={AppManager.Modules.Item(moduleId.toString())} />
|
||||
);
|
||||
});
|
||||
document.querySelectorAll(".InWords").forEach((el) => {
|
||||
const root = createRoot(el);
|
||||
const moduleId = el.dataInt("moduleid");
|
||||
root.render(
|
||||
<InStoriesPage module={AppManager.Modules.Item(moduleId.toString())} />
|
||||
);
|
||||
});
|
||||
document.querySelectorAll(".Messages").forEach((el) => {
|
||||
const root = createRoot(el);
|
||||
const moduleId = el.dataInt("moduleid");
|
||||
root.render(
|
||||
<MessagesPage module={AppManager.Modules.Item(moduleId.toString())} />
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
209
Client/Js/InMemoriam/Components/InPictures/EditImage.tsx
Normal file
209
Client/Js/InMemoriam/Components/InPictures/EditImage.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { Modal, Form } from "react-bootstrap";
|
||||
import { IPicture } from "../../Models/IPicture";
|
||||
|
||||
interface IEditImageProps {
|
||||
module: IAppModule;
|
||||
picture: IPicture;
|
||||
shown: boolean;
|
||||
dismiss: () => void;
|
||||
editedPicture: (editedPicture: IPicture | null) => void;
|
||||
}
|
||||
|
||||
const EditImage: React.FC<IEditImageProps> = (props) => {
|
||||
const [title, setTitle] = React.useState(props.picture.Title);
|
||||
const [description, setDescription] = React.useState(
|
||||
props.picture.Description
|
||||
);
|
||||
const [visibility, setVisibility] = React.useState(props.picture.Visibility); // 0 = public, 1 = friends, 2 = private
|
||||
const [pictureYear, setPictureYear] = React.useState(
|
||||
props.picture.PictureYear < 1 ? "" : props.picture.PictureYear.toString()
|
||||
);
|
||||
const [pictureMonth, setPictureMonth] = React.useState(
|
||||
props.picture.PictureMonth < 1 ? "" : props.picture.PictureMonth.toString()
|
||||
);
|
||||
const [pictureDay, setPictureDay] = React.useState(
|
||||
props.picture.PictureDay < 1 ? "" : props.picture.PictureDay.toString()
|
||||
);
|
||||
|
||||
const pictureYearIsValid =
|
||||
pictureYear.length === 0 || /^\d{4}$/.test(pictureYear);
|
||||
const pictureMonthIsValid =
|
||||
pictureMonth.length === 0 || /^\d{1,2}$/.test(pictureMonth);
|
||||
const pictureDayIsValid =
|
||||
pictureDay.length === 0 || /^\d{1,2}$/.test(pictureDay);
|
||||
const formIsValid =
|
||||
pictureYearIsValid &&
|
||||
pictureMonthIsValid &&
|
||||
pictureDayIsValid &&
|
||||
title !== "";
|
||||
|
||||
return (
|
||||
<Modal show={props.shown} onHide={props.dismiss}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.module.resources.AddImage}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
<img
|
||||
src={
|
||||
props.module.service.baseServicepath +
|
||||
"Pictures/Get?id=" +
|
||||
props.picture.ImageIdentifier +
|
||||
"&width=200&height=200&method=c&moduleId=" +
|
||||
props.module.moduleId +
|
||||
"&tabId=" +
|
||||
props.module.service.tabId
|
||||
}
|
||||
alt={props.picture.Title}
|
||||
/>
|
||||
</div>
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Title}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
isValid={title !== ""}
|
||||
isInvalid={title === ""}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="description" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Description}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={description}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Visibility}</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={visibility}
|
||||
onChange={(e) => {
|
||||
setVisibility(parseInt(e.target.value));
|
||||
}}
|
||||
>
|
||||
<option value="0">{props.module.resources.Public}</option>
|
||||
<option value="1">{props.module.resources.Friends}</option>
|
||||
<option value="2">{props.module.resources.Private}</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Date}</Form.Label>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Year}
|
||||
value={pictureYear}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureYear(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureYearIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Month}
|
||||
value={pictureMonth}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureMonth(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureMonthIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Day}
|
||||
value={pictureDay}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureDay(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureDayIsValid}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.dismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Cancel}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (confirm(props.module.resources.DeleteConfirm) === false) {
|
||||
return;
|
||||
}
|
||||
props.module.service.deletePicture(
|
||||
props.picture.PictureId,
|
||||
() => {
|
||||
// Success
|
||||
props.editedPicture(null);
|
||||
props.dismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
// Error
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
props.dismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Delete}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={formIsValid === false}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const picture = {
|
||||
...props.picture,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Visibility: visibility,
|
||||
PictureYear: pictureYear === "" ? -1 : parseInt(pictureYear),
|
||||
PictureMonth: pictureMonth === "" ? -1 : parseInt(pictureMonth),
|
||||
PictureDay: pictureDay === "" ? -1 : parseInt(pictureDay),
|
||||
};
|
||||
props.module.service.editPicture(
|
||||
picture,
|
||||
(picture: IPicture) => {
|
||||
// Success
|
||||
props.editedPicture(picture);
|
||||
props.dismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
// Error
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
props.dismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Save}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditImage;
|
||||
139
Client/Js/InMemoriam/Components/InPictures/InPicturesPage.tsx
Normal file
139
Client/Js/InMemoriam/Components/InPictures/InPicturesPage.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import NewImage from "./NewImage";
|
||||
import { IPicture } from "../../Models/IPicture";
|
||||
import Picture from "./Picture";
|
||||
import EditImage from "./EditImage";
|
||||
import PictureOverlay from "./PictureOverlay";
|
||||
|
||||
interface IInPicturesPageProps {
|
||||
module: IAppModule;
|
||||
}
|
||||
|
||||
const InPicturesPage: React.FC<IInPicturesPageProps> = (props) => {
|
||||
const [showNewImage, setShowNewImage] = React.useState(false);
|
||||
const [showEditImage, setShowEditImage] = React.useState(false);
|
||||
const [pictures, setPictures] = React.useState<IPicture[]>([]);
|
||||
const [pictureInEdit, setPictureInEdit] = React.useState<IPicture | null>(
|
||||
null
|
||||
);
|
||||
const [selectedPictureIndex, setSelectedPictureIndex] = React.useState(-1);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
console.log(e.key, selectedPictureIndex, pictures.length);
|
||||
if (e.key === "Escape") {
|
||||
setSelectedPictureIndex(-1);
|
||||
} else if (
|
||||
e.key === "ArrowRight" &&
|
||||
selectedPictureIndex < pictures.length - 1
|
||||
) {
|
||||
setSelectedPictureIndex(selectedPictureIndex + 1);
|
||||
} else if (e.key === "ArrowLeft" && selectedPictureIndex > 0) {
|
||||
setSelectedPictureIndex(selectedPictureIndex - 1);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyPress);
|
||||
return () => window.removeEventListener("keydown", handleKeyPress);
|
||||
}, [selectedPictureIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
props.module.service.getPictures(
|
||||
(data) => {
|
||||
setPictures(data);
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setShowEditImage(pictureInEdit !== null);
|
||||
}, [pictureInEdit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.module.security.CanAdd && (
|
||||
<div className="d-flex flex-row-reverse">
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowNewImage(true);
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Add}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="pictures">
|
||||
{pictures.map((picture, index) => {
|
||||
return (
|
||||
<Picture
|
||||
key={index}
|
||||
module={props.module}
|
||||
picture={picture}
|
||||
onEdit={(picture) => {
|
||||
setPictureInEdit(picture);
|
||||
}}
|
||||
onClick={(picture) => {
|
||||
setSelectedPictureIndex(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<NewImage
|
||||
module={props.module}
|
||||
shown={showNewImage}
|
||||
dismiss={() => {
|
||||
setShowNewImage(false);
|
||||
}}
|
||||
addNewPicture={(picture) => {
|
||||
setPictures([...pictures, picture]);
|
||||
}}
|
||||
/>
|
||||
{pictureInEdit && (
|
||||
<EditImage
|
||||
module={props.module}
|
||||
picture={pictureInEdit}
|
||||
shown={showEditImage}
|
||||
dismiss={() => {
|
||||
setPictureInEdit(null);
|
||||
}}
|
||||
editedPicture={(picture) => {
|
||||
if (picture) {
|
||||
setPictures(
|
||||
pictures.map((p) => {
|
||||
return p.PictureId === picture.PictureId ? picture : p;
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selectedPictureIndex > -1 && (
|
||||
<PictureOverlay
|
||||
module={props.module}
|
||||
picture={pictures[selectedPictureIndex]}
|
||||
onClose={() => {
|
||||
setSelectedPictureIndex(-1);
|
||||
}}
|
||||
hasNext={selectedPictureIndex < pictures.length - 1}
|
||||
hasPrevious={selectedPictureIndex > 0}
|
||||
onNext={() => {
|
||||
setSelectedPictureIndex(selectedPictureIndex + 1);
|
||||
}}
|
||||
onPrevious={() => {
|
||||
setSelectedPictureIndex(selectedPictureIndex - 1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InPicturesPage;
|
||||
189
Client/Js/InMemoriam/Components/InPictures/NewImage.tsx
Normal file
189
Client/Js/InMemoriam/Components/InPictures/NewImage.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { Modal, Form } from "react-bootstrap";
|
||||
import { IPicture } from "../../Models/IPicture";
|
||||
|
||||
interface INewImageProps {
|
||||
module: IAppModule;
|
||||
shown: boolean;
|
||||
dismiss: () => void;
|
||||
addNewPicture: (newPicture: IPicture) => void;
|
||||
}
|
||||
|
||||
const NewImage: React.FC<INewImageProps> = (props) => {
|
||||
const [newFile, setNewFile] = React.useState<File | null>(null);
|
||||
const [title, setTitle] = React.useState("");
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [visibility, setVisibility] = React.useState(0); // 0 = public, 1 = friends, 2 = private
|
||||
const [pictureYear, setPictureYear] = React.useState("");
|
||||
const [pictureMonth, setPictureMonth] = React.useState("");
|
||||
const [pictureDay, setPictureDay] = React.useState("");
|
||||
|
||||
const clearFormAndDismiss = () => {
|
||||
setNewFile(null);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setVisibility(1);
|
||||
setPictureYear("");
|
||||
setPictureMonth("");
|
||||
setPictureDay("");
|
||||
props.dismiss();
|
||||
};
|
||||
|
||||
const pictureYearIsValid =
|
||||
pictureYear.length === 0 || /^\d{4}$/.test(pictureYear);
|
||||
const pictureMonthIsValid =
|
||||
pictureMonth.length === 0 || /^\d{1,2}$/.test(pictureMonth);
|
||||
const pictureDayIsValid =
|
||||
pictureDay.length === 0 || /^\d{1,2}$/.test(pictureDay);
|
||||
const formIsValid =
|
||||
pictureYearIsValid &&
|
||||
pictureMonthIsValid &&
|
||||
pictureDayIsValid &&
|
||||
newFile &&
|
||||
title !== "";
|
||||
|
||||
return (
|
||||
<Modal show={props.shown} onHide={props.dismiss}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.module.resources.AddImage}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form.Group controlId="formFile" className="mb-3">
|
||||
<Form.Label>{props.module.resources.SelectJpg}</Form.Label>
|
||||
<Form.Control
|
||||
type="file"
|
||||
accept=".jpg"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.item(0);
|
||||
if (file) {
|
||||
setNewFile(file);
|
||||
}
|
||||
}}
|
||||
isValid={newFile !== null}
|
||||
isInvalid={newFile === null}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Title}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
isValid={title !== ""}
|
||||
isInvalid={title === ""}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="description" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Description}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={description}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Visibility}</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={visibility}
|
||||
onChange={(e) => {
|
||||
setVisibility(parseInt(e.target.value));
|
||||
}}
|
||||
>
|
||||
<option value="0">{props.module.resources.Public}</option>
|
||||
<option value="1">{props.module.resources.Friends}</option>
|
||||
<option value="2">{props.module.resources.Private}</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Date}</Form.Label>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Year}
|
||||
value={pictureYear}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureYear(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureYearIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Month}
|
||||
value={pictureMonth}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureMonth(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureMonthIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Day}
|
||||
value={pictureDay}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setPictureDay(e.target.value);
|
||||
}}
|
||||
isInvalid={!pictureDayIsValid}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Cancel}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={formIsValid === false}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Do something with newFile, title, description, and visibility
|
||||
if (newFile !== null) {
|
||||
props.module.service.createPicture(
|
||||
newFile,
|
||||
title,
|
||||
description,
|
||||
visibility,
|
||||
pictureYear === "" ? -1 : parseInt(pictureYear),
|
||||
pictureMonth === "" ? -1 : parseInt(pictureMonth),
|
||||
pictureDay === "" ? -1 : parseInt(pictureDay),
|
||||
(newPicture: IPicture) => {
|
||||
// Success
|
||||
props.addNewPicture(newPicture);
|
||||
clearFormAndDismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
// Error
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Save}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewImage;
|
||||
86
Client/Js/InMemoriam/Components/InPictures/Picture.tsx
Normal file
86
Client/Js/InMemoriam/Components/InPictures/Picture.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IPicture } from "../../Models/IPicture";
|
||||
import Icon from "@mdi/react";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
|
||||
interface IPictureProps {
|
||||
module: IAppModule;
|
||||
picture: IPicture;
|
||||
onEdit: (picture: IPicture) => void;
|
||||
onClick: (picture: IPicture) => void;
|
||||
}
|
||||
|
||||
const Picture: React.FC<IPictureProps> = (props) => {
|
||||
const canEdit =
|
||||
props.module.security.IsFamily ||
|
||||
props.module.security.UserId === props.picture.CreatedByUserID;
|
||||
|
||||
let dt = "";
|
||||
if (props.picture.PictureYear > 0) {
|
||||
dt = props.picture.PictureYear.toString();
|
||||
if (props.picture.PictureMonth > 0) {
|
||||
const month = new Date(0, props.picture.PictureMonth - 1).toLocaleString(
|
||||
undefined,
|
||||
{ month: "long" }
|
||||
);
|
||||
dt = month + ", " + dt;
|
||||
if (props.picture.PictureDay > 0) {
|
||||
dt = props.picture.PictureDay.toString() + " " + dt;
|
||||
}
|
||||
}
|
||||
dt = "(" + dt + ")";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="picture"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClick(props.picture);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src={
|
||||
props.module.service.baseServicepath +
|
||||
"Pictures/Get?id=" +
|
||||
props.picture.ImageIdentifier +
|
||||
"&width=200&height=200&method=c&moduleId=" +
|
||||
props.module.moduleId +
|
||||
"&tabId=" +
|
||||
props.module.service.tabId
|
||||
}
|
||||
alt={props.picture.Title}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-100 text-body-secondary text-center text-small mt-1 overflow-hidden">
|
||||
{props.picture.Title} <span className="date">{dt}</span>
|
||||
</div>
|
||||
<div className="footer mt-3">
|
||||
<div>
|
||||
<small>
|
||||
<strong>by</strong> {props.picture.CreatedByUser}
|
||||
</small>
|
||||
</div>
|
||||
<div className="buttons">
|
||||
{canEdit && (
|
||||
<a
|
||||
href="#"
|
||||
className="btn btn-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.onEdit(props.picture);
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiPencil} size={0.5} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picture;
|
||||
107
Client/Js/InMemoriam/Components/InPictures/PictureOverlay.tsx
Normal file
107
Client/Js/InMemoriam/Components/InPictures/PictureOverlay.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IPicture } from "../../Models/IPicture";
|
||||
|
||||
interface IPictureOverlayProps {
|
||||
module: IAppModule;
|
||||
picture: IPicture;
|
||||
hasNext: boolean;
|
||||
hasPrevious: boolean;
|
||||
onClose: () => void;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
}
|
||||
|
||||
const PictureOverlay: React.FC<IPictureOverlayProps> = (props) => {
|
||||
let dt = "";
|
||||
if (props.picture.PictureYear > 0) {
|
||||
dt = props.picture.PictureYear.toString();
|
||||
if (props.picture.PictureMonth > 0) {
|
||||
const month = new Date(0, props.picture.PictureMonth - 1).toLocaleString(
|
||||
undefined,
|
||||
{ month: "long" }
|
||||
);
|
||||
dt = month + ", " + dt;
|
||||
if (props.picture.PictureDay > 0) {
|
||||
dt = props.picture.PictureDay.toString() + " " + dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="picture-overlay">
|
||||
<div className="top">
|
||||
<div className="title">{props.picture.Title}</div>
|
||||
<div className="close" onClick={props.onClose}>
|
||||
×
|
||||
</div>
|
||||
</div>
|
||||
<div className="overlay-container">
|
||||
<div className="picture">
|
||||
<img
|
||||
src={
|
||||
props.module.service.baseServicepath +
|
||||
"Pictures/Get?id=" +
|
||||
props.picture.ImageIdentifier +
|
||||
"&width=800&height=800&method=b&moduleId=" +
|
||||
props.module.moduleId +
|
||||
"&tabId=" +
|
||||
props.module.service.tabId
|
||||
}
|
||||
alt={props.picture.Title}
|
||||
/>
|
||||
</div>
|
||||
<div className="details">
|
||||
<div className="data">
|
||||
<strong>{props.module.resources.Created}:</strong>{" "}
|
||||
{new Intl.DateTimeFormat(undefined, { dateStyle: "short" }).format(
|
||||
new Date(props.picture.CreatedOnDate)
|
||||
)}{" "}
|
||||
<strong>{props.module.resources.By}</strong>{" "}
|
||||
{props.picture.CreatedByUser}
|
||||
</div>
|
||||
{dt !== "" && (
|
||||
<div className="data">
|
||||
<strong>{props.module.resources.Date}:</strong> {dt}
|
||||
</div>
|
||||
)}
|
||||
<div className="description">{props.picture.Description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom">
|
||||
<div className="previous">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!props.hasPrevious}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.hasPrevious) {
|
||||
props.onPrevious();
|
||||
}
|
||||
}}
|
||||
className="btn"
|
||||
>
|
||||
<
|
||||
</button>
|
||||
</div>
|
||||
<div className="next">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!props.hasNext}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.hasNext) {
|
||||
props.onNext();
|
||||
}
|
||||
}}
|
||||
className="btn"
|
||||
>
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PictureOverlay;
|
||||
189
Client/Js/InMemoriam/Components/InStories/EditStory.tsx
Normal file
189
Client/Js/InMemoriam/Components/InStories/EditStory.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IStory, Story } from "../../Models/IStory";
|
||||
import { Modal, Form } from "react-bootstrap";
|
||||
|
||||
interface IEditStoryProps {
|
||||
module: IAppModule;
|
||||
story: IStory;
|
||||
shown: boolean;
|
||||
dismiss: () => void;
|
||||
editedStory: (editedStory: IStory | null) => void;
|
||||
}
|
||||
|
||||
const EditStory: React.FC<IEditStoryProps> = (props) => {
|
||||
console.log(props.story);
|
||||
const [title, setTitle] = React.useState(props.story.Title);
|
||||
const [contents, setContents] = React.useState(props.story.Contents);
|
||||
const [visibility, setVisibility] = React.useState(props.story.Visibility); // 0 = public, 1 = friends, 2 = private
|
||||
const [storyYear, setStoryYear] = React.useState(
|
||||
props.story.StoryYear < 1 ? "" : props.story.StoryYear.toString()
|
||||
);
|
||||
const [storyMonth, setStoryMonth] = React.useState(
|
||||
props.story.StoryMonth < 1 ? "" : props.story.StoryMonth.toString()
|
||||
);
|
||||
const [storyDay, setStoryDay] = React.useState(
|
||||
props.story.StoryDay < 1 ? "" : props.story.StoryDay.toString()
|
||||
);
|
||||
console.log(title);
|
||||
|
||||
const clearFormAndDismiss = () => {
|
||||
setTitle("");
|
||||
setContents("");
|
||||
setVisibility(1);
|
||||
setStoryYear("");
|
||||
setStoryMonth("");
|
||||
setStoryDay("");
|
||||
props.dismiss();
|
||||
};
|
||||
|
||||
const storyYearIsValid = storyYear.length === 0 || /^\d{4}$/.test(storyYear);
|
||||
const storyMonthIsValid =
|
||||
storyMonth.length === 0 || /^\d{1,2}$/.test(storyMonth);
|
||||
const storyDayIsValid = storyDay.length === 0 || /^\d{1,2}$/.test(storyDay);
|
||||
|
||||
return (
|
||||
<Modal show={props.shown} onHide={props.dismiss} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.module.resources.EditStory}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Title}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="contents" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Contents}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={contents}
|
||||
rows={10}
|
||||
onChange={(e) => setContents(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Visibility}</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={visibility}
|
||||
onChange={(e) => setVisibility(parseInt(e.target.value))}
|
||||
>
|
||||
<option value="0">{props.module.resources.Public}</option>
|
||||
<option value="1">{props.module.resources.Friends}</option>
|
||||
<option value="2">{props.module.resources.Private}</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="visibility" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Date}</Form.Label>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Year}
|
||||
value={storyYear}
|
||||
onChange={(e) => setStoryYear(e.target.value)}
|
||||
isInvalid={!storyYearIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Month}
|
||||
value={storyMonth}
|
||||
onChange={(e) => setStoryMonth(e.target.value)}
|
||||
isInvalid={!storyMonthIsValid}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={props.module.resources.Day}
|
||||
value={storyDay}
|
||||
onChange={(e) => setStoryDay(e.target.value)}
|
||||
isInvalid={!storyDayIsValid}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Cancel}
|
||||
</button>
|
||||
{props.story && props.story.StoryId !== -1 && (
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (confirm(props.module.resources.DeleteConfirm) === false) {
|
||||
return;
|
||||
}
|
||||
props.module.service.deleteStory(
|
||||
(props.story as IStory).StoryId,
|
||||
() => {
|
||||
// Success
|
||||
props.editedStory(null);
|
||||
clearFormAndDismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
// Error
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Delete}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
if (storyYearIsValid && storyMonthIsValid && storyDayIsValid) {
|
||||
const story = {
|
||||
...new Story(),
|
||||
StoryId: props.story ? props.story.StoryId : -1,
|
||||
Title: title,
|
||||
Contents: contents,
|
||||
Visibility: visibility,
|
||||
StoryYear: storyYear.length === 0 ? 0 : parseInt(storyYear),
|
||||
StoryMonth: storyMonth.length === 0 ? 0 : parseInt(storyMonth),
|
||||
StoryDay: storyDay.length === 0 ? 0 : parseInt(storyDay),
|
||||
};
|
||||
props.module.service.editStory(
|
||||
story,
|
||||
(editedStory: IStory) => {
|
||||
props.editedStory(editedStory);
|
||||
clearFormAndDismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
clearFormAndDismiss();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Save}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditStory;
|
||||
84
Client/Js/InMemoriam/Components/InStories/InStoriesPage.tsx
Normal file
84
Client/Js/InMemoriam/Components/InStories/InStoriesPage.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IStory, Story as NewStory } from "../../Models/IStory";
|
||||
import Story from "./Story";
|
||||
import EditStory from "./EditStory";
|
||||
|
||||
interface IInStoriesPageProps {
|
||||
module: IAppModule;
|
||||
}
|
||||
|
||||
const InStoriesPage: React.FC<IInStoriesPageProps> = (props) => {
|
||||
const [showEditStory, setShowEditStory] = React.useState(false);
|
||||
const [stories, setStories] = React.useState<IStory[]>([]);
|
||||
const [storyInEdit, setStoryInEdit] = React.useState<IStory | null>(null);
|
||||
|
||||
const getStories = () => {
|
||||
props.module.service.getStories(
|
||||
(data) => {
|
||||
setStories(data);
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
getStories();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
setShowEditStory(storyInEdit !== null);
|
||||
}, [storyInEdit]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-row-reverse mb-4">
|
||||
{props.module.security.CanAdd && (
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setStoryInEdit(new NewStory());
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Add}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="stories">
|
||||
{stories.map((story, index) => {
|
||||
return (
|
||||
<Story
|
||||
key={index}
|
||||
module={props.module}
|
||||
story={story}
|
||||
onEdit={(story) => {
|
||||
setStoryInEdit(story);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{storyInEdit && (
|
||||
<EditStory
|
||||
module={props.module}
|
||||
shown={showEditStory}
|
||||
story={storyInEdit}
|
||||
editedStory={(story) => {
|
||||
setStoryInEdit(null);
|
||||
getStories();
|
||||
}}
|
||||
dismiss={() => {
|
||||
setStoryInEdit(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InStoriesPage;
|
||||
72
Client/Js/InMemoriam/Components/InStories/Story.tsx
Normal file
72
Client/Js/InMemoriam/Components/InStories/Story.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IStory } from "../../Models/IStory";
|
||||
import Icon from "@mdi/react";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import SanitizedHTML from "react-sanitized-html";
|
||||
|
||||
interface IStoryProps {
|
||||
module: IAppModule;
|
||||
story: IStory;
|
||||
onEdit: (story: IStory) => void;
|
||||
}
|
||||
|
||||
const Story: React.FC<IStoryProps> = (props) => {
|
||||
const canEdit =
|
||||
props.module.security.IsFamily ||
|
||||
props.module.security.UserId === props.story.CreatedByUserID;
|
||||
|
||||
let dt = "";
|
||||
if (props.story.StoryYear > 0) {
|
||||
dt = props.story.StoryYear.toString();
|
||||
if (props.story.StoryMonth > 0) {
|
||||
const month = new Date(0, props.story.StoryMonth - 1).toLocaleString(
|
||||
undefined,
|
||||
{ month: "long" }
|
||||
);
|
||||
dt = month + ", " + dt;
|
||||
if (props.story.StoryDay > 0) {
|
||||
dt = props.story.StoryDay.toString() + " " + dt;
|
||||
}
|
||||
}
|
||||
dt = "(" + dt + ")";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="story mb-4">
|
||||
<div className="title">
|
||||
{props.story.Title} <span className="date">{dt}</span>
|
||||
</div>
|
||||
<div className="content">
|
||||
<SanitizedHTML html={props.story.Contents.replace(/\n/g, "<br />")} />
|
||||
</div>
|
||||
<div className="footer mt-3">
|
||||
<div>
|
||||
<small>
|
||||
<strong>Created:</strong>{" "}
|
||||
{new Intl.DateTimeFormat(undefined, { dateStyle: "short" }).format(
|
||||
new Date(props.story.CreatedOnDate)
|
||||
)}{" "}
|
||||
<strong>by</strong> {props.story.CreatedByUser}
|
||||
</small>
|
||||
</div>
|
||||
<div className="buttons">
|
||||
{canEdit && (
|
||||
<a
|
||||
href="#"
|
||||
className="btn btn-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
props.onEdit(props.story);
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiPencil} size={0.5} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Story;
|
||||
113
Client/Js/InMemoriam/Components/Messages/CreateMessage.tsx
Normal file
113
Client/Js/InMemoriam/Components/Messages/CreateMessage.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IMessage, Message } from "../../Models/IMessage";
|
||||
import { Modal, Form } from "react-bootstrap";
|
||||
|
||||
interface ICreateMessageProps {
|
||||
module: IAppModule;
|
||||
shown: boolean;
|
||||
dismiss: () => void;
|
||||
addNewMessage: (newMessage: IMessage) => void;
|
||||
}
|
||||
|
||||
const CreateMessage: React.FC<ICreateMessageProps> = (props) => {
|
||||
const [contents, setContents] = React.useState("");
|
||||
const [senderName, setSenderName] = React.useState("");
|
||||
const [senderEmail, setSenderEmail] = React.useState("");
|
||||
|
||||
const clearFormAndDismiss = () => {
|
||||
setContents("");
|
||||
setSenderName("");
|
||||
setSenderEmail("");
|
||||
props.dismiss();
|
||||
};
|
||||
|
||||
const contentsIsValid = contents.trim() !== "";
|
||||
const senderNameIsValid = senderName.trim() !== "";
|
||||
const formIsValid = contentsIsValid && senderNameIsValid;
|
||||
|
||||
return (
|
||||
<Modal show={props.shown} onHide={props.dismiss}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{props.module.resources.AddMessage}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form.Group controlId="contents" className="mb-3">
|
||||
<Form.Label>{props.module.resources.Contents}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={contents}
|
||||
isInvalid={!contentsIsValid}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContents(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="senderName" className="mb-3">
|
||||
<Form.Label>{props.module.resources.SenderName}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={senderName}
|
||||
isInvalid={!senderNameIsValid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSenderName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="senderEmail" className="mb-3">
|
||||
<Form.Label>{props.module.resources.SenderEmail}</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
value={senderEmail}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSenderEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Cancel}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={formIsValid === false}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Do something with newFile, title, description, and visibility
|
||||
const newMessage = {
|
||||
...new Message(),
|
||||
Contents: contents.trim(),
|
||||
SenderName: senderName.trim(),
|
||||
SenderEmail: senderEmail.trim(),
|
||||
};
|
||||
props.module.service.addMessage(
|
||||
newMessage,
|
||||
(newMessage: IMessage) => {
|
||||
// Success
|
||||
props.addNewMessage(newMessage);
|
||||
clearFormAndDismiss();
|
||||
},
|
||||
(error: string) => {
|
||||
// Error
|
||||
alert(error);
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
clearFormAndDismiss();
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Save}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMessage;
|
||||
60
Client/Js/InMemoriam/Components/Messages/Message.tsx
Normal file
60
Client/Js/InMemoriam/Components/Messages/Message.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IMessage } from "../../Models/IMessage";
|
||||
import SanitizedHTML from "react-sanitized-html";
|
||||
import Icon from "@mdi/react";
|
||||
import { mdiDeleteForever } from "@mdi/js";
|
||||
|
||||
interface IMessageProps {
|
||||
module: IAppModule;
|
||||
message: IMessage;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const Message: React.FC<IMessageProps> = (props) => {
|
||||
return (
|
||||
<div className="message">
|
||||
<div className="content">
|
||||
<SanitizedHTML html={props.message.Contents.replace(/\n/g, "<br />")} />
|
||||
</div>
|
||||
<div className="footer mt-3">
|
||||
<div>
|
||||
<small>
|
||||
<strong>Created:</strong>{" "}
|
||||
{new Intl.DateTimeFormat(undefined, { dateStyle: "short" }).format(
|
||||
new Date(props.message.CreatedOn)
|
||||
)}{" "}
|
||||
<strong>by</strong> {props.message.SenderName}
|
||||
</small>
|
||||
</div>
|
||||
{props.module.security.IsFamily && (
|
||||
<div className="buttons">
|
||||
<a
|
||||
href="#"
|
||||
className="btn btn-sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (confirm(props.module.resources.DeleteConfirm)) {
|
||||
props.module.service.deleteMessage(
|
||||
props.message.MessageId,
|
||||
() => {
|
||||
props.onDelete();
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
props.onDelete();
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon path={mdiDeleteForever} size={1} />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
73
Client/Js/InMemoriam/Components/Messages/MessagesPage.tsx
Normal file
73
Client/Js/InMemoriam/Components/Messages/MessagesPage.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
import { IAppModule } from "../../Models/IAppModule";
|
||||
import { IMessage } from "../../Models/IMessage";
|
||||
import Message from "./Message";
|
||||
import CreateMessage from "./CreateMessage";
|
||||
|
||||
interface IMessagesPageProps {
|
||||
module: IAppModule;
|
||||
}
|
||||
|
||||
const MessagesPage: React.FC<IMessagesPageProps> = (props) => {
|
||||
const [messages, setMessages] = React.useState<IMessage[]>([]);
|
||||
const [showAddMessage, setShowAddMessage] = React.useState(false);
|
||||
|
||||
const refreshMessages = () => {
|
||||
props.module.service.getMessages(
|
||||
(data) => {
|
||||
setMessages(data);
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshMessages();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-row-reverse mb-4">
|
||||
{props.module.security.CanMessage && (
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowAddMessage(true);
|
||||
}}
|
||||
>
|
||||
{props.module.resources.Add}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="messages">
|
||||
{messages.map((message) => (
|
||||
<Message
|
||||
key={message.MessageId}
|
||||
message={message}
|
||||
module={props.module}
|
||||
onDelete={() => {
|
||||
setMessages(messages.filter((m) => m.MessageId !== message.MessageId));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{showAddMessage && (
|
||||
<CreateMessage
|
||||
module={props.module}
|
||||
shown={showAddMessage}
|
||||
dismiss={() => setShowAddMessage(false)}
|
||||
addNewMessage={(message) => {
|
||||
setMessages([message, ...messages]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessagesPage;
|
||||
35
Client/Js/InMemoriam/LinkState.ts
Normal file
35
Client/Js/InMemoriam/LinkState.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Utils } from "./Utils";
|
||||
|
||||
function createHandler(
|
||||
component: any,
|
||||
key: string,
|
||||
property: string,
|
||||
type?: string
|
||||
) {
|
||||
return (e: any) => {
|
||||
const el: any = e.target;
|
||||
var value: any = el.type === "checkbox" ? el.checked : el.value;
|
||||
if (type) {
|
||||
switch (type) {
|
||||
case "int":
|
||||
if (value != "" && !Utils.isInt(value)) return;
|
||||
if (value != "") value = parseInt(value);
|
||||
}
|
||||
}
|
||||
var obj = component.state[key];
|
||||
obj[property] = value;
|
||||
component.setState({
|
||||
[key]: obj
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function linkState(
|
||||
component: any,
|
||||
key: string,
|
||||
property: string,
|
||||
type?: string
|
||||
) {
|
||||
return createHandler(component, key, property, type);
|
||||
}
|
||||
|
||||
31
Client/Js/InMemoriam/Models/IAppModule.ts
Normal file
31
Client/Js/InMemoriam/Models/IAppModule.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import DataService from "../Service";
|
||||
import { IContextSecurity } from "./IContextSecurity";
|
||||
|
||||
export interface IAppModule {
|
||||
moduleId: number;
|
||||
tabId: number;
|
||||
locale: string;
|
||||
resources: any;
|
||||
common: any;
|
||||
security: IContextSecurity;
|
||||
service: DataService;
|
||||
}
|
||||
|
||||
export class AppModule implements IAppModule {
|
||||
public moduleId: number;
|
||||
public tabId: number;
|
||||
public locale: string;
|
||||
public resources: any;
|
||||
public common: any;
|
||||
public security: IContextSecurity;
|
||||
public service: DataService;
|
||||
constructor(moduleId: number, tabId: number, locale: string, resources: any, common: any, security: IContextSecurity, service: DataService) {
|
||||
this.moduleId = moduleId;
|
||||
this.tabId = tabId;
|
||||
this.locale = locale;
|
||||
this.resources = resources;
|
||||
this.common = common;
|
||||
this.security = security;
|
||||
this.service = service;
|
||||
}
|
||||
}
|
||||
9
Client/Js/InMemoriam/Models/IContextSecurity.ts
Normal file
9
Client/Js/InMemoriam/Models/IContextSecurity.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface IContextSecurity {
|
||||
UserId: number;
|
||||
CanView: boolean;
|
||||
CanEdit: boolean;
|
||||
CanAdd: boolean;
|
||||
CanMessage: boolean;
|
||||
IsFamily: boolean;
|
||||
IsAdmin: boolean;
|
||||
}
|
||||
62
Client/Js/InMemoriam/Models/IKeyedCollection.ts
Normal file
62
Client/Js/InMemoriam/Models/IKeyedCollection.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export interface IKeyedCollection<T> {
|
||||
Add(key: string, value: T): void;
|
||||
ContainsKey(key: string): boolean;
|
||||
Count(): number;
|
||||
Item(key: string): T;
|
||||
Keys(): string[];
|
||||
Remove(key: string): T;
|
||||
Values(): T[];
|
||||
}
|
||||
export class KeyedCollection<T> implements IKeyedCollection<T> {
|
||||
private items: { [index: string]: T } = {};
|
||||
|
||||
private count: number = 0;
|
||||
|
||||
public ContainsKey(key: string): boolean {
|
||||
return this.items.hasOwnProperty(key);
|
||||
}
|
||||
|
||||
public Count(): number {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
public Add(key: string, value: T) {
|
||||
this.items[key] = value;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
public Remove(key: string): T {
|
||||
var val = this.items[key];
|
||||
delete this.items[key];
|
||||
this.count--;
|
||||
return val;
|
||||
}
|
||||
|
||||
public Item(key: string): T {
|
||||
return this.items[key];
|
||||
}
|
||||
|
||||
public Keys(): string[] {
|
||||
var keySet: string[] = [];
|
||||
|
||||
for (var prop in this.items) {
|
||||
if (this.items.hasOwnProperty(prop)) {
|
||||
keySet.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
return keySet;
|
||||
}
|
||||
|
||||
public Values(): T[] {
|
||||
var values: T[] = [];
|
||||
|
||||
for (var prop in this.items) {
|
||||
if (this.items.hasOwnProperty(prop)) {
|
||||
values.push(this.items[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
24
Client/Js/InMemoriam/Models/IMessage.ts
Normal file
24
Client/Js/InMemoriam/Models/IMessage.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface IMessage {
|
||||
MessageId: number;
|
||||
ModuleId: number;
|
||||
Contents: string;
|
||||
SenderName: string;
|
||||
SenderEmail: string;
|
||||
CreatedOn: Date;
|
||||
}
|
||||
|
||||
export class Message implements IMessage {
|
||||
MessageId: number;
|
||||
ModuleId: number;
|
||||
Contents: string;
|
||||
SenderName: string;
|
||||
SenderEmail: string;
|
||||
CreatedOn: Date;
|
||||
constructor() {
|
||||
this.MessageId = -1;
|
||||
this.ModuleId = -1;
|
||||
this.Contents = "";
|
||||
this.CreatedOn = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
56
Client/Js/InMemoriam/Models/IPicture.ts
Normal file
56
Client/Js/InMemoriam/Models/IPicture.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
export interface IPicture {
|
||||
PictureId: number;
|
||||
ModuleId: number;
|
||||
ImageIdentifier: any;
|
||||
OriginalWidth: number;
|
||||
OriginalHeight: number;
|
||||
OriginalName: string;
|
||||
Title: string;
|
||||
Description: string;
|
||||
PictureYear: number;
|
||||
PictureMonth: number;
|
||||
PictureDay: number;
|
||||
Visibility: number;
|
||||
CreatedByUserID: number;
|
||||
CreatedOnDate: Date;
|
||||
LastModifiedByUserID: number;
|
||||
LastModifiedOnDate: Date;
|
||||
CreatedByUser: string;
|
||||
LastModifiedByUser: string;
|
||||
}
|
||||
|
||||
export class Picture implements IPicture {
|
||||
PictureId: number;
|
||||
ModuleId: number;
|
||||
ImageIdentifier: any;
|
||||
OriginalWidth: number;
|
||||
OriginalHeight: number;
|
||||
OriginalName: string;
|
||||
Title: string;
|
||||
Description: string;
|
||||
PictureYear: number;
|
||||
PictureMonth: number;
|
||||
PictureDay: number;
|
||||
Visibility: number;
|
||||
CreatedByUserID: number;
|
||||
CreatedOnDate: Date;
|
||||
LastModifiedByUserID: number;
|
||||
LastModifiedOnDate: Date;
|
||||
CreatedByUser: string;
|
||||
LastModifiedByUser: string;
|
||||
constructor() {
|
||||
this.PictureId = -1;
|
||||
this.ModuleId = -1;
|
||||
this.OriginalWidth = -1;
|
||||
this.OriginalHeight = -1;
|
||||
this.PictureYear = -1;
|
||||
this.PictureMonth = -1;
|
||||
this.PictureDay = -1;
|
||||
this.Visibility = -1;
|
||||
this.CreatedByUserID = -1;
|
||||
this.CreatedOnDate = new Date();
|
||||
this.LastModifiedByUserID = -1;
|
||||
this.LastModifiedOnDate = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
45
Client/Js/InMemoriam/Models/IStory.ts
Normal file
45
Client/Js/InMemoriam/Models/IStory.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export interface IStory {
|
||||
StoryId: number;
|
||||
ModuleId: number;
|
||||
Title: string;
|
||||
Contents: string;
|
||||
StoryYear: number;
|
||||
StoryMonth: number;
|
||||
StoryDay: number;
|
||||
Visibility: number;
|
||||
CreatedByUserID: number;
|
||||
CreatedOnDate: Date;
|
||||
LastModifiedByUserID: number;
|
||||
LastModifiedOnDate: Date;
|
||||
CreatedByUser: string;
|
||||
LastModifiedByUser: string;
|
||||
}
|
||||
|
||||
export class Story implements IStory {
|
||||
StoryId: number;
|
||||
ModuleId: number;
|
||||
Title: string;
|
||||
Contents: string;
|
||||
StoryYear: number;
|
||||
StoryMonth: number;
|
||||
StoryDay: number;
|
||||
Visibility: number;
|
||||
CreatedByUserID: number;
|
||||
CreatedOnDate: Date;
|
||||
LastModifiedByUserID: number;
|
||||
LastModifiedOnDate: Date;
|
||||
CreatedByUser: string;
|
||||
LastModifiedByUser: string;
|
||||
constructor() {
|
||||
this.StoryId = -1;
|
||||
this.ModuleId = -1;
|
||||
this.StoryYear = -1;
|
||||
this.StoryMonth = -1;
|
||||
this.StoryDay = -1;
|
||||
this.Visibility = -1;
|
||||
this.CreatedByUserID = -1;
|
||||
this.CreatedOnDate = new Date();
|
||||
this.LastModifiedByUserID = -1;
|
||||
this.LastModifiedOnDate = new Date();
|
||||
}
|
||||
}
|
||||
90
Client/Js/InMemoriam/Service.ts
Normal file
90
Client/Js/InMemoriam/Service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { IMessage } from "./Models/IMessage";
|
||||
import { IPicture } from "./Models/IPicture";
|
||||
import { IStory } from "./Models/IStory";
|
||||
|
||||
export interface DnnServiceFramework extends JQueryStatic {
|
||||
dnnSF(moduleId: number): DnnServiceFramework;
|
||||
getServiceRoot(path: string): string;
|
||||
setModuleHeaders(): void;
|
||||
getTabId(): string;
|
||||
}
|
||||
|
||||
export default class DataService {
|
||||
private moduleId: number = -1;
|
||||
private dnn: DnnServiceFramework = <DnnServiceFramework>$;
|
||||
public baseServicepath: string = this.dnn.dnnSF(this.moduleId).getServiceRoot('Bring2mind/InMemoriam');
|
||||
public tabId: string = this.dnn.dnnSF(this.moduleId).getTabId();
|
||||
constructor(mid: number) {
|
||||
this.moduleId = mid;
|
||||
};
|
||||
private ajaxCall(type: string, servicePath: string, controller: string, action: string, id: any, headers: any, data: any, success: Function, fail?: Function, isUploadForm?: boolean)
|
||||
: void {
|
||||
var opts: JQuery.AjaxSettings = {
|
||||
headers: headers,
|
||||
type: type === "POSTFORM" ? "POST": type,
|
||||
url: servicePath + controller + '/' + action + (id != undefined
|
||||
? '/' + id
|
||||
: ''),
|
||||
beforeSend: this
|
||||
.dnn
|
||||
.dnnSF(this.moduleId)
|
||||
.setModuleHeaders,
|
||||
contentType: type === "POSTFORM" ? undefined : "application/json; charset=utf-8",
|
||||
data: type == "POST" ? JSON.stringify(data) : data,
|
||||
dataType: "json"
|
||||
};
|
||||
if (isUploadForm) {
|
||||
opts.contentType = false;
|
||||
opts.processData = false;
|
||||
}
|
||||
$.ajax(opts)
|
||||
.done(function (retdata: any) {
|
||||
if (success != undefined) {
|
||||
success(retdata);
|
||||
}
|
||||
})
|
||||
.fail(function (xhr: any, status: any) {
|
||||
if (fail != undefined) {
|
||||
fail(xhr.responseText);
|
||||
}
|
||||
});
|
||||
};
|
||||
public deletePicture(pictureId: number, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Pictures', 'DeletePicture', pictureId, null, null, success, fail);
|
||||
}
|
||||
public editPicture(picture: IPicture, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Pictures', 'EditPicture', picture.PictureId, null, picture, success, fail, true)
|
||||
}
|
||||
public getPictures(success: Function, fail: Function): void {
|
||||
this.ajaxCall('GET', this.baseServicepath, 'Pictures', 'GetPictures', null, null, null, success, fail);
|
||||
}
|
||||
public createPicture(file: File, title: string, description: string, visibility: number, pictureYear: number, pictureMonth: number, pictureDay: number, success: Function, fail: Function): void {
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('title', title);
|
||||
formData.append('description', description);
|
||||
formData.append('visibility', visibility.toString());
|
||||
formData.append('pictureYear', pictureYear.toString());
|
||||
formData.append('pictureMonth', pictureMonth.toString());
|
||||
formData.append('pictureDay', pictureDay.toString());
|
||||
this.ajaxCall('POSTFORM', this.baseServicepath, 'Pictures', 'UploadPicture', null, null, formData, success, fail, true)
|
||||
}
|
||||
public getStories(success: Function, fail: Function): void {
|
||||
this.ajaxCall('GET', this.baseServicepath, 'Stories', 'GetStories', null, null, null, success, fail);
|
||||
}
|
||||
public editStory(story: IStory, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Stories', 'EditStory', story.StoryId, null, story, success, fail, true)
|
||||
}
|
||||
public deleteStory(storyId: number, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Stories', 'DeleteStory', storyId, null, null, success, fail);
|
||||
}
|
||||
public addMessage(message: IMessage, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Messages', 'AddMessage', -1, null, message, success, fail, true)
|
||||
}
|
||||
public getMessages(success: Function, fail: Function): void {
|
||||
this.ajaxCall('GET', this.baseServicepath, 'Messages', 'GetMessages', null, null, null, success, fail);
|
||||
}
|
||||
public deleteMessage(messageId: number, success: Function, fail: Function): void {
|
||||
this.ajaxCall('POST', this.baseServicepath, 'Messages', 'DeleteMessage', messageId, null, null, success, fail);
|
||||
}
|
||||
}
|
||||
69
Client/Js/InMemoriam/Utils.ts
Normal file
69
Client/Js/InMemoriam/Utils.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export class Utils {
|
||||
public static UnNull(input: Date | undefined, defaultValue: Date): Date {
|
||||
if (input == undefined) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
public static Merge(input: any, mergeObject: any, property: string): void {
|
||||
if (input[property] == undefined) {
|
||||
input[property] = mergeObject[property];
|
||||
}
|
||||
}
|
||||
public static ArraySort(array: any[], sortProperty: string, sortDirection: string): any[] {
|
||||
var res = array;
|
||||
if (sortDirection == "asc") {
|
||||
res.sort((a, b) => {
|
||||
return a[sortProperty] > b[sortProperty] ? 1 : b[sortProperty] > a[sortProperty] ? -1 : 0;
|
||||
});
|
||||
} else {
|
||||
res.sort((a, b) => {
|
||||
return a[sortProperty] > b[sortProperty] ? -1 : b[sortProperty] > a[sortProperty] ? 1 : 0;
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
public static ArrayContains(
|
||||
array: any[],
|
||||
propertyName: string | null,
|
||||
propertyValue: any
|
||||
): boolean {
|
||||
if (propertyName) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (array[i][propertyName] == propertyValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (array[i] == propertyValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static UnNullStr(input: any): string {
|
||||
if (input) return input;
|
||||
return "";
|
||||
}
|
||||
public static isInt(value: any): boolean {
|
||||
return (
|
||||
!isNaN(value) && parseInt(Number(value).toString()) == value && !isNaN(parseInt(value, 10))
|
||||
);
|
||||
}
|
||||
public static Round(value: number, decimals: number) {
|
||||
switch (decimals) {
|
||||
case 0:
|
||||
return Math.round(value);
|
||||
case 1:
|
||||
return Math.round(value * 10) / 10;
|
||||
case 2:
|
||||
return Math.round(value * 100) / 100;
|
||||
default:
|
||||
var m = Math.pow(10, decimals);
|
||||
return Math.round(value * m) / m;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Client/Js/InMemoriam/tsconfig.json
Normal file
22
Client/Js/InMemoriam/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2015",
|
||||
"module": "esnext",
|
||||
"jsx": "react-jsx",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": true,
|
||||
"strictNullChecks": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"typeRoots": ["node_modules/@types", "Types"]
|
||||
}
|
||||
20
Client/Js/InMemoriam/webpack.config.js
Normal file
20
Client/Js/InMemoriam/webpack.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
var path = require("path"),
|
||||
commonConfig = require("../../webpack.common.config");
|
||||
|
||||
var clientAppConfig = Object.assign({}, commonConfig, {
|
||||
context: path.join(__dirname, "."),
|
||||
entry: "./App.tsx",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "../../../Server/InMemoriam/js"),
|
||||
filename: "inmemoriam.js"
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".js", ".ts", ".tsx"],
|
||||
mainFields: ["module", "browser", "main"],
|
||||
alias: {
|
||||
app: path.resolve(__dirname, "src/app/")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = clientAppConfig;
|
||||
69
Client/webpack.common.config.js
Normal file
69
Client/webpack.common.config.js
Normal file
@@ -0,0 +1,69 @@
|
||||
var path = require("path"),
|
||||
webpack = require("webpack"),
|
||||
MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
|
||||
var isProduction =
|
||||
process.argv.indexOf("-p") >= 0 || process.env.NODE_ENV === "production";
|
||||
var sourcePath = path.join(__dirname, ".");
|
||||
|
||||
var commonConfig = {
|
||||
context: sourcePath,
|
||||
target: "web",
|
||||
mode: isProduction ? "production" : "development",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: [/node_modules/, /_Development/],
|
||||
use: {
|
||||
loader: "ts-loader",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /.jsx?$/,
|
||||
exclude: [/node_modules/, /_Development/],
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@babel/preset-env", "@babel/preset-react"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
!isProduction ? "style-loader" : MiniCssExtractPlugin.loader,
|
||||
"css-loader",
|
||||
"postcss-loader",
|
||||
"sass-loader",
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif|svg)$/,
|
||||
loader: "file-loader",
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
jquery: "jQuery",
|
||||
},
|
||||
plugins: [
|
||||
new webpack.EnvironmentPlugin({
|
||||
NODE_ENV: "development", // use 'development' unless process.env.NODE_ENV is defined
|
||||
DEBUG: false,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
jQuery: "jquery",
|
||||
"window.jQuery": "jquery",
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: "../module.css",
|
||||
chunkFilename: "[id].css",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = commonConfig;
|
||||
6
Client/webpack.config.js
Normal file
6
Client/webpack.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
var InMemoriamAppConfigSkinCss = require("./Css/InMemoriam/webpack.config");
|
||||
var InMemoriamAppConfigReact = require("./Js/InMemoriam/webpack.config");
|
||||
module.exports = [
|
||||
InMemoriamAppConfigSkinCss,
|
||||
InMemoriamAppConfigReact,
|
||||
];
|
||||
Reference in New Issue
Block a user