Initial commit

This commit is contained in:
2025-02-13 22:10:32 +01:00
commit 3563d783d4
162 changed files with 14738 additions and 0 deletions

View 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;
}

View 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;
}
}
}

View 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;
}
}
}
}
}

View 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;
}
}
}

View 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;
}

View 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;

View 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();
});

View 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)
)
);
});
}
}

View 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())} />
);
});
}
}

View 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;

View 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;

View 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;

View 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;

View 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}>
&times;
</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"
>
&lt;
</button>
</div>
<div className="next">
<button
type="button"
disabled={!props.hasNext}
onClick={(e) => {
e.preventDefault();
if (props.hasNext) {
props.onNext();
}
}}
className="btn"
>
&gt;
</button>
</div>
</div>
</div>
);
};
export default PictureOverlay;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);
}

View 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;
}
}

View File

@@ -0,0 +1,9 @@
export interface IContextSecurity {
UserId: number;
CanView: boolean;
CanEdit: boolean;
CanAdd: boolean;
CanMessage: boolean;
IsFamily: boolean;
IsAdmin: boolean;
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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);
}
}

View 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;
}
}
}

View 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"]
}

View 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;

View 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
View File

@@ -0,0 +1,6 @@
var InMemoriamAppConfigSkinCss = require("./Css/InMemoriam/webpack.config");
var InMemoriamAppConfigReact = require("./Js/InMemoriam/webpack.config");
module.exports = [
InMemoriamAppConfigSkinCss,
InMemoriamAppConfigReact,
];