summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.example.toml3
-rwxr-xr-xpublic/css/style.css39
-rw-r--r--public/js/modules/event-edit.js2
-rw-r--r--public/js/modules/group-edit.js5
-rw-r--r--public/js/modules/new.js7
-rw-r--r--src/lib/config.ts66
-rw-r--r--src/models/Event.ts5
-rwxr-xr-xsrc/models/EventGroup.ts5
-rw-r--r--src/routes/event.ts2
-rw-r--r--src/routes/frontend.ts71
-rw-r--r--src/routes/group.ts2
-rw-r--r--src/util/validation.ts31
-rwxr-xr-xviews/event.handlebars3
-rwxr-xr-xviews/eventgroup.handlebars1
-rwxr-xr-xviews/home.handlebars44
-rwxr-xr-xviews/layouts/main.handlebars2
-rwxr-xr-xviews/partials/eventForm.handlebars8
-rw-r--r--views/partials/eventGroupForm.handlebars11
-rwxr-xr-xviews/partials/sidebar.handlebars12
-rw-r--r--views/publicEventList.handlebars72
20 files changed, 365 insertions, 26 deletions
diff --git a/config/config.example.toml b/config/config.example.toml
index e9995de..4e00171 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -16,6 +16,9 @@ email_logo_url = ""
# Show a Ko-Fi box to donate money to Raphael (Gathio's creator) on the front
# page.
show_kofi = false
+# Show a list of events and groups on the front page which have been marked as
+# 'Display this event/group on the public event/group list'.
+show_public_event_list = false
# Which mail service to use to send emails to hosts and attendees. Options are
# 'nodemailer' or 'sendgrid'. Configure settings for this mail
# service below.
diff --git a/public/css/style.css b/public/css/style.css
index dd59d6b..24d10b8 100755
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -381,7 +381,7 @@ li.hidden-attendee .attendee-name {
}
}
-@media (min-width: 577px) {
+@media (min-width: 576px) {
#sidebar {
border-right: 2px solid #e0e0e0;
min-height: 100vh;
@@ -422,7 +422,7 @@ li.hidden-attendee .attendee-name {
}
.list-group-item-action:hover {
- background-color: #d4edda;
+ background-color: #f2f8ff;
}
.code {
@@ -548,3 +548,38 @@ img.group-preview__image {
opacity: 1;
pointer-events: auto;
}
+
+ul#sidebar__nav {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1rem;
+}
+
+ul#sidebar__nav li {
+ padding: 0 1rem 0.5rem 1rem;
+ text-align: center;
+}
+
+ul#sidebar__nav a {
+ display: block;
+ width: 100%;
+}
+
+@media (min-width: 576px) {
+ ul#sidebar__nav {
+ flex-direction: column;
+ }
+ ul#sidebar__nav li {
+ width: 100%;
+ padding: 0 0 0.5rem 0;
+ }
+ ul#sidebar__nav li:has(a:not(.btn)):not(:last-child) {
+ border-bottom: 1px solid #e0e0e0;
+ }
+}
diff --git a/public/js/modules/event-edit.js b/public/js/modules/event-edit.js
index 740c861..736547f 100644
--- a/public/js/modules/event-edit.js
+++ b/public/js/modules/event-edit.js
@@ -32,6 +32,7 @@ function editEventForm() {
creatorEmail: window.eventData.creatorEmail,
eventGroupID: window.eventData.eventGroupID,
eventGroupEditToken: window.eventData.eventGroupEditToken,
+ publicCheckbox: window.eventData.showOnPublicList,
interactionCheckbox: window.eventData.usersCanComment,
joinCheckbox: window.eventData.usersCanAttend,
maxAttendeesCheckbox: window.eventData.maxAttendees !== null,
@@ -53,6 +54,7 @@ function editEventForm() {
this.data.joinCheckbox = window.eventData.usersCanAttend;
this.data.maxAttendeesCheckbox =
window.eventData.maxAttendees !== null;
+ this.data.publicCheckbox = window.eventData.showOnPublicList;
},
async submitForm() {
this.submitting = true;
diff --git a/public/js/modules/group-edit.js b/public/js/modules/group-edit.js
index 1a2c1db..2d55346 100644
--- a/public/js/modules/group-edit.js
+++ b/public/js/modules/group-edit.js
@@ -27,6 +27,11 @@ function editEventGroupForm() {
eventGroupURL: window.groupData.url,
hostName: window.groupData.hostName,
creatorEmail: window.groupData.creatorEmail,
+ publicCheckbox: window.groupData.showOnPublicList,
+ },
+ init() {
+ // Set checkboxes
+ this.data.publicCheckbox = window.groupData.showOnPublicList;
},
errors: [],
submitting: false,
diff --git a/public/js/modules/new.js b/public/js/modules/new.js
index a018087..f7c3e34 100644
--- a/public/js/modules/new.js
+++ b/public/js/modules/new.js
@@ -87,6 +87,7 @@ function newEventForm() {
creatorEmail: "",
eventGroupID: "",
eventGroupEditToken: "",
+ publicCheckbox: false,
interactionCheckbox: false,
joinCheckbox: false,
maxAttendeesCheckbox: false,
@@ -107,6 +108,7 @@ function newEventForm() {
this.data.interactionCheckbox = false;
this.data.joinCheckbox = false;
this.data.maxAttendeesCheckbox = false;
+ this.data.publicCheckbox = false;
},
async submitForm() {
this.submitting = true;
@@ -160,6 +162,11 @@ function newEventGroupForm() {
eventGroupURL: "",
hostName: "",
creatorEmail: "",
+ publicCheckbox: false,
+ },
+ init() {
+ // Reset checkboxes
+ this.data.publicCheckbox = false;
},
errors: [],
submitting: false,
diff --git a/src/lib/config.ts b/src/lib/config.ts
index 1029be9..b4385ca 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -14,10 +14,11 @@ interface GathioConfig {
port: string;
email: string;
site_name: string;
- delete_after_days: number | null;
+ delete_after_days: number;
is_federated: boolean;
email_logo_url: string;
show_kofi: boolean;
+ show_public_event_list: boolean;
mail_service: "nodemailer" | "sendgrid";
creator_email_addresses: string[];
};
@@ -42,6 +43,7 @@ interface FrontendConfig {
isFederated: boolean;
emailLogoUrl: string;
showKofi: boolean;
+ showPublicEventList: boolean;
showInstanceInformation: boolean;
staticPages?: StaticPage[];
version: string;
@@ -56,8 +58,10 @@ const defaultConfig: GathioConfig = {
is_federated: true,
delete_after_days: 7,
email_logo_url: "",
+ show_public_event_list: false,
show_kofi: false,
mail_service: "nodemailer",
+ creator_email_addresses: [],
},
database: {
mongodb_url: "mongodb://localhost:27017/gathio",
@@ -69,15 +73,71 @@ export const frontendConfig = (): FrontendConfig => {
return {
domain: config.general.domain,
siteName: config.general.site_name,
- isFederated: config.general.is_federated,
+ isFederated: !!config.general.is_federated,
emailLogoUrl: config.general.email_logo_url,
- showKofi: config.general.show_kofi,
+ showPublicEventList: !!config.general.show_public_event_list,
+ showKofi: !!config.general.show_kofi,
showInstanceInformation: !!config.static_pages?.length,
staticPages: config.static_pages,
version: process.env.npm_package_version || "unknown",
};
};
+interface InstanceRule {
+ icon: string;
+ text: string;
+}
+
+export const instanceRules = (): InstanceRule[] => {
+ const config = getConfig();
+ const rules = [];
+ rules.push(
+ config.general.show_public_event_list
+ ? {
+ text: "Public events and groups are displayed on the homepage",
+ icon: "fas fa-eye",
+ }
+ : {
+ text: "Events and groups can only be accessed by direct link",
+ icon: "fas fa-eye-slash",
+ },
+ );
+ rules.push(
+ config.general.creator_email_addresses?.length
+ ? {
+ text: "Only specific people can create events and groups",
+ icon: "fas fa-user-check",
+ }
+ : {
+ text: "Anyone can create events and groups",
+ icon: "fas fa-users",
+ },
+ );
+ rules.push(
+ config.general.delete_after_days > 0
+ ? {
+ text: `Events are automatically deleted ${config.general.delete_after_days} days after they end`,
+ icon: "far fa-calendar-times",
+ }
+ : {
+ text: "Events are permanent, and are never automatically deleted",
+ icon: "far fa-calendar-check",
+ },
+ );
+ rules.push(
+ config.general.is_federated
+ ? {
+ text: "This instance federates with other instances using ActivityPub",
+ icon: "fas fa-globe",
+ }
+ : {
+ text: "This instance does not federate with other instances",
+ icon: "fas fa-globe",
+ },
+ );
+ return rules;
+};
+
// Attempt to load our global config. Will stop the app if the config file
// cannot be read (there's no point trying to continue!)
export const getConfig = (): GathioConfig => {
diff --git a/src/models/Event.ts b/src/models/Event.ts
index f67d40b..5731680 100644
--- a/src/models/Event.ts
+++ b/src/models/Event.ts
@@ -73,6 +73,7 @@ export interface IEvent extends mongoose.Document {
privateKey?: string;
followers?: IFollower[];
activityPubMessages?: IActivityPubMessage[];
+ showOnPublicList?: boolean;
}
const Attendees = new mongoose.Schema({
@@ -334,6 +335,10 @@ const EventSchema = new mongoose.Schema({
},
followers: [Followers],
activityPubMessages: [ActivityPubMessages],
+ showOnPublicList: {
+ type: Boolean,
+ default: false,
+ },
});
export default mongoose.model<IEvent>("Event", EventSchema);
diff --git a/src/models/EventGroup.ts b/src/models/EventGroup.ts
index 2b5c2aa..de7187d 100755
--- a/src/models/EventGroup.ts
+++ b/src/models/EventGroup.ts
@@ -16,6 +16,7 @@ export interface IEventGroup extends mongoose.Document {
firstLoad?: boolean;
events?: mongoose.Types.ObjectId[];
subscribers?: ISubscriber[];
+ showOnPublicList?: boolean;
}
const Subscriber = new mongoose.Schema({
@@ -70,6 +71,10 @@ const EventGroupSchema = new mongoose.Schema({
},
events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }],
subscribers: [Subscriber],
+ showOnPublicList: {
+ type: Boolean,
+ default: false,
+ },
});
export default mongoose.model<IEventGroup>("EventGroup", EventGroupSchema);
diff --git a/src/routes/event.ts b/src/routes/event.ts
index fb9d8c7..6be5ff8 100644
--- a/src/routes/event.ts
+++ b/src/routes/event.ts
@@ -140,6 +140,7 @@ router.post(
viewPassword: "", // Backwards compatibility
editPassword: "", // Backwards compatibility
editToken: editToken,
+ showOnPublicList: eventData?.publicBoolean,
eventGroup: isPartOfEventGroup ? eventGroup?._id : null,
usersCanAttend: eventData.joinBoolean ? true : false,
showUsersList: false, // Backwards compatibility
@@ -371,6 +372,7 @@ router.put(
url: eventData.eventURL,
hostName: eventData.hostName,
image: eventImageFilename,
+ showOnPublicList: eventData.publicBoolean,
usersCanAttend: eventData.joinBoolean,
showUsersList: false, // Backwards compatibility
usersCanComment: eventData.interactionBoolean,
diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts
index 0d8793a..4cdce8a 100644
--- a/src/routes/frontend.ts
+++ b/src/routes/frontend.ts
@@ -2,7 +2,7 @@ import { Router, Request, Response } from "express";
import moment from "moment-timezone";
import { marked } from "marked";
import { markdownToSanitizedHTML, renderPlain } from "../util/markdown.js";
-import getConfig, { frontendConfig } from "../lib/config.js";
+import getConfig, { frontendConfig, instanceRules } from "../lib/config.js";
import { addToLog, exportICal } from "../helpers.js";
import Event from "../models/Event.js";
import EventGroup, { IEventGroup } from "../models/EventGroup.js";
@@ -16,7 +16,20 @@ const config = getConfig();
const router = Router();
router.get("/", (_: Request, res: Response) => {
- res.render("home", frontendConfig());
+ if (config.general.show_public_event_list) {
+ return res.redirect("/events");
+ }
+ return res.render("home", {
+ ...frontendConfig(),
+ instanceRules: instanceRules(),
+ });
+});
+
+router.get("/about", (_: Request, res: Response) => {
+ return res.render("home", {
+ ...frontendConfig(),
+ instanceRules: instanceRules(),
+ });
});
router.get("/new", (_: Request, res: Response) => {
@@ -57,6 +70,51 @@ router.get("/new/:magicLinkToken", async (req: Request, res: Response) => {
});
});
+router.get("/events", async (_: Request, res: Response) => {
+ if (!config.general.show_public_event_list) {
+ return res.status(404).render("404", frontendConfig());
+ }
+ const events = await Event.find({ showOnPublicList: true })
+ .populate("eventGroup")
+ .lean()
+ .sort("start");
+ const updatedEvents = events.map((event) => {
+ const startMoment = moment.tz(event.start, event.timezone);
+ const endMoment = moment.tz(event.end, event.timezone);
+ const isSameDay = startMoment.isSame(endMoment, "day");
+
+ return {
+ id: event.id,
+ name: event.name,
+ location: event.location,
+ displayDate: isSameDay
+ ? startMoment.format("D MMM YYYY")
+ : `${startMoment.format("D MMM YYYY")} - ${endMoment.format(
+ "D MMM YYYY",
+ )}`,
+ eventHasConcluded: endMoment.isBefore(moment.tz(event.timezone)),
+ eventGroup: event.eventGroup,
+ };
+ });
+ const upcomingEvents = updatedEvents.filter(
+ (event) => event.eventHasConcluded === false,
+ );
+ const pastEvents = updatedEvents.filter(
+ (event) => event.eventHasConcluded === true,
+ );
+ const eventGroups = await EventGroup.find({
+ showOnPublicList: true,
+ }).lean();
+
+ res.render("publicEventList", {
+ title: "Public events",
+ upcomingEvents: upcomingEvents,
+ pastEvents: pastEvents,
+ eventGroups: eventGroups,
+ ...frontendConfig(),
+ });
+});
+
router.get("/:eventID", async (req: Request, res: Response) => {
try {
const event = await Event.findOne({
@@ -266,6 +324,11 @@ router.get("/:eventID", async (req: Request, res: Response) => {
firstLoad: firstLoad,
eventHasConcluded: eventHasConcluded,
eventHasBegun: eventHasBegun,
+ eventWillBeDeleted: config.general.delete_after_days > 0,
+ daysUntilDeletion: moment
+ .tz(event.end, event.timezone)
+ .add(config.general.delete_after_days, "days")
+ .fromNow(),
metadata: metadata,
jsonData: {
name: event.name,
@@ -276,6 +339,7 @@ router.get("/:eventID", async (req: Request, res: Response) => {
url: event.url,
hostName: event.hostName,
creatorEmail: event.creatorEmail,
+ showOnPublicList: event.showOnPublicList,
eventGroupID: event.eventGroup
? (event.eventGroup as unknown as IEventGroup).id
: null,
@@ -337,6 +401,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => {
return {
id: event.id,
name: event.name,
+ location: event.location,
displayDate: isSameDay
? startMoment.format("D MMM YYYY")
: `${startMoment.format("D MMM YYYY")} - ${endMoment.format(
@@ -388,6 +453,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => {
res.set("X-Robots-Tag", "noindex");
res.render("eventgroup", {
+ ...frontendConfig(),
domain: config.general.domain,
title: eventGroup.name,
eventGroupData: eventGroup,
@@ -409,6 +475,7 @@ router.get("/group/:eventGroupID", async (req: Request, res: Response) => {
creatorEmail: eventGroup.creatorEmail,
image: eventGroup.image,
editToken: editingEnabled ? eventGroupEditToken : null,
+ showOnPublicList: eventGroup.showOnPublicList,
},
});
} catch (err) {
diff --git a/src/routes/group.ts b/src/routes/group.ts
index 34377b0..c006a5d 100644
--- a/src/routes/group.ts
+++ b/src/routes/group.ts
@@ -81,6 +81,7 @@ router.post(
hostName: groupData.hostName,
editToken: editToken,
firstLoad: true,
+ showOnPublicList: groupData.publicBoolean,
});
await eventGroup.save();
@@ -206,6 +207,7 @@ router.put(
url: req.body.eventGroupURL,
hostName: req.body.hostName,
image: eventGroupImageFilename,
+ showOnPublicList: groupData.publicBoolean,
};
await EventGroup.findOneAndUpdate(
diff --git a/src/util/validation.ts b/src/util/validation.ts
index 732fbf3..b9a0c8a 100644
--- a/src/util/validation.ts
+++ b/src/util/validation.ts
@@ -5,11 +5,16 @@ type Error = {
field?: string;
};
-type ValidationResponse = {
+type EventValidationResponse = {
data?: ValidatedEventData;
errors?: Error[];
};
+type EventGroupValidationResponse = {
+ data?: ValidatedEventGroupData;
+ errors?: Error[];
+};
+
interface EventData {
eventName: string;
eventLocation: string;
@@ -21,6 +26,7 @@ interface EventData {
imagePath: string;
hostName: string;
creatorEmail: string;
+ publicCheckbox: string;
eventGroupCheckbox: string;
eventGroupID: string;
eventGroupEditToken: string;
@@ -33,11 +39,13 @@ interface EventData {
// EventData without the 'checkbox' fields
export type ValidatedEventData = Omit<
EventData,
+ | "publicCheckbox"
| "eventGroupCheckbox"
| "interactionCheckbox"
| "joinCheckbox"
| "maxAttendeesCheckbox"
> & {
+ publicBoolean: boolean;
eventGroupBoolean: boolean;
interactionBoolean: boolean;
joinBoolean: boolean;
@@ -50,8 +58,13 @@ interface EventGroupData {
eventGroupURL: string;
hostName: string;
creatorEmail: string;
+ publicCheckbox: string;
}
+export type ValidatedEventGroupData = Omit<EventGroupData, "publicCheckbox"> & {
+ publicBoolean: boolean;
+};
+
const validateEmail = (email: string) => {
if (!email || email.length === 0 || typeof email !== "string") {
return false;
@@ -89,9 +102,12 @@ export const validateEventTime = (start: Date, end: Date): Error | boolean => {
return true;
};
-export const validateEventData = (eventData: EventData): ValidationResponse => {
+export const validateEventData = (
+ eventData: EventData,
+): EventValidationResponse => {
const validatedData: ValidatedEventData = {
...eventData,
+ publicBoolean: eventData.publicCheckbox === "true",
eventGroupBoolean: eventData.eventGroupCheckbox === "true",
interactionBoolean: eventData.interactionCheckbox === "true",
joinBoolean: eventData.joinCheckbox === "true",
@@ -186,7 +202,9 @@ export const validateEventData = (eventData: EventData): ValidationResponse => {
};
};
-export const validateGroupData = (groupData: EventGroupData) => {
+export const validateGroupData = (
+ groupData: EventGroupData,
+): EventGroupValidationResponse => {
const errors: Error[] = [];
if (!groupData.eventGroupName) {
errors.push({
@@ -209,8 +227,13 @@ export const validateGroupData = (groupData: EventGroupData) => {
}
}
+ const validatedData: ValidatedEventGroupData = {
+ ...groupData,
+ publicBoolean: groupData.publicCheckbox === "true",
+ };
+
return {
- data: groupData,
+ data: validatedData,
errors: errors,
};
};
diff --git a/views/event.handlebars b/views/event.handlebars
index 4402578..44c2f4b 100755
--- a/views/event.handlebars
+++ b/views/event.handlebars
@@ -109,7 +109,7 @@
{{#if eventHasConcluded}}
<div class="alert alert-warning mb-4" role="alert">
- This event has concluded. It can no longer be edited, and will be automatically deleted <span class="daysToDeletion"></span>.
+ This event has concluded. It can no longer be edited{{#if eventWillBeDeleted}}, and will be automatically deleted {{daysUntilDeletion}}{{/if}}.
</div>
{{/if}}
{{#if firstLoad}}
@@ -523,7 +523,6 @@ window.eventData = {{{ json jsonData }}};
$(this).html('<i class="fas fa-copy"></i> Copied!');
setTimeout(function(){ $("#copyAPLink").html('<i class="fas fa-copy"></i> Copy');}, 5000);
})
- $(".daysToDeletion").html(moment("{{eventEndISO}}").add(7, 'days').fromNow());
if ($("#joinCheckbox").is(':checked')){
$("#maxAttendeesCheckboxContainer").css("display","flex");
}
diff --git a/views/eventgroup.handlebars b/views/eventgroup.handlebars
index 434f691..7ad3570 100755
--- a/views/eventgroup.handlebars
+++ b/views/eventgroup.handlebars
@@ -122,6 +122,7 @@
<a href="/{{this.id}}" class="list-group-item list-group-item-action" target="_blank">
<i class="fas fa-fw fa-calendar-day"></i>
<strong>{{this.name}}</strong>
+ {{#if this.location}}<span class="ml-2 text-muted"><i class="fas fa-map-marker-alt"></i> {{this.location}}</span>{{/if}}
<span class="ml-2 text-muted">{{this.displayDate}}</span>
</a>
{{/unless}}
diff --git a/views/home.handlebars b/views/home.handlebars
index add7eac..bf92724 100755
--- a/views/home.handlebars
+++ b/views/home.handlebars
@@ -1,25 +1,53 @@
-<p class="lead">A quick and easy way to make and share events which respects your privacy.</p>
+<p class="lead">Gathio is a simple, federated, privacy-first event hosting platform.</p>
-<hr>
+<div class="card mb-3 border-primary">
+<div class="card-header">
+ This instance, <strong>{{siteName}}</strong>, has the following features:
+</div>
-<p>You don't need to sign up for an account. When you create an event, we generate a password which allows you to edit the event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password. A week after the event finishes, it's deleted from our servers for ever, and your email goes with it.</p>
+<div class="card-body m-0 p-0">
+ <ul class="list-group list-group-flush">
+ {{#each instanceRules}}
+ <li class="list-group-item"><i class="{{this.icon}} fa-fw mr-2"></i> {{this.text}}</li>
+ {{/each}}
+ </ul>
+</div>
+</div>
<div id="example-event" class="text-center w-100 mt-4 mb-5">
<img alt ="An example event page for a picnic. The page shows the event's location, host, date and time, and description, as well as buttons to save the event to Google Calendar, export it, and open the location in OpenStreetMap and Google Maps." src="images/example-event-2023.png" class="img-fluid w-75 mx-auto shadow-lg rounded">
</div>
-<p>Also, we don't show you ads, don't sell your data, and never send you unnecessary emails.</p>
+<h3>Privacy-first</h3>
+
+<p>There are no accounts on Gathio. When you create an event, we generate a password which allows you to edit the event. Send all your guests the public link, and all your co-hosts the secret editing link containing the password.</p>
+
+<p>If you supply your email, we'll send you the editing password so you don't lose it - but supplying your email is optional!</p>
+
+<p>If this instance automatically deletes its events, sometime after the event finishes, it's deleted from the database for ever, and your data goes with it.</p>
+
+<p>Also, Gathio doesn't show you ads, doesn't sell your data, and never sends you unnecessary emails.</p>
+
+<p>But remember: all events are visible to anyone who knows the link, so probably don't use Gathio to plot your surprise birthday party or revolution. Or whatever, you do you.</p>
+
+<h3>Configurable</h3>
+
+<p>The <a href="https://gath.io">flagship Gathio instance at gath.io</a> is designed for anyone to create ephemeral, hidden events. Anyone can create an event; events are never displayed anywhere public; and they're deleted 7 days after they end.</p>
+
+<p>But if your community sets up their own instance, you can limit event creation to a specific list of people, display events on a handy list on the homepage, and disable event deletion entirely!</p>
+
+<h3>Federation and self-hosting</h3>
-<p>But remember: all events are visible to anyone who knows the link, so probably don't use <strong>gathio</strong> to plot your surprise birthday party or revolution. Or whatever, you do you.</p>
+<p>Gathio can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your community. Detailed instructions on <a href="https://github.com/lowercasename/gathio/wiki/Fediverse-Instructions">ActivityPub access</a> and <a href="https://github.com/lowercasename/gathio/wiki/Installation-instructions">self-hosted installation</a> live on our GitHub wiki.
-<h4 class="pt-2">Federation and self-hosting</h4>
+<h3>Open source</h3>
-<p><strong>gathio</strong> can easily be self-hosted, and supports ActivityPub services like Mastodon, Pleroma, and Friendica, allowing you to access events from anywhere on the Fediverse. We encourage you to spin up your own instance for your community. Detailed instructions on <a href="https://github.com/lowercasename/gathio/wiki/Fediverse-Instructions">ActivityPub access</a> and <a href="https://github.com/lowercasename/gathio/wiki/Installation-instructions">self-hosted installation</a> live on our GitHub wiki. Leave a question in our <a href="https://github.com/lowercasename/gathio/issues">tracker</a> if you encounter any issues.</p>
+<p>Gathio is delighted to be open source, and is built by a lovely group of people. Leave a question in our <a href="https://github.com/lowercasename/gathio/issues">tracker</a> if you encounter any issues.</p>
{{#if showKofi}}
<div class="card border-secondary mt-5 mb-3 mx-auto" style="min-width:300px;max-width:50%;">
<div class="card-body text-secondary">
- <p>If you find yourself using and enjoying <strong>gath<span class="text-muted">io</span></strong>, consider buying me a coffee. It'll help keep the site running! <i class="far fa-heart"></i></p>
+ <p>If you find yourself using and enjoying Gathio, consider buying Raphael a coffee. It'll help keep the project and main site running! <i class="far fa-heart"></i></p>
<script type='text/javascript' src='https://ko-fi.com/widgets/widget_2.js'></script><script type='text/javascript'>kofiwidget2.init('Support Me on Ko-fi', '#46b798', 'Q5Q2O7T5');kofiwidget2.draw();</script>
</div>
</div>
diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars
index d45d596..996d35f 100755
--- a/views/layouts/main.handlebars
+++ b/views/layouts/main.handlebars
@@ -70,7 +70,7 @@
<div id="footerContainer">
{{#if showInstanceInformation}}
<p class="small text-muted">
- <strong>{{domain}}</strong>
+ <strong>{{siteName}}</strong>
{{#each staticPages}}
{{#if @first}}
&middot;
diff --git a/views/partials/eventForm.handlebars b/views/partials/eventForm.handlebars
index c2eebc3..9227300 100755
--- a/views/partials/eventForm.handlebars
+++ b/views/partials/eventForm.handlebars
@@ -73,6 +73,14 @@
<div class="form-group">
<label>Options</label>
<div >
+ {{#if showPublicEventList}}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="publicEventCheckbox" name="publicCheckbox" x-model="data.publicCheckbox">
+ <label class="form-check-label" for="publicEventCheckbox">
+ Display this event on the public event list
+ </label>
+ </div>
+ {{/if}}
<div class="form-check">
<input class="form-check-input" type="checkbox" id="eventGroupCheckbox" name="eventGroupCheckbox" x-model="data.eventGroupCheckbox">
<label class="form-check-label" for="eventGroupCheckbox">
diff --git a/views/partials/eventGroupForm.handlebars b/views/partials/eventGroupForm.handlebars
index 258c321..284343f 100644
--- a/views/partials/eventGroupForm.handlebars
+++ b/views/partials/eventGroupForm.handlebars
@@ -31,6 +31,17 @@
</div>
<small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small>
</div>
+{{#if showPublicEventList}}
+ <div class="form-group">
+ <label>Options</label>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="publicGroupCheckbox" name="publicCheckbox" x-model="data.publicCheckbox">
+ <label class="form-check-label" for="publicGroupCheckbox">
+ Display this group on the public group list
+ </label>
+ </div>
+ </div>
+{{/if}}
<div class="form-group">
<div class="col-12">
<div
diff --git a/views/partials/sidebar.handlebars b/views/partials/sidebar.handlebars
index 5d8e847..1882aef 100755
--- a/views/partials/sidebar.handlebars
+++ b/views/partials/sidebar.handlebars
@@ -1,7 +1,11 @@
<div id="fixedContainer" class="pt-3">
- <h1><a href="/">gath<span class="text-muted">io</span></a></h1>
+ <h1 class="mb-4"><a href="/">gathio</a></h1>
- <p class="lead text-center mb-4">Nicer events</p>
-
- <a class="btn btn-success mb-2 btn-block" href="/new"><i class="far fa-calendar-plus"></i> New</a>
+ <ul id="sidebar__nav">
+ <li><a class="btn btn-success" href="/new"><i class="far fa-calendar-plus"></i> New</a></li>
+ {{#if showPublicEventList}}
+ <li><a href="/events">View events</a></li>
+ <li><a href="/about">About</a></li>
+ {{/if}}
+ </ul>
</div>
diff --git a/views/publicEventList.handlebars b/views/publicEventList.handlebars
new file mode 100644
index 0000000..6d85ca7
--- /dev/null
+++ b/views/publicEventList.handlebars
@@ -0,0 +1,72 @@
+<article x-data="{currentTab: 'events'}">
+<h2 class="mb-4">{{siteName}}</h2>
+<p><strong>{{siteName}}</strong> is an instance of <a href="/about">Gathio</a>, a simple, federated, privacy-first event hosting platform.</p>
+<ul class="nav nav-pills">
+ <li class="nav-item">
+ <a class="nav-link" x-bind:class="currentTab === 'events' && 'active'" aria-current="page" href="#" x-on:click.prevent="currentTab = 'events'">Events</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" x-bind:class="currentTab === 'groups' && 'active'" href="#" x-on:click.prevent="currentTab = 'groups'">Groups</a>
+ </li>
+</ul>
+
+<div x-show="currentTab === 'events'">
+ <div class="card mt-4 mb-4">
+ <h5 class="card-header">Upcoming events</h5>
+ <div class="list-group list-group-flush">
+ {{#if upcomingEvents}}
+ {{#each upcomingEvents}}
+ <a href="/{{this.id}}" class="list-group-item list-group-item-action">
+ <i class="fas fa-fw fa-calendar-day"></i>
+ <strong>{{this.name}}</strong>
+ {{#if this.location}}<span class="ml-2 text-muted"><i class="fas fa-map-marker-alt"></i> {{this.location}}</span>{{/if}}
+ <span class="ml-2 text-muted">{{this.displayDate}}</span>
+ {{#if this.eventGroup}}
+ <span class="badge badge-secondary ml-2">{{this.eventGroup.name}}</span>
+ {{/if}}
+ </a>
+ {{/each}}
+ {{else}}
+ <div class="list-group-item">No events!</div>
+ {{/if}}
+ </div>
+ </div>
+
+ <div class="card mt-4 mb-4">
+ <h5 class="card-header">Past events</h5>
+ <div class="list-group list-group-flush">
+ {{#if pastEvents}}
+ {{#each pastEvents}}
+ <a href="/{{this.id}}" class="list-group-item list-group-item-action">
+ <i class="fas fa-fw fa-calendar-day"></i>
+ <strong>{{this.name}}</strong>
+ <span class="ml-2 text-muted">{{this.displayDate}}</span>
+ {{#if this.eventGroup}}
+ <span class="badge badge-secondary ml-2">In group: {{this.eventGroup.name}}</span>
+ {{/if}}
+ </a>
+ {{/each}}
+ {{else}}
+ <div class="list-group-item">No events!</div>
+ {{/if}}
+ </div>
+ </div>
+</div>
+
+<div x-show="currentTab === 'groups'">
+ <div class="card mt-4 mb-4">
+ <h5 class="card-header">Event groups</h5>
+ <div class="list-group list-group-flush">
+ {{#if eventGroups}}
+ {{#each eventGroups}}
+ <a href="/group/{{this.id}}" class="list-group-item list-group-item-action">
+ <i class="fas fa-fw fa-calendar-alt"></i>
+ <strong>{{this.name}}</strong>
+ </a>
+ {{/each}}
+ {{else}}
+ <div class="list-group-item">No groups!</div>
+ {{/if}}
+</div>
+
+</article> \ No newline at end of file