Skip to content

Commit 6552f58

Browse files
Add admin information card page
1 parent 7e6d0e4 commit 6552f58

File tree

8 files changed

+282
-4
lines changed

8 files changed

+282
-4
lines changed

db/entities.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,47 @@ describeClass(
158158
notesHTML: Fields.string()
159159
}
160160
);
161+
162+
export class Information {
163+
/** @type {string} */
164+
id;
165+
/** @type {string} */
166+
title;
167+
/** @type {string} */
168+
link;
169+
/** @type {string} */
170+
descriptionMD;
171+
/** @type {string} */
172+
descriptionHTML;
173+
/** @type {string | undefined} */
174+
photo;
175+
};
176+
177+
let md_information;
178+
179+
describeClass(
180+
Information,
181+
Entity(
182+
"informations",
183+
{
184+
allowApiRead: true,
185+
allowApiCrud: r=> r.user && r.user.isLeader,
186+
saving: async information => {
187+
if (isBackend()){
188+
if (!md_information) {
189+
md_information = (await import("markdown-it")).default();
190+
}
191+
information.descriptionHTML = md_information.render(information.descriptionMD || "");
192+
}
193+
}
194+
},
195+
),
196+
{
197+
id: Fields.uuid(),
198+
title: Fields.string(),
199+
link: Fields.string(),
200+
descriptionMD: Fields.string(),
201+
descriptionHTML: Fields.string(),
202+
photo: Fields.string()
203+
}
204+
);

server/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { remultExpress } from "remult/remult-express"
1212
import setUpAuth from "./auth.js";
1313
import setUpUpload from "./upload.js";
1414
import setUpLogging from "./log.js";
15-
import { Redirect, StaffMember, Event, Note } from '../db/entities.js';
15+
import { Redirect, StaffMember, Event, Note, Information } from '../db/entities.js';
1616

1717
let app = express();
1818

@@ -21,7 +21,7 @@ setUpUpload(app);
2121
setUpLogging(app);
2222

2323
// set up db:
24-
const db = remultExpress({ entities: [Redirect, StaffMember, Event, Note] });
24+
const db = remultExpress({ entities: [Redirect, StaffMember, Event, Note, Information] });
2525
app.use(db);
2626

2727
// set up redirects:

server/upload.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ export default function setUpUpload(app){
3535
res.send(`/${staffFolderName}/`+req.file.filename);
3636
});
3737

38+
const infoFolderName = "information-photos";
39+
app.use("/information-photo-upload", makePhotoReceiver(infoFolderName), async (req, res) =>{
40+
const buffer = await sharp(req.file.path)
41+
.resize(800, 800, {
42+
fit: sharp.fit.inside,
43+
withoutEnlargement: true,
44+
}).toBuffer();
45+
await sharp(buffer).toFile(req.file.path);
46+
res.send('/${infoFolderName}/'+req.file.filename);
47+
});
48+
3849
const eventFolderName = "event-photos";
3950
const maxEventPhotoWidth = 1000;
4051
const eventPhotoAR = 2.5;

src/components/Navigation.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export default {
3131
['Meetings', '/meetings'],
3232
['Leadership', '/leadership'],
3333
['Contact', '/contact'],
34+
['IBM Fall Fest!','/ibm']
3435
];
3536
if (loggedIn.value){
3637
buttons.push(

src/router/index.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import Admin from "../views/Admin.vue";
99
import EditStaff from "../views/EditStaff.vue";
1010
import EditEvents from "../views/EditEvents.vue";
1111
import EditNotes from "../views/EditNotes.vue";
12-
import AdminMeetings from "../views/AdminMeetings.vue"
12+
import AdminMeetings from "../views/AdminMeetings.vue";
13+
import FallFest from "../views/FallFest.vue";
14+
import AdminInformation from "../views/AdminInformation.vue";
1315

1416
export const routes = [
1517
{
@@ -66,6 +68,15 @@ export const routes = [
6668
},
6769
component: Meetings,
6870
},
71+
{
72+
path: '/ibm',
73+
name: "IBM Fall Fest",
74+
meta: {
75+
title: "IBM Fall Fest!",
76+
description: "Infomration about our collaboration with IBM, Case Western Reserve University, and Cleveland State University."
77+
},
78+
component: FallFest,
79+
},
6980
{
7081
path: "/admin",
7182
name: "Admin Stuff",
@@ -113,5 +124,13 @@ export const routes = [
113124
meta: {
114125
admin: true
115126
}
116-
}
127+
},
128+
{
129+
path: "/admin/informations",
130+
name: "Information Editor",
131+
component: AdminInformation,
132+
meta: {
133+
admin: true
134+
}
135+
},
117136
];

src/views/Admin.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<li><router-link to="/admin/redirects">Redirect Links</router-link></li>
66
<li><router-link to="/admin/staff">Staff Page Editor</router-link></li>
77
<li><router-link to="/admin/events">Events Editor</router-link></li>
8+
<li><router-link to="/admin/informations">Information Cards Editor</router-link></li>
89
<li><router-link to="/admin/notes">Meeting Notes (edit)</router-link></li>
910
<li><router-link to="/admin/meetings">Meeting Notes (view)</router-link></li>
1011
<li><a href="/audit-log">DB Edit Log</a></li>

src/views/AdminInformation.vue

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<template>
2+
<div>
3+
<p style="margin: 100px 30px 0 30px; max-width: 600px">
4+
<h2>Information Editor</h2>
5+
</p>
6+
<div id="edit-information-container">
7+
<div class="information" v-for="information, j in informationToDisplay" :key="information.id" @input="edited.add(j)">
8+
<label><span>Title: </span><input type="text" v-model="information.title" /></label>
9+
<label>
10+
<span>Photo: </span>
11+
<input type="string" v-model="information.photo" disabled />
12+
<button v-if="information.photo" @click="information.photo=''" class="upload-button">❌</button>
13+
<button @click="upload(j)" class="upload-button" title="Upload photo">Upload</button>
14+
<input type="file" ref="fileUpload" style="display:none" accept="image/jpeg,image/png" />
15+
</label>
16+
<label style="flex-direction: column;">
17+
<span>Markdown Description:</span>
18+
<textarea v-model="information.descriptionMD" />
19+
</label>
20+
<label><span>Link: </span><input type="text" v-model="information.link" /></label>
21+
<div style="display: flex; justify-content: space-evenly; gap: 10px">
22+
<button style="width:100%" @click="update(information, j)">
23+
{{(j==0 ? "➕" : "💾") + (edited.has(j) ? "❗" : "")}}{{ confirmation.has(j) ? ' ✅' : "" }}
24+
</button>
25+
<button style="width:100%" v-if="j!==0" @click="remove(information, j)">
26+
🗑️
27+
</button>
28+
</div>
29+
</div>
30+
</div>
31+
</div>
32+
</template>
33+
34+
<script setup>
35+
import { ref, onMounted, onUnmounted, computed, watch } from "vue";
36+
import { remult } from "remult";
37+
import { Information } from "../../db/entities.js";
38+
39+
import { Cropper } from 'vue-advanced-cropper';
40+
import 'vue-advanced-cropper/dist/style.css';
41+
42+
const informations = ref([]);
43+
44+
const confirmation = ref(new Set());
45+
46+
const edited = ref(new Set());
47+
48+
const repo = remult.repo(Information);
49+
50+
const newInformation = ref(repo.create());
51+
52+
const informationToDisplay = computed(() => {
53+
return [newInformation.value].concat(informations.value);
54+
});
55+
56+
let unsubscribe;
57+
onMounted(() => {
58+
unsubscribe = repo.liveQuery({orderBy: {date: "desc"}})
59+
.subscribe(info => {
60+
informations.value = info.items;
61+
});
62+
});
63+
64+
onUnmounted(() => {
65+
if (unsubscribe){
66+
unsubscribe();
67+
}
68+
});
69+
70+
const update = (information, j) => {
71+
let update;
72+
if (j == 0){
73+
update = repo.insert(newInformation.value);
74+
} else {
75+
update = repo.update(information.id, information);
76+
}
77+
update.then(() => {
78+
confirmation.value.add(j);
79+
edited.value.delete(j);
80+
setTimeout(() => confirmation.value.delete(j), 3000);
81+
if (j == 0){
82+
newInformation.value = repo.create();
83+
}
84+
}).catch((e) => {
85+
alert("Error updating event: " + e);
86+
alert("e: " + e.message);
87+
})
88+
};
89+
90+
const remove = (information, j) => {
91+
if(confirm("really delete " + information.title + "?")){
92+
repo.delete(information);
93+
}
94+
}
95+
96+
const fileUpload = ref(null);
97+
watch(fileUpload, (inputs) => {
98+
for (let i=0; i<inputs.length; ++i){
99+
inputs[i].onchange = () => {
100+
const data = new FormData();
101+
data.append("photo", inputs[i].files[0]);
102+
fetch("/information-photo-upload", { method: "POST", body: data })
103+
.then(async (res) => {
104+
const path = await res.text();
105+
informationToDisplay.value[i].photo = path;
106+
});
107+
};
108+
}
109+
}, { deep: true });
110+
const upload = (i) => {
111+
if (fileUpload.value[i]){
112+
fileUpload.value[i].click();
113+
}
114+
};
115+
116+
</script>
117+
118+
<style scoped lang="scss">
119+
* {
120+
color: black;
121+
}
122+
#edit-information-container {
123+
margin: 10px 10px;
124+
display: flex;
125+
flex-wrap: wrap;
126+
}
127+
.cover-photo-preview {
128+
width: 50%;
129+
height: auto;
130+
padding-bottom: 40%;
131+
display: flex;
132+
justify-content: center;
133+
align-items: center;
134+
position: relative;
135+
background-size: contain;
136+
background-position: center;
137+
background-repeat: no-repeat;
138+
button.add-photo {
139+
position: absolute;
140+
left: 50%;
141+
top: 50%;
142+
transform: translate(-50%, -50%);
143+
padding: 2px;
144+
font-size: 1.3rem;
145+
border-radius: 0;
146+
width: 30px;
147+
height: 30px;
148+
}
149+
button.remove-photo {
150+
position: absolute;
151+
top: 2px;
152+
right: 2px;
153+
padding: 2px;
154+
font-size: 0.9rem;
155+
border-radius: 0;
156+
width: 20px;
157+
height: 20px;
158+
font-size: small;
159+
}
160+
}
161+
.information {
162+
display: flex;
163+
flex-direction: column;
164+
gap: 10px;
165+
margin: 20px;
166+
width: 900px;
167+
}
168+
label {
169+
display: flex;
170+
width: 100%;
171+
}
172+
input {
173+
padding: 2px;
174+
width: 100%;
175+
border-radius: 5px;
176+
}
177+
label span {
178+
flex-shrink: 0;
179+
margin-right: 15px;
180+
}
181+
button, button:hover {
182+
color: black;
183+
border: 1px solid darkgray;
184+
transition: none;
185+
}
186+
textarea {
187+
width: 900px;
188+
height: 100px;
189+
overflow: scroll;
190+
resize: none;
191+
}
192+
.cropper {
193+
width: 600px;
194+
max-height: 600px;
195+
}
196+
</style>
197+

src/views/FallFest.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
3+
</template>
4+
<script setup>
5+
</script>

0 commit comments

Comments
 (0)