init
This commit is contained in:
21
announcements/AnnouncementCard.js
Normal file
21
announcements/AnnouncementCard.js
Normal file
@@ -0,0 +1,21 @@
|
||||
class AnnouncementCard extends Shadow {
|
||||
announcement
|
||||
|
||||
constructor(announcement) {
|
||||
super()
|
||||
this.announcement = announcement
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
p(this.announcement.text)
|
||||
.color("var(--text)")
|
||||
.marginVertical(3, em)
|
||||
})
|
||||
.paddingLeft(2, em)
|
||||
.borderTop("0.5px solid var(--divider)")
|
||||
.borderBottom("0.5px solid var(--divider)")
|
||||
}
|
||||
}
|
||||
|
||||
register(AnnouncementCard, "announcement-card")
|
||||
150
announcements/Old.js
Normal file
150
announcements/Old.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import './Panel.js'
|
||||
import server from '/@server/serverFunctions.js'
|
||||
|
||||
css(`
|
||||
announcements- {
|
||||
font-family: 'Bona';
|
||||
}
|
||||
|
||||
announcements- input::placeholder {
|
||||
font-family: 'Bona Nova';
|
||||
font-size: 0.9em;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none; /* remove default style */
|
||||
-webkit-appearance: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
background-color: var(--red);
|
||||
}
|
||||
`)
|
||||
|
||||
class Announcements extends Shadow {
|
||||
announcements;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.announcements = global.currentNetwork.data.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
console.log(this.announcements)
|
||||
}
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
VStack(() => {
|
||||
|
||||
Panel(this.announcements)
|
||||
|
||||
input("Message", "70%")
|
||||
.paddingVertical(0.75, em)
|
||||
.boxSizing("border-box")
|
||||
.paddingHorizontal(2, em)
|
||||
.color("var(--text)")
|
||||
.background("var(--searchbackground)")
|
||||
.marginBottom(1, em)
|
||||
.border("0.5px solid var(--accent)")
|
||||
.outline("none")
|
||||
.borderRadius(100, px)
|
||||
.fontFamily("Arial")
|
||||
.fontSize(1, em)
|
||||
.onKeyDown(async function(e) {
|
||||
if (e.key === "Enter") {
|
||||
const result = await server.addAnnouncement(this.value, global.currentNetwork.id, global.profile.id)
|
||||
if (result.data.status === 200) {
|
||||
window.dispatchEvent(new CustomEvent('new-announcement', {
|
||||
detail: { announcement: result.data.announcement }
|
||||
}));
|
||||
} else {
|
||||
// error
|
||||
}
|
||||
this.value = ""
|
||||
}
|
||||
})
|
||||
})
|
||||
.gap(1, em)
|
||||
.boxSizing("border-box")
|
||||
.width(100, pct)
|
||||
.height(100, pct)
|
||||
.horizontalAlign("center")
|
||||
.verticalAlign("end")
|
||||
.minHeight(0)
|
||||
})
|
||||
.backgroundColor("var(--main)")
|
||||
.boxSizing("border-box")
|
||||
.paddingVertical(1, em)
|
||||
.width(100, pct)
|
||||
.height(100, pct)
|
||||
.flex("1 1 auto")
|
||||
.onEvent("new-announcement", this.onNewAnnouncement)
|
||||
.onEvent("deleted-announcement", this.onDeletedAnnouncement)
|
||||
.onEvent("edited-announcement", this.onEditedAnnouncement)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.getAnnouncements(global.currentNetwork.id)
|
||||
}
|
||||
|
||||
checkForUpdates(currentAnnouncements, fetchedAnnouncements) {
|
||||
if (currentAnnouncements.length !== fetchedAnnouncements.length) return true;
|
||||
|
||||
const currentMap = new Map(currentAnnouncements.map(ann => [ann.id, ann]));
|
||||
|
||||
for (const fetchedAnn of fetchedAnnouncements) {
|
||||
const currentAnn = currentMap.get(fetchedAnn.id);
|
||||
|
||||
// new event added
|
||||
if (!currentAnn) return true;
|
||||
|
||||
// existing event changed
|
||||
if (currentAnn.updated_at !== fetchedAnn.updated_at) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async getAnnouncements(networkId) {
|
||||
const fetchedAnnouncements = await server.getAnnouncements(networkId)
|
||||
if (this.checkForUpdates(this.announcements, fetchedAnnouncements.data)) {
|
||||
console.log("found updates")
|
||||
this.announcements = fetchedAnnouncements.data.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
global.currentNetwork.data.announcements = this.announcements
|
||||
this.rerender()
|
||||
}
|
||||
}
|
||||
|
||||
onNewAnnouncement = (e) => {
|
||||
let newAnnouncement = e.detail.announcement;
|
||||
this.announcements.push(newAnnouncement)
|
||||
this.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
onDeletedAnnouncement = (e) => {
|
||||
let deletedId = e.detail.id
|
||||
const i = this.announcements.findIndex(ann => ann.id === deletedId)
|
||||
if (i !== -1) this.announcements.splice(i, 1);
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
onEditedAnnouncement = (e) => {
|
||||
let editedAnnouncement = e.detail
|
||||
const i = this.announcements.findIndex(ann => ann.id === editedPost.id)
|
||||
if (i !== -1) {
|
||||
this.announcements.splice(i, 1)
|
||||
this.announcements.unshift(editedAnnouncement)
|
||||
}
|
||||
this.rerender()
|
||||
}
|
||||
}
|
||||
|
||||
register(Announcements)
|
||||
150
announcements/Panel.js
Normal file
150
announcements/Panel.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import "/_/code/components/LoadingCircle.js"
|
||||
|
||||
css(`
|
||||
panel- {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
panel-::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
panel-::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
panel-::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`)
|
||||
|
||||
class Panel extends Shadow {
|
||||
announcements = []
|
||||
isSending = false
|
||||
|
||||
constructor(announcements) {
|
||||
super()
|
||||
this.announcements = announcements
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
if(this.announcements.length > 0) {
|
||||
let previousDate = null
|
||||
|
||||
for(let i=0; i<this.announcements.length; i++) {
|
||||
let announcement = this.announcements[i]
|
||||
const isMe = announcement.creator_id === global.profile.id
|
||||
const dateParts = this.parseDate(announcement.created);
|
||||
const { date, time } = dateParts;
|
||||
|
||||
if (previousDate !== date) {
|
||||
previousDate = date;
|
||||
|
||||
p(date)
|
||||
.textAlign("center")
|
||||
.opacity(0.6)
|
||||
.fontWeight("bold")
|
||||
.paddingTop(1, em)
|
||||
.paddingBottom(0.5, em)
|
||||
.color("var(--quillred)")
|
||||
.borderTop(`1px solid var(--${i == 0 ? "transparent" : "divider"})`)
|
||||
}
|
||||
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
h3(isMe ? "Me" : this.getAuthorName(announcement))
|
||||
.color(isMe ? "var(--quillred)" : "var(--headertext")
|
||||
.opacity(0.75)
|
||||
.margin(0)
|
||||
|
||||
h3(`${date} ${time}`)
|
||||
.opacity(0.5)
|
||||
.color("var(--headertext)")
|
||||
.margin(0)
|
||||
.marginLeft(0.5, em)
|
||||
.fontSize(1, em)
|
||||
|
||||
if (announcement.created !== announcement.updated_at) {
|
||||
p("(edited)")
|
||||
.color("var(--headertext)")
|
||||
.letterSpacing(0.8, "px")
|
||||
.opacity(0.5)
|
||||
.fontWeight("bold")
|
||||
.paddingLeft(0.25, em)
|
||||
.fontSize(0.9, em)
|
||||
}
|
||||
})
|
||||
.verticalAlign("center")
|
||||
.marginBottom(0.1, em)
|
||||
|
||||
p(announcement.text)
|
||||
.color("var(--text)")
|
||||
.marginHorizontal(0.2, em)
|
||||
.paddingVertical(0.2, em)
|
||||
.boxSizing("border-box")
|
||||
})
|
||||
.marginBottom(0.05, em)
|
||||
}
|
||||
} else {
|
||||
LoadingCircle()
|
||||
}
|
||||
})
|
||||
.gap(1, em)
|
||||
.fontSize(1.1, em)
|
||||
.boxSizing("border-box")
|
||||
.flex("1 1 auto")
|
||||
.minHeight(0)
|
||||
.overflowY("auto")
|
||||
.width(100, pct)
|
||||
.paddingBottom(2, em)
|
||||
.paddingHorizontal(4, pct)
|
||||
.backgroundColor("var(--main)")
|
||||
.onAppear(async () => {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
getAuthorName(announcement) {
|
||||
const members = global.currentNetwork.data.members;
|
||||
const creator = members.find(m => m.id === announcement.creator_id);
|
||||
if (creator) {
|
||||
return `${creator.first_name} ${creator.last_name}`
|
||||
} else {
|
||||
return "No name"
|
||||
}
|
||||
}
|
||||
|
||||
parseDate(str) {
|
||||
// Format: YYYY-MM-DDTHH:MM:SS.mmmZ
|
||||
const match = str.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):\d{2}\.\d+Z$/);
|
||||
if (!match) return null;
|
||||
|
||||
const [, yyyy, mm, dd, hh, min] = match;
|
||||
|
||||
// Convert 24h to 12h
|
||||
const hour24 = parseInt(hh, 10);
|
||||
const ampm = hour24 >= 12 ? 'pm' : 'am';
|
||||
const hour12 = hour24 % 12 || 12;
|
||||
|
||||
const date = `${mm}/${dd}/${yyyy}`;
|
||||
const time = `${hour12}:${min}${ampm}`;
|
||||
|
||||
return { date, time };
|
||||
}
|
||||
|
||||
formatTime(str) {
|
||||
const match = str.match(/-(\d+:\d+):\d+.*(am|pm)/i);
|
||||
if (!match) return null;
|
||||
|
||||
const [_, hourMin, ampm] = match;
|
||||
return hourMin + ampm.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
register(Panel)
|
||||
273
announcements/announcements.js
Normal file
273
announcements/announcements.js
Normal file
@@ -0,0 +1,273 @@
|
||||
import server from '/@server/server.js'
|
||||
import '../components/SearchBar.js'
|
||||
import "/_/code/components/LoadingCircle.js"
|
||||
import './AnnouncementCard.js'
|
||||
|
||||
css(`
|
||||
announcements- {
|
||||
font-family: 'Arial';
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
announcements- h1 {
|
||||
font-family: 'Bona';
|
||||
}
|
||||
|
||||
announcements- .VStack::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
announcements- .VStack::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
announcements- .VStack::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
`)
|
||||
|
||||
class Announcements extends Shadow {
|
||||
static searchableKeys = ['message', 'author'];
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.announcements = global.currentNetwork.data.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
this.searchedAnnouncements = [];
|
||||
this.searchText = "";
|
||||
}
|
||||
|
||||
render() {
|
||||
ZStack(() => {
|
||||
VStack(() => {
|
||||
SearchBar(this.searchText, "90vw")
|
||||
|
||||
VStack(() => {
|
||||
if (global.appRefreshing) {
|
||||
LoadingCircle()
|
||||
} else if (!this.announcements || this.announcements == []) {
|
||||
LoadingCircle()
|
||||
} else if (this.searchText) {
|
||||
if (this.searchedAnnouncements.length > 0) {
|
||||
for (let i = 0; i < this.searchedAnnouncements.length; i++) {
|
||||
AnnouncementCard(this.searchedAnnouncements[i])
|
||||
}
|
||||
} else {
|
||||
h2("Could not find any announcements with your search criteria.")
|
||||
.color("var(--divider)")
|
||||
.fontWeight("bold")
|
||||
.marginTop(7.5, em)
|
||||
.marginBottom(0.5, em)
|
||||
.textAlign("center")
|
||||
}
|
||||
} else if (this.announcements.length > 0) {
|
||||
for (let i = 0; i < this.announcements.length; i++) {
|
||||
AnnouncementCard(this.announcements[i])
|
||||
}
|
||||
} else {
|
||||
h2("No Announcements")
|
||||
.color("var(--divider)")
|
||||
.fontWeight("bold")
|
||||
.marginTop(7.5, em)
|
||||
.marginBottom(0.5, em)
|
||||
.textAlign("center")
|
||||
}
|
||||
})
|
||||
.overflowY("scroll")
|
||||
.gap(0.75, em)
|
||||
|
||||
if(!global.appRefreshing && global.currentNetwork.permissions.includes("announcements.add")) {
|
||||
HStack(() => {
|
||||
input("Image Upload", "0px", "0px")
|
||||
.attr({ name: "image-upload", type: "file" })
|
||||
.display("none")
|
||||
.visibility("hidden")
|
||||
.onChange((e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
this.stagedImage = file
|
||||
this.stagedImageURL = URL.createObjectURL(file)
|
||||
this.rerender()
|
||||
})
|
||||
|
||||
div("+")
|
||||
.width(3, rem)
|
||||
.height(3, rem)
|
||||
.borderRadius(50, pct)
|
||||
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
|
||||
.fontSize(2, em)
|
||||
.transform("rotate(180deg)")
|
||||
.zIndex(1001)
|
||||
.display("flex")
|
||||
.alignItems("center")
|
||||
.justifyContent("center")
|
||||
.transition("scale .2s")
|
||||
.state("touched", function (touched) {
|
||||
if(touched) {
|
||||
this.scale("1.5")
|
||||
this.color("var(--darkaccent)")
|
||||
this.backgroundColor("var(--divider)")
|
||||
} else {
|
||||
this.scale("")
|
||||
this.color("var(--divider)")
|
||||
this.backgroundColor("var(--searchbackground)")
|
||||
}
|
||||
})
|
||||
.onTouch(function (start) {
|
||||
if(start) {
|
||||
this.attr({touched: "true"})
|
||||
} else {
|
||||
this.attr({touched: ""})
|
||||
}
|
||||
})
|
||||
.onClick((done) => {
|
||||
if(done) {
|
||||
const inputSelector = this.$('[name="image-upload"]');
|
||||
inputSelector.click()
|
||||
}
|
||||
})
|
||||
|
||||
input("Add an Announcement")
|
||||
.flex("1 1 auto")
|
||||
.minWidth(0)
|
||||
.color("var(--text)")
|
||||
.background("var(--searchbackground)")
|
||||
.paddingVertical(0, rem)
|
||||
.fontSize(1, rem)
|
||||
.paddingHorizontal(1, rem)
|
||||
.borderRadius(100, px)
|
||||
.border("1px solid color-mix(in srgb, var(--accent) 60%, transparent)")
|
||||
.onTouch(function (start) {
|
||||
if (start) {
|
||||
this.style.backgroundColor = "var(--accent)"
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
$("appmenu-").display("none")
|
||||
this.focus()
|
||||
}, 20)
|
||||
console.log($("appmenu-"))
|
||||
this.style.backgroundColor = "var(--searchbackground)"
|
||||
}
|
||||
})
|
||||
.addEventListener("blur", () => {
|
||||
setTimeout(() => {
|
||||
$("appmenu-").display("grid")
|
||||
}, 20)
|
||||
})
|
||||
})
|
||||
.width(100, pct)
|
||||
.boxSizing("border-box")
|
||||
.position("absolute")
|
||||
.paddingHorizontal(1, rem)
|
||||
.bottom(1, vh)
|
||||
.gap(0.5, rem)
|
||||
}
|
||||
})
|
||||
.boxSizing("border-box")
|
||||
.height(100, pct)
|
||||
.width(100, pct)
|
||||
.onEvent("announcementsearch", this.onAnnouncementSearch)
|
||||
.onEvent("new-announcement", this.onNewAnnouncement)
|
||||
.onEvent("deleted-announcement", this.onDeletedAnnouncement)
|
||||
.onEvent("edited-announcement", this.onEditedAnnouncement)
|
||||
})
|
||||
}
|
||||
|
||||
async handleUpload(file) {
|
||||
try {
|
||||
const body = new FormData();
|
||||
body.append('image', file);
|
||||
const res = await mobileUtil.authFetch(`${config.SERVER}/profile/upload-image`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
},
|
||||
body: body
|
||||
});
|
||||
|
||||
if(res.status === 401) {
|
||||
return res.status
|
||||
}
|
||||
if (!res.ok) return res.status;
|
||||
const data = await res.json()
|
||||
global.profile = data.member
|
||||
console.log(global.profile)
|
||||
} catch (err) { // Network error / Error reaching server
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
addPhoto() {
|
||||
console.log("hey")
|
||||
}
|
||||
|
||||
onNewAnnouncement = (e) => {
|
||||
let newAnnouncement = e.detail.announcement;
|
||||
this.announcements.push(newAnnouncement)
|
||||
this.announcements.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
onDeletedAnnouncement = (e) => {
|
||||
let deletedId = e.detail.id
|
||||
const i = this.announcements.findIndex(ann => ann.id === deletedId)
|
||||
if (i !== -1) this.announcements.splice(i, 1);
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
onEditedAnnouncement = (e) => {
|
||||
let editedAnnouncement = e.detail
|
||||
const i = this.announcements.findIndex(ann => ann.id === editedAnnouncement.id)
|
||||
if (i !== -1) {
|
||||
this.announcements.splice(i, 1)
|
||||
this.announcements.unshift(editedAnnouncement)
|
||||
}
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
onAnnouncementSearch = (e) => {
|
||||
let searchText = e.detail.searchText.toLowerCase().trim();
|
||||
if (!searchText) {
|
||||
this.searchedAnnouncements = [];
|
||||
} else {
|
||||
this.searchedAnnouncements = this.announcements.filter(announcement =>
|
||||
Announcements.searchableKeys.some(key =>
|
||||
String(announcement[key]).toLowerCase().includes(searchText)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.searchText = searchText
|
||||
this.rerender()
|
||||
}
|
||||
|
||||
async getAnnouncements(networkId) {
|
||||
const fetchedAnnouncements = await server.getAnnouncements(networkId)
|
||||
if (this.checkForUpdates(this.announcements, fetchedAnnouncements.data)) {
|
||||
this.announcements = fetchedAnnouncements.data.sort((a, b) => new Date(b.created) - new Date(a.created));
|
||||
global.currentNetwork.data.announcements = this.announcements
|
||||
this.rerender()
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.getAnnouncements(global.currentNetwork.id)
|
||||
}
|
||||
|
||||
checkForUpdates(currentAnnouncements, fetchedAnnouncements) {
|
||||
if (currentAnnouncements.length !== fetchedAnnouncements.length) return true;
|
||||
|
||||
const currentMap = new Map(currentAnnouncements.map(ann => [ann.id, ann]));
|
||||
|
||||
for (const fetchedAnn of fetchedAnnouncements) {
|
||||
const currentAnn = currentMap.get(fetchedAnn.id);
|
||||
if (!currentAnn) return true;
|
||||
if (currentAnn.updated_at !== fetchedAnn.updated_at) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
register(Announcements)
|
||||
154
announcements/desktop/DesktopAnnouncementsFeed.js
Normal file
154
announcements/desktop/DesktopAnnouncementsFeed.js
Normal file
@@ -0,0 +1,154 @@
|
||||
class DesktopAnnouncementsFeed extends Shadow {
|
||||
constructor(announcements, selectedId, searchText, onSelect, onSearch) {
|
||||
super()
|
||||
this.announcements = announcements
|
||||
this.selectedId = selectedId
|
||||
this.searchText = searchText
|
||||
this.onSelect = onSelect
|
||||
this.onSearch = onSearch
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
// Search bar
|
||||
HStack(() => {
|
||||
p("🔍")
|
||||
.margin(0).fontSize(0.78, em).opacity(0.38).flexShrink(0)
|
||||
input()
|
||||
.attr({ type: "text", placeholder: "Search announcements…", value: this.searchText })
|
||||
.flex(1).border("none").outline("none").background("transparent")
|
||||
.color("var(--headertext)").fontSize(0.85, em)
|
||||
.onInput((e) => this.onSearch(e.target.value))
|
||||
})
|
||||
.gap(0.5, em).paddingHorizontal(0.85, em).paddingVertical(0.58, em)
|
||||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||||
.borderRadius(0.5, em).alignItems("center")
|
||||
.marginHorizontal(0.75, em).marginTop(0.75, em).marginBottom(0.5, em)
|
||||
.flexShrink(0)
|
||||
|
||||
// Count
|
||||
p(`${this.announcements.length} announcement${this.announcements.length !== 1 ? "s" : ""}`)
|
||||
.margin(0).paddingHorizontal(1.1, em).paddingBottom(0.35, em)
|
||||
.fontSize(0.68, em).fontWeight("700").letterSpacing("0.05em")
|
||||
.color("var(--headertext)").opacity(0.32)
|
||||
.flexShrink(0)
|
||||
|
||||
// List
|
||||
VStack(() => {
|
||||
if (this.announcements.length === 0) {
|
||||
VStack(() => {
|
||||
p(this.searchText ? "No results" : "No announcements yet")
|
||||
.margin(0).fontSize(0.85, em)
|
||||
.color("var(--headertext)").opacity(0.32).textAlign("center")
|
||||
})
|
||||
.flex(1).justifyContent("center").alignItems("center").paddingTop(3, em)
|
||||
} else {
|
||||
this.announcements.forEach(ann => this.renderRow(ann))
|
||||
}
|
||||
})
|
||||
.flex(1).overflowY("auto").gap(0).paddingBottom(0.75, em)
|
||||
})
|
||||
.height(100, pct).width(100, pct).boxSizing("border-box")
|
||||
}
|
||||
|
||||
renderRow(ann) {
|
||||
const isSelected = ann.id === this.selectedId
|
||||
const isEdited = ann.created !== ann.updated_at
|
||||
const isMe = ann.creator_id === global.profile.id
|
||||
const author = this.getAuthor(ann.creator_id)
|
||||
const authorName = isMe ? "You" : author
|
||||
const initials = this.getInitials(ann.creator_id)
|
||||
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
// Avatar
|
||||
VStack(() => {
|
||||
p(initials)
|
||||
.margin(0).fontSize(0.6, em).fontWeight("700")
|
||||
.color("white").lineHeight("1")
|
||||
})
|
||||
.width(2.1, em).height(2.1, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(author))
|
||||
.justifyContent("center").alignItems("center").flexShrink(0)
|
||||
|
||||
VStack(() => {
|
||||
// Author + date
|
||||
HStack(() => {
|
||||
p(authorName)
|
||||
.margin(0).fontSize(0.8, em).fontWeight("600")
|
||||
.color(isMe ? "var(--quillred)" : "var(--headertext)")
|
||||
.flex(1).minWidth(0)
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
p(this.relativeDate(ann.created))
|
||||
.margin(0).fontSize(0.68, em)
|
||||
.color("var(--headertext)").opacity(0.38).flexShrink(0)
|
||||
})
|
||||
.alignItems("center").gap(0.4, em)
|
||||
|
||||
// Preview text
|
||||
p(ann.text)
|
||||
.margin(0).marginTop(0.12, em).fontSize(0.78, em)
|
||||
.color("var(--headertext)")
|
||||
.opacity(isSelected ? 0.75 : 0.45)
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
.lineHeight("1.4")
|
||||
})
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.gap(0.62, em).alignItems("flex-start")
|
||||
|
||||
if (isEdited) {
|
||||
p("edited")
|
||||
.margin(0).marginTop(0.3, em)
|
||||
.fontSize(0.62, em).fontStyle("italic")
|
||||
.color("var(--headertext)").opacity(0.28)
|
||||
.alignSelf("flex-end")
|
||||
}
|
||||
})
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.7, em)
|
||||
.marginHorizontal(0.4, em)
|
||||
.borderRadius(0.55, em)
|
||||
.background(isSelected ? "var(--accent)" : "transparent")
|
||||
.cursor("pointer")
|
||||
.width("calc(100% - 0.8em)").boxSizing("border-box")
|
||||
.onClick((done) => {
|
||||
if(done) this.onSelect(ann.id)
|
||||
})
|
||||
}
|
||||
|
||||
getAuthor(creatorId) {
|
||||
const members = global.currentNetwork.data?.members || []
|
||||
const m = members.find(m => m.id === creatorId)
|
||||
return m ? `${m.first_name} ${m.last_name}` : "Unknown"
|
||||
}
|
||||
|
||||
getInitials(creatorId) {
|
||||
const members = global.currentNetwork.data?.members || []
|
||||
const m = members.find(m => m.id === creatorId)
|
||||
if (!m) return "?"
|
||||
return [m.first_name?.[0], m.last_name?.[0]].filter(Boolean).join("").toUpperCase()
|
||||
}
|
||||
|
||||
relativeDate(raw) {
|
||||
const d = new Date(raw)
|
||||
const diff = Date.now() - d
|
||||
const mins = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
if (mins < 1) return "just now"
|
||||
if (mins < 60) return `${mins}m ago`
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
if (days === 1) return "yesterday"
|
||||
if (days < 7) return `${days}d ago`
|
||||
return d.toLocaleDateString([], { month: "short", day: "numeric" })
|
||||
}
|
||||
|
||||
avatarColor(name) {
|
||||
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"]
|
||||
let hash = 0
|
||||
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
}
|
||||
|
||||
register(DesktopAnnouncementsFeed)
|
||||
420
announcements/desktop/DesktopAnnouncementsViewer.js
Normal file
420
announcements/desktop/DesktopAnnouncementsViewer.js
Normal file
@@ -0,0 +1,420 @@
|
||||
import server from "/@server/server.js"
|
||||
|
||||
class DesktopAnnouncementsViewer extends Shadow {
|
||||
constructor(announcement, canPost, canEdit, canDelete, onNew, onEdited, onDeleted) {
|
||||
super()
|
||||
this.announcement = announcement
|
||||
this.canPost = canPost
|
||||
this.canEdit = canEdit
|
||||
this.canDelete = canDelete
|
||||
this.onNew = onNew
|
||||
this.onEdited = onEdited
|
||||
this.onDeleted = onDeleted
|
||||
|
||||
this.composing = false
|
||||
this.editingId = null
|
||||
this.draftText = ""
|
||||
this.sending = false
|
||||
this.errorMsg = ""
|
||||
this.confirmDeleteId = null
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
if (this.composing || this.editingId) {
|
||||
this.renderCompose()
|
||||
} else if (this.announcement) {
|
||||
this.renderViewer()
|
||||
} else {
|
||||
this.renderEmpty()
|
||||
}
|
||||
})
|
||||
.height(100, pct).width(100, pct).overflow("hidden")
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
VStack(() => {
|
||||
// Stats strip
|
||||
VStack(() => {
|
||||
const members = global.currentNetwork.data?.members || []
|
||||
const allAnnouncements = global.currentNetwork.data?.announcements || []
|
||||
const thisWeek = allAnnouncements.filter(a => {
|
||||
return (Date.now() - new Date(a.created)) < 7 * 86400000
|
||||
}).length
|
||||
|
||||
HStack(() => {
|
||||
this.statCard("📣", "Total", allAnnouncements.length)
|
||||
this.statCard("📅", "This week", thisWeek)
|
||||
this.statCard("👥", "Authors", new Set(allAnnouncements.map(a => a.creator_id)).size)
|
||||
})
|
||||
.gap(0.85, em)
|
||||
})
|
||||
.paddingHorizontal(2, em).paddingTop(2, em).paddingBottom(1.5, em)
|
||||
.borderBottom("1px solid var(--divider)").flexShrink(0)
|
||||
|
||||
VStack(() => {
|
||||
p("📋")
|
||||
.margin(0).fontSize(2.2, em).opacity(0.15)
|
||||
p("Select an announcement to read it")
|
||||
.margin(0).marginTop(0.65, em).fontSize(0.9, em)
|
||||
.color("var(--headertext)").opacity(0.3).textAlign("center")
|
||||
|
||||
if (this.canPost) {
|
||||
button("+ Write Announcement")
|
||||
.marginTop(1.25, em)
|
||||
.paddingHorizontal(1.25, em).paddingVertical(0.55, em)
|
||||
.background("var(--quillred)").border("none")
|
||||
.borderRadius(0.5, em).color("white")
|
||||
.fontSize(0.88, em).fontWeight("600").cursor("pointer")
|
||||
.onClick((done) => {if(!done) return;
|
||||
this.composing = true
|
||||
this.draftText = ""
|
||||
this.rerender()
|
||||
})
|
||||
}
|
||||
})
|
||||
.flex(1).justifyContent("center").alignItems("center")
|
||||
})
|
||||
.height(100, pct).overflow("hidden")
|
||||
}
|
||||
|
||||
renderViewer() {
|
||||
const ann = this.announcement
|
||||
const isMe = ann.creator_id === global.profile.id
|
||||
const isEdited = ann.created !== ann.updated_at
|
||||
const author = this.getAuthor(ann.creator_id)
|
||||
const authorDisplay = isMe ? "You" : author
|
||||
|
||||
VStack(() => {
|
||||
// ── Header ────────────────────────────────────────────────
|
||||
HStack(() => {
|
||||
// Big avatar
|
||||
VStack(() => {
|
||||
p(this.getInitials(ann.creator_id))
|
||||
.margin(0).fontSize(0.95, em).fontWeight("700")
|
||||
.color("white").lineHeight("1")
|
||||
})
|
||||
.width(3.2, em).height(3.2, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(author))
|
||||
.justifyContent("center").alignItems("center").flexShrink(0)
|
||||
|
||||
VStack(() => {
|
||||
p(authorDisplay)
|
||||
.margin(0).fontSize(1, em).fontWeight("700")
|
||||
.color(isMe ? "var(--quillred)" : "var(--headertext)")
|
||||
|
||||
HStack(() => {
|
||||
p(this.formatDateTime(ann.created))
|
||||
.margin(0).fontSize(0.75, em)
|
||||
.color("var(--headertext)").opacity(0.4)
|
||||
if (isEdited) {
|
||||
p("· edited " + this.relativeDate(ann.updated_at))
|
||||
.margin(0).fontSize(0.72, em).fontStyle("italic")
|
||||
.color("var(--headertext)").opacity(0.3)
|
||||
}
|
||||
})
|
||||
.gap(0.45, em).alignItems("center").marginTop(0.15, em)
|
||||
})
|
||||
.flex(1)
|
||||
|
||||
// Actions
|
||||
if ((isMe && this.canEdit) || (isMe && this.canDelete)) {
|
||||
HStack(() => {
|
||||
if (isMe && this.canEdit) {
|
||||
button("Edit")
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.38, em)
|
||||
.background("transparent").border("1px solid var(--divider)")
|
||||
.borderRadius(0.45, em).color("var(--headertext)")
|
||||
.fontSize(0.8, em).cursor("pointer").opacity(0.65)
|
||||
.onClick((done) => {if(!done) return;
|
||||
this.editingId = ann.id
|
||||
this.draftText = ann.text
|
||||
this.rerender()
|
||||
})
|
||||
}
|
||||
if (isMe && this.canDelete) {
|
||||
if (this.confirmDeleteId === ann.id) {
|
||||
button("Confirm delete")
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.38, em)
|
||||
.background("rgba(239,68,68,0.1)").border("1px solid rgba(239,68,68,0.3)")
|
||||
.borderRadius(0.45, em).color("#ef4444")
|
||||
.fontSize(0.8, em).fontWeight("600").cursor("pointer")
|
||||
.onClick((done) => {if (!done) return; this.deleteAnnouncement(ann.id)})
|
||||
} else {
|
||||
button("Delete")
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.38, em)
|
||||
.background("transparent").border("1px solid var(--divider)")
|
||||
.borderRadius(0.45, em).color("var(--headertext)")
|
||||
.fontSize(0.8, em).cursor("pointer").opacity(0.5)
|
||||
.onClick((done) => {if(!done) return; this.confirmDeleteId = ann.id; this.rerender() })
|
||||
}
|
||||
}
|
||||
})
|
||||
.gap(0.45, em).alignItems("center")
|
||||
}
|
||||
})
|
||||
.gap(0.9, em).alignItems("flex-start")
|
||||
.paddingHorizontal(2, em).paddingTop(1.75, em).paddingBottom(1.35, em)
|
||||
.borderBottom("1px solid var(--divider)").flexShrink(0)
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────
|
||||
VStack(() => {
|
||||
p(ann.text)
|
||||
.margin(0)
|
||||
.fontSize(1, em)
|
||||
.lineHeight("1.75")
|
||||
.color("var(--headertext)")
|
||||
.whiteSpace("pre-wrap")
|
||||
.wordBreak("break-word")
|
||||
})
|
||||
.paddingHorizontal(2, em).paddingVertical(1.75, em)
|
||||
.flex(1).overflowY("auto")
|
||||
|
||||
// ── Footer: compose new ───────────────────────────────────
|
||||
if (this.canPost) {
|
||||
HStack(() => {
|
||||
button("+ New Announcement")
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.52, em)
|
||||
.background("var(--quillred)").border("none")
|
||||
.borderRadius(0.5, em).color("white")
|
||||
.fontSize(0.85, em).fontWeight("600").cursor("pointer")
|
||||
.flexShrink(0)
|
||||
.onClick((done) => {if(!done) return;
|
||||
this.composing = true
|
||||
this.draftText = ""
|
||||
this.rerender()
|
||||
})
|
||||
})
|
||||
.paddingHorizontal(2, em).paddingVertical(1, em)
|
||||
.borderTop("1px solid var(--divider)").flexShrink(0)
|
||||
.justifyContent("flex-end")
|
||||
}
|
||||
})
|
||||
.height(100, pct).overflow("hidden")
|
||||
}
|
||||
|
||||
renderCompose() {
|
||||
const isEdit = !!this.editingId
|
||||
|
||||
VStack(() => {
|
||||
// Header
|
||||
HStack(() => {
|
||||
VStack(() => {
|
||||
p(isEdit ? "Edit Announcement" : "New Announcement")
|
||||
.margin(0).fontSize(1.05, em).fontWeight("700").color("var(--headertext)")
|
||||
p(isEdit ? "Update your announcement below" : "Write something to share with the network")
|
||||
.margin(0).marginTop(0.15, em).fontSize(0.75, em)
|
||||
.color("var(--headertext)").opacity(0.4)
|
||||
})
|
||||
.flex(1)
|
||||
|
||||
button("✕")
|
||||
.border("none").background("transparent")
|
||||
.color("var(--headertext)").opacity(0.4)
|
||||
.fontSize(0.85, em).cursor("pointer").padding(0.3, em)
|
||||
.borderRadius(0.35, em)
|
||||
.onClick((done) => {if(!done) return;
|
||||
this.composing = false
|
||||
this.editingId = null
|
||||
this.draftText = ""
|
||||
this.errorMsg = ""
|
||||
this.rerender()
|
||||
})
|
||||
})
|
||||
.paddingHorizontal(2, em).paddingTop(1.75, em).paddingBottom(1.25, em)
|
||||
.borderBottom("1px solid var(--divider)").alignItems("flex-start").flexShrink(0)
|
||||
|
||||
// Compose area
|
||||
VStack(() => {
|
||||
// Author row
|
||||
HStack(() => {
|
||||
VStack(() => {
|
||||
p(this.getInitials(global.profile.id))
|
||||
.margin(0).fontSize(0.72, em).fontWeight("700")
|
||||
.color("white").lineHeight("1")
|
||||
})
|
||||
.width(2.2, em).height(2.2, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(this.getAuthor(global.profile.id)))
|
||||
.justifyContent("center").alignItems("center").flexShrink(0)
|
||||
|
||||
p(this.getAuthor(global.profile.id))
|
||||
.margin(0).fontSize(0.88, em).fontWeight("600")
|
||||
.color("var(--quillred)")
|
||||
})
|
||||
.gap(0.65, em).alignItems("center").marginBottom(1.1, em)
|
||||
|
||||
// Text area
|
||||
textarea(this.draftText)
|
||||
.attr({ placeholder: "What would you like to announce?", rows: 10, id: "compose-textarea" })
|
||||
.width(100, pct).boxSizing("border-box")
|
||||
.padding(1, em)
|
||||
.background("var(--darkaccent)")
|
||||
.border("1px solid var(--divider)")
|
||||
.borderRadius(0.55, em)
|
||||
.color("var(--headertext)")
|
||||
.fontSize(0.95, em).lineHeight("1.7")
|
||||
.outline("none").resize("none")
|
||||
.fontFamily("Arial")
|
||||
.onInput((e) => { this.draftText = e.target.value })
|
||||
|
||||
// Char count
|
||||
HStack(() => {
|
||||
if (this.errorMsg) {
|
||||
p(this.errorMsg)
|
||||
.margin(0).fontSize(0.75, em).color("#ef4444").fontWeight("500")
|
||||
}
|
||||
HStack(() => {}).flex(1)
|
||||
p(`${this.draftText.length} chars`)
|
||||
.margin(0).fontSize(0.72, em)
|
||||
.color("var(--headertext)").opacity(0.3)
|
||||
})
|
||||
.alignItems("center").marginTop(0.5, em)
|
||||
})
|
||||
.paddingHorizontal(2, em).paddingTop(1.5, em).flex(1).overflowY("auto")
|
||||
|
||||
// Actions
|
||||
HStack(() => {
|
||||
button("Cancel")
|
||||
.paddingHorizontal(1.1, em).paddingVertical(0.55, em)
|
||||
.background("transparent").border("1px solid var(--divider)")
|
||||
.borderRadius(0.5, em).color("var(--headertext)")
|
||||
.fontSize(0.88, em).cursor("pointer").opacity(0.6)
|
||||
.onClick((done) => {if(!done) return;
|
||||
this.composing = false
|
||||
this.editingId = null
|
||||
this.draftText = ""
|
||||
this.errorMsg = ""
|
||||
this.rerender()
|
||||
})
|
||||
|
||||
button(this.sending ? "Posting…" : (isEdit ? "Save Changes" : "Post Announcement"))
|
||||
.paddingHorizontal(1.25, em).paddingVertical(0.55, em)
|
||||
.background("var(--quillred)").border("none")
|
||||
.borderRadius(0.5, em).color("white")
|
||||
.fontSize(0.88, em).fontWeight("600")
|
||||
.cursor(this.sending ? "default" : "pointer")
|
||||
.opacity(this.sending ? 0.6 : 1)
|
||||
.onClick((done) => {if(!done) return; isEdit ? this.submitEdit() : this.submitNew()})
|
||||
})
|
||||
.gap(0.65, em).justifyContent("flex-end")
|
||||
.paddingHorizontal(2, em).paddingVertical(1.1, em)
|
||||
.borderTop("1px solid var(--divider)").flexShrink(0)
|
||||
})
|
||||
.height(100, pct).overflow("hidden")
|
||||
}
|
||||
|
||||
statCard(icon, label, value) {
|
||||
VStack(() => {
|
||||
HStack(() => {
|
||||
p(icon).margin(0).fontSize(1.1, em).lineHeight("1").flexShrink(0)
|
||||
p(String(value))
|
||||
.margin(0).fontSize(1.35, em).fontWeight("800")
|
||||
.color("var(--headertext)").lineHeight("1")
|
||||
})
|
||||
.gap(0.45, em).alignItems("center")
|
||||
p(label)
|
||||
.margin(0).marginTop(0.35, em).fontSize(0.72, em)
|
||||
.color("var(--headertext)").opacity(0.4).fontWeight("500")
|
||||
})
|
||||
.flex(1)
|
||||
.padding(0.9, em)
|
||||
.background("var(--darkaccent)")
|
||||
.border("1px solid var(--divider)")
|
||||
.borderRadius(0.55, em)
|
||||
.alignItems("flex-start")
|
||||
}
|
||||
|
||||
async submitNew() {
|
||||
if (this.sending) return
|
||||
const text = this.draftText.trim()
|
||||
if (!text) { this.errorMsg = "Announcement can't be empty."; this.rerender(); return }
|
||||
|
||||
this.sending = true
|
||||
this.errorMsg = ""
|
||||
this.rerender()
|
||||
|
||||
const result = await server.addAnnouncement(text, global.currentNetwork.id, global.profile.id)
|
||||
this.sending = false
|
||||
|
||||
if (result?.error) {
|
||||
this.errorMsg = result.error
|
||||
this.rerender()
|
||||
} else {
|
||||
this.composing = false
|
||||
this.draftText = ""
|
||||
this.onNew(result.announcement)
|
||||
}
|
||||
}
|
||||
|
||||
async submitEdit() {
|
||||
if (this.sending) return
|
||||
const text = this.draftText.trim()
|
||||
if (!text) { this.errorMsg = "Announcement can't be empty."; this.rerender(); return }
|
||||
|
||||
this.sending = true
|
||||
this.errorMsg = ""
|
||||
this.rerender()
|
||||
|
||||
const result = await server.editAnnouncement({ id: this.editingId, text }, global.profile.id)
|
||||
this.sending = false
|
||||
|
||||
if (result?.error) {
|
||||
this.errorMsg = result.error
|
||||
this.rerender()
|
||||
} else {
|
||||
this.editingId = null
|
||||
this.draftText = ""
|
||||
this.onEdited({ ...this.announcement, text, updated_at: new Date().toISOString() })
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAnnouncement(id) {
|
||||
const result = await server.deleteAnnouncement(id, global.profile.id)
|
||||
if (!result?.error) {
|
||||
this.confirmDeleteId = null
|
||||
this.onDeleted(id)
|
||||
}
|
||||
}
|
||||
|
||||
getAuthor(creatorId) {
|
||||
const members = global.currentNetwork.data?.members || []
|
||||
const m = members.find(m => m.id === creatorId)
|
||||
return m ? `${m.first_name} ${m.last_name}` : "Unknown"
|
||||
}
|
||||
|
||||
getInitials(creatorId) {
|
||||
const members = global.currentNetwork.data?.members || []
|
||||
const m = members.find(m => m.id === creatorId)
|
||||
if (!m) return "?"
|
||||
return [m.first_name?.[0], m.last_name?.[0]].filter(Boolean).join("").toUpperCase()
|
||||
}
|
||||
|
||||
formatDateTime(raw) {
|
||||
const d = new Date(raw)
|
||||
return d.toLocaleDateString([], { month: "long", day: "numeric", year: "numeric" })
|
||||
+ " at " + d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })
|
||||
}
|
||||
|
||||
relativeDate(raw) {
|
||||
const d = new Date(raw)
|
||||
const diff = Date.now() - d
|
||||
const mins = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
const days = Math.floor(diff / 86400000)
|
||||
if (mins < 1) return "just now"
|
||||
if (mins < 60) return `${mins}m ago`
|
||||
if (hours < 24) return `${hours}h ago`
|
||||
if (days === 1) return "yesterday"
|
||||
if (days < 7) return `${days}d ago`
|
||||
return d.toLocaleDateString([], { month: "short", day: "numeric" })
|
||||
}
|
||||
|
||||
avatarColor(name) {
|
||||
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"]
|
||||
let hash = 0
|
||||
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
}
|
||||
}
|
||||
|
||||
register(DesktopAnnouncementsViewer)
|
||||
178
announcements/desktop/announcements.js
Normal file
178
announcements/desktop/announcements.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import "./DesktopAnnouncementsFeed.js"
|
||||
import "./DesktopAnnouncementsViewer.js"
|
||||
import server from "/@server/server.js"
|
||||
|
||||
css(`
|
||||
announcements- {
|
||||
font-family: 'Arial';
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
announcements- input::placeholder {
|
||||
color: var(--headertext);
|
||||
opacity: 0.35;
|
||||
}
|
||||
announcements- textarea::placeholder {
|
||||
color: var(--headertext);
|
||||
opacity: 0.3;
|
||||
}
|
||||
`)
|
||||
|
||||
class Announcements extends Shadow {
|
||||
announcements = []
|
||||
selectedId = null
|
||||
searchText = ""
|
||||
|
||||
get canPost() { return global.currentNetwork.permissions.includes("announcements.add") }
|
||||
get canEdit() { return global.currentNetwork.permissions.includes("announcements.edit") }
|
||||
get canDelete() { return global.currentNetwork.permissions.includes("announcements.delete") }
|
||||
|
||||
get filtered() {
|
||||
if (!this.searchText) return this.announcements
|
||||
const q = this.searchText.toLowerCase()
|
||||
return this.announcements.filter(a => a.text.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
get selectedAnnouncement() {
|
||||
return this.announcements.find(a => a.id === this.selectedId) || null
|
||||
}
|
||||
|
||||
render() {
|
||||
HStack(() => {
|
||||
// ── Left: feed list ───────────────────────────────────────
|
||||
VStack(() => {
|
||||
// Toolbar
|
||||
HStack(() => {
|
||||
p("Announcements")
|
||||
.fontFamily("Laandbrau")
|
||||
.margin(0).fontSize(1.8, em).fontWeight("700").color("var(--headertext)")
|
||||
.flex(1)
|
||||
|
||||
if (this.canPost) {
|
||||
button("+ New")
|
||||
.paddingHorizontal(0.85, em).paddingVertical(0.42, em)
|
||||
.background("var(--quillred)").border("none")
|
||||
.borderRadius(0.45, em).color("white")
|
||||
.fontSize(0.82, em).fontWeight("600").cursor("pointer")
|
||||
.onClick((done) => {
|
||||
// Tell the viewer to open compose mode
|
||||
if(done) {
|
||||
this.selectedId = null
|
||||
this._openCompose = true
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.paddingHorizontal(1.1, em).paddingTop(1.1, em).paddingBottom(0.35, em)
|
||||
.alignItems("center").flexShrink(0)
|
||||
|
||||
DesktopAnnouncementsFeed(
|
||||
this.filtered,
|
||||
this.selectedId,
|
||||
this.searchText,
|
||||
(id) => {
|
||||
this.selectedId = id
|
||||
this._openCompose = false
|
||||
this.rerender()
|
||||
},
|
||||
(text) => {
|
||||
this.searchText = text
|
||||
this.rerender()
|
||||
}
|
||||
)
|
||||
.flex(1).minHeight(0).overflow("hidden")
|
||||
})
|
||||
.width(320, px).minWidth(280, px)
|
||||
.height(100, pct)
|
||||
.borderRight("1px solid var(--divider)")
|
||||
.background("var(--main)")
|
||||
.flexShrink(0).overflow("hidden")
|
||||
|
||||
// ── Right: viewer / composer ──────────────────────────────
|
||||
VStack(() => {
|
||||
const viewer = DesktopAnnouncementsViewer(
|
||||
this.selectedAnnouncement,
|
||||
this.canPost,
|
||||
this.canEdit,
|
||||
this.canDelete,
|
||||
// onNew
|
||||
(ann) => {
|
||||
this.announcements.unshift(ann)
|
||||
if (global.currentNetwork.data?.announcements) {
|
||||
global.currentNetwork.data.announcements.unshift(ann)
|
||||
}
|
||||
this.selectedId = ann.id
|
||||
this._openCompose = false
|
||||
this.rerender()
|
||||
window.dispatchEvent(new CustomEvent("new-announcement", { detail: { announcement: ann } }))
|
||||
},
|
||||
// onEdited
|
||||
(ann) => {
|
||||
const i = this.announcements.findIndex(a => a.id === ann.id)
|
||||
if (i !== -1) this.announcements[i] = ann
|
||||
if (global.currentNetwork.data?.announcements) {
|
||||
const gi = global.currentNetwork.data.announcements.findIndex(a => a.id === ann.id)
|
||||
if (gi !== -1) global.currentNetwork.data.announcements[gi] = ann
|
||||
}
|
||||
this.selectedId = ann.id
|
||||
this.rerender()
|
||||
window.dispatchEvent(new CustomEvent("edited-announcement", { detail: ann }))
|
||||
},
|
||||
// onDeleted
|
||||
(id) => {
|
||||
this.announcements = this.announcements.filter(a => a.id !== id)
|
||||
if (global.currentNetwork.data?.announcements) {
|
||||
global.currentNetwork.data.announcements = global.currentNetwork.data.announcements.filter(a => a.id !== id)
|
||||
}
|
||||
this.selectedId = null
|
||||
this.rerender()
|
||||
window.dispatchEvent(new CustomEvent("deleted-announcement", { detail: { id } }))
|
||||
}
|
||||
)
|
||||
|
||||
// If the "New" button was pressed, trigger compose mode immediately
|
||||
if (this._openCompose && viewer) {
|
||||
viewer.composing = true
|
||||
this._openCompose = false
|
||||
}
|
||||
})
|
||||
.flex(1).height(100, pct).background("var(--main)").overflow("hidden")
|
||||
})
|
||||
.height(100, pct).width(100, pct).overflow("hidden")
|
||||
.onAppear(async () => {
|
||||
const res = await server.getAnnouncements(global.currentNetwork.id)
|
||||
const data = Array.isArray(res) ? res : (res?.data || [])
|
||||
if (data.length !== this.announcements.length) {
|
||||
this.announcements = data.sort((a, b) => new Date(b.created) - new Date(a.created))
|
||||
if (global.currentNetwork.data) {
|
||||
global.currentNetwork.data.announcements = this.announcements
|
||||
}
|
||||
if (!this.selectedId && this.announcements.length) {
|
||||
this.selectedId = this.announcements[0].id
|
||||
}
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
.onEvent("new-announcement", (e) => {
|
||||
const ann = e.detail.announcement
|
||||
if (!this.announcements.find(a => a.id === ann.id)) {
|
||||
this.announcements.unshift(ann)
|
||||
this.rerender()
|
||||
}
|
||||
})
|
||||
.onEvent("deleted-announcement", (e) => {
|
||||
const id = e.detail.id
|
||||
if (this.selectedId === id) this.selectedId = null
|
||||
this.announcements = this.announcements.filter(a => a.id !== id)
|
||||
this.rerender()
|
||||
})
|
||||
.onEvent("edited-announcement", (e) => {
|
||||
const ann = e.detail
|
||||
const i = this.announcements.findIndex(a => a.id === ann.id)
|
||||
if (i !== -1) { this.announcements[i] = ann; this.rerender() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
register(Announcements)
|
||||
6
announcements/icons/announcements.svg
Normal file
6
announcements/icons/announcements.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="145" height="108" viewBox="0 0 145 108" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M94.7998 1.41992C95.3518 0.800579 96.2238 0.588052 96.9971 0.87793L97.0049 0.880859C97.7807 1.1815 98.2832 1.92754 98.2832 2.74902V2.75293L97.8105 89.9863V89.9922C97.8008 90.7646 97.3472 91.4731 96.6279 91.7939H96.627C96.3712 91.9076 96.0977 91.9678 95.8193 91.9678C95.3977 91.9677 94.9804 91.835 94.6338 91.5791L94.4902 91.4619C80.9575 79.3572 67.4789 72.082 53.2793 69.1533V69.1523C53.1181 69.1217 52.9787 69.0681 52.8623 69.0127H45.1064L54.0742 104.412L54.0752 104.414C54.2388 105.068 54.0657 105.759 53.6084 106.255L53.6074 106.254C53.224 106.674 52.689 106.896 52.1484 106.896C52.0382 106.896 51.9236 106.887 51.8125 106.868L51.8076 106.867L35.9531 104.1V104.099C35.1845 103.969 34.5656 103.397 34.3701 102.65L25.4648 69.0186H21.8037C20.7053 69.0185 19.8125 68.1258 19.8125 67.0273V62.9902C18.6413 63.2528 17.4362 63.3877 16.209 63.3877C7.74514 63.3877 0.75 57.022 0.75 49.0791C0.750129 41.1363 7.74522 34.7715 16.209 34.7715C17.438 34.7715 18.6432 34.9091 19.8125 35.1699V29.7266C19.8125 28.6282 20.7053 27.7345 21.8037 27.7344L54.249 27.7227C54.3928 27.7227 54.5276 27.7403 54.6533 27.7676C70.4266 22.6561 83.9135 13.813 94.7959 1.42383L94.7988 1.41992H94.7998ZM37.8945 100.4L49.4688 102.421L41.0088 69.0176H29.583L37.8945 100.4ZM94.2715 7.85059C83.6492 18.7246 70.8894 26.6414 56.2441 31.4492L56.2393 65.7441C69.2296 68.8183 81.5908 75.3647 93.8477 85.6455L94.2715 7.85059ZM23.7949 65.042H52.2568V32.6523C51.4474 32.7815 50.6525 32.3986 50.2412 31.7109H23.7949V65.042ZM16.208 38.7471C9.81156 38.7471 4.73252 43.4502 4.73242 49.0781C4.73242 54.7061 9.81136 59.4092 16.208 59.4092C17.4559 59.4092 18.6614 59.2301 19.8115 58.8818V39.2695C18.6609 38.9253 17.4552 38.7471 16.208 38.7471Z" fill="black" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M134.317 17.4297C135.222 16.9839 136.334 17.2863 136.881 18.1611C137.466 19.0977 137.171 20.3199 136.252 20.9004L136.249 20.9023L118.457 32.0215L118.458 32.0225C118.13 32.2314 117.758 32.3271 117.401 32.3271C116.743 32.3271 116.092 31.9987 115.713 31.3955L115.711 31.3936C115.129 30.4614 115.411 29.2354 116.344 28.6523L134.14 17.5283L134.317 17.4297Z" fill="black" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M142.259 44.5488C143.345 44.5491 144.24 45.4224 144.25 46.5215C144.26 47.6228 143.373 48.5156 142.277 48.5254H142.276L121.196 48.6826H121.179C120.084 48.6825 119.197 47.8067 119.188 46.71C119.178 45.6087 120.065 44.7158 121.16 44.7061H121.161L142.24 44.5488H142.259Z" fill="black" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M116.345 60.7305C116.984 59.9179 118.126 59.7411 118.977 60.2812L119.143 60.3984L119.144 60.3994L136.321 73.957L136.476 74.0908C137.203 74.7933 137.289 75.9454 136.653 76.7539L136.652 76.7529C136.262 77.2507 135.678 77.5127 135.089 77.5127C134.655 77.5127 134.223 77.3758 133.854 77.085V77.084L116.676 63.5273V63.5264C115.81 62.8445 115.667 61.5924 116.345 60.7305Z" fill="black" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
6
announcements/icons/announcementslight.svg
Normal file
6
announcements/icons/announcementslight.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="145" height="108" viewBox="0 0 145 108" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M94.7998 1.41992C95.3518 0.800579 96.2238 0.588052 96.9971 0.87793L97.0049 0.880859C97.7807 1.1815 98.2832 1.92754 98.2832 2.74902V2.75293L97.8105 89.9863V89.9922C97.8008 90.7646 97.3472 91.4731 96.6279 91.7939H96.627C96.3712 91.9076 96.0977 91.9678 95.8193 91.9678C95.3977 91.9677 94.9804 91.835 94.6338 91.5791L94.4902 91.4619C80.9575 79.3572 67.4789 72.082 53.2793 69.1533V69.1523C53.1181 69.1217 52.9787 69.0681 52.8623 69.0127H45.1064L54.0742 104.412L54.0752 104.414C54.2388 105.068 54.0657 105.759 53.6084 106.255L53.6074 106.254C53.224 106.674 52.689 106.896 52.1484 106.896C52.0382 106.896 51.9236 106.887 51.8125 106.868L51.8076 106.867L35.9531 104.1V104.099C35.1845 103.969 34.5656 103.397 34.3701 102.65L25.4648 69.0186H21.8037C20.7053 69.0185 19.8125 68.1258 19.8125 67.0273V62.9902C18.6413 63.2528 17.4362 63.3877 16.209 63.3877C7.74514 63.3877 0.75 57.022 0.75 49.0791C0.750129 41.1363 7.74522 34.7715 16.209 34.7715C17.438 34.7715 18.6432 34.9091 19.8125 35.1699V29.7266C19.8125 28.6282 20.7053 27.7345 21.8037 27.7344L54.249 27.7227C54.3928 27.7227 54.5276 27.7403 54.6533 27.7676C70.4266 22.6561 83.9135 13.813 94.7959 1.42383L94.7988 1.41992H94.7998ZM37.8945 100.4L49.4688 102.421L41.0088 69.0176H29.583L37.8945 100.4ZM94.2715 7.85059C83.6492 18.7246 70.8894 26.6414 56.2441 31.4492L56.2393 65.7441C69.2296 68.8183 81.5908 75.3647 93.8477 85.6455L94.2715 7.85059ZM23.7949 65.042H52.2568V32.6523C51.4474 32.7815 50.6525 32.3986 50.2412 31.7109H23.7949V65.042ZM16.208 38.7471C9.81156 38.7471 4.73252 43.4502 4.73242 49.0781C4.73242 54.7061 9.81136 59.4092 16.208 59.4092C17.4559 59.4092 18.6614 59.2301 19.8115 58.8818V39.2695C18.6609 38.9253 17.4552 38.7471 16.208 38.7471Z" fill="#FEE9C9" stroke="#FEE9C9" stroke-width="1.5"/>
|
||||
<path d="M134.317 17.4297C135.222 16.9839 136.334 17.2863 136.881 18.1611C137.466 19.0977 137.171 20.3199 136.252 20.9004L136.249 20.9023L118.457 32.0215L118.458 32.0225C118.13 32.2314 117.758 32.3271 117.401 32.3271C116.743 32.3271 116.092 31.9987 115.713 31.3955L115.711 31.3936C115.129 30.4614 115.411 29.2354 116.344 28.6523L134.14 17.5283L134.317 17.4297Z" fill="#FEE9C9" stroke="#FEE9C9" stroke-width="1.5"/>
|
||||
<path d="M142.259 44.5488C143.345 44.5491 144.24 45.4224 144.25 46.5215C144.26 47.6228 143.373 48.5156 142.277 48.5254H142.276L121.196 48.6826H121.179C120.084 48.6825 119.197 47.8067 119.188 46.71C119.178 45.6087 120.065 44.7158 121.16 44.7061H121.161L142.24 44.5488H142.259Z" fill="#FEE9C9" stroke="#FEE9C9" stroke-width="1.5"/>
|
||||
<path d="M116.345 60.7305C116.984 59.9179 118.126 59.7411 118.977 60.2812L119.143 60.3984L119.144 60.3994L136.321 73.957L136.476 74.0908C137.203 74.7933 137.289 75.9454 136.653 76.7539L136.652 76.7529C136.262 77.2507 135.678 77.5127 135.089 77.5127C134.655 77.5127 134.223 77.3758 133.854 77.085V77.084L116.676 63.5273V63.5264C115.81 62.8445 115.667 61.5924 116.345 60.7305Z" fill="#FEE9C9" stroke="#FEE9C9" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
1
announcements/icons/announcementslightselected.svg
Normal file
1
announcements/icons/announcementslightselected.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="27" viewBox="0 0 24 27" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m.5 8.95076v16.66664h23v-16.66664l-11.5-8.333328z" fill="#74000a" stroke="#ffe9c8"/><path d="m15.8357 25.6171v-10.4167h-7.66663v10.4167" fill="#74000a"/><path d="m15.8357 25.6171v-10.4167h-7.66663v10.4167" stroke="#ffe9c8"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
Reference in New Issue
Block a user