diff --git a/src/EventGroupingHtmlRenderer/DayRenderer.ts b/src/EventGroupingHtmlRenderer/DayRenderer.ts
new file mode 100644
index 0000000..a5c152d
--- /dev/null
+++ b/src/EventGroupingHtmlRenderer/DayRenderer.ts
@@ -0,0 +1,27 @@
+import EventGrouping from "../EventGrouping";
+import Renderer from "./Renderer";
+
+export default class DayRenderer extends Renderer {
+ constructor(eventGrouping: EventGrouping) {
+ super(eventGrouping);
+ this.container.className += " scale-day";
+ }
+
+ protected renderDateAsIcon(eventDate, groupIndex): HTMLSpanElement {
+ const html = document.createElement("span");
+ html.className = "event";
+
+ const dayInMonth = eventDate.getDate();
+
+ if (dayInMonth === 1) {
+ html.textContent = eventDate
+ .toLocaleDateString(undefined, { month: "long" })
+ .slice(0, 3);
+ html.className += " primary";
+ } else {
+ html.textContent = dayInMonth.toString();
+ }
+
+ return html;
+ }
+}
diff --git a/src/EventGroupingHtmlRenderer/MonthRenderer.ts b/src/EventGroupingHtmlRenderer/MonthRenderer.ts
new file mode 100644
index 0000000..3e7e218
--- /dev/null
+++ b/src/EventGroupingHtmlRenderer/MonthRenderer.ts
@@ -0,0 +1,32 @@
+import EventGrouping from "../EventGrouping";
+import Renderer from "./Renderer";
+
+export default class MonthRenderer extends Renderer {
+ constructor(eventGrouping: EventGrouping) {
+ super(eventGrouping);
+ this.container.className += " scale-month";
+ }
+
+ protected renderDateAsIcon(eventDate, groupIndex): HTMLSpanElement {
+ const html = document.createElement("span");
+ html.className = "event";
+
+ // unlike getDate(), getMonth is 0 based
+ const month = eventDate.getMonth() + 1;
+
+ if (month === 1) {
+ html.textContent = eventDate.toLocaleDateString(undefined, {
+ year: "numeric",
+ });
+ html.className += " primary";
+ } else {
+ html.textContent = eventDate
+ .toLocaleDateString(undefined, {
+ month: "long",
+ })
+ .slice(0, 3);
+ }
+
+ return html;
+ }
+}
diff --git a/src/EventGroupingHtmlRenderer/Renderer.ts b/src/EventGroupingHtmlRenderer/Renderer.ts
new file mode 100644
index 0000000..bbe5863
--- /dev/null
+++ b/src/EventGroupingHtmlRenderer/Renderer.ts
@@ -0,0 +1,101 @@
+import { generateRandomColor } from "../utilities";
+import EventGrouping from "../EventGrouping";
+import { Event } from "../types";
+
+export default abstract class Renderer {
+ protected abstract renderDateAsIcon(
+ eventDate: Date,
+ groupIndex: number
+ ): HTMLSpanElement;
+
+ protected container: HTMLDivElement;
+ protected eventGrouping: EventGrouping;
+
+ protected constructor(eventGrouping: EventGrouping) {
+ this.container = document.createElement("div");
+ this.container.className = "event-calendar";
+ this.eventGrouping = eventGrouping;
+ }
+
+ protected renderGroups(): HTMLDivElement[] {
+ return this.eventGrouping.groups.map((group, index) => {
+ const html = document.createElement("div");
+ html.className = "group";
+
+ if (group.length) {
+ const htmlHoverCard = this.renderHoverCard(group);
+ html.appendChild(htmlHoverCard);
+ }
+
+ let htmlGroupIcon: HTMLSpanElement;
+ // only need to render icon from first event in group
+ const event = group[0];
+
+ if (event) {
+ htmlGroupIcon = this.renderIcon(event);
+ html.className += " icon";
+ if (event.bgColor) {
+ html.style.backgroundColor = event.bgColor;
+ } else {
+ const randomColor = generateRandomColor();
+ html.style.backgroundColor = `rgb(${randomColor.r}, ${randomColor.g}, ${randomColor.b})`;
+ }
+ } else {
+ const eventDate = this.eventGrouping.getDateFromGroupIndex(index);
+ htmlGroupIcon = this.renderDateAsIcon(eventDate, index);
+ }
+
+ html.appendChild(htmlGroupIcon);
+
+ return html;
+ });
+ }
+
+ protected renderHoverCard(events: Event[]): HTMLDivElement {
+ const htmlHoverCard = document.createElement("div");
+ htmlHoverCard.className = "hover-card";
+
+ const cardDetails = events.map((event) => {
+ if (!event) {
+ return;
+ }
+
+ const htmlEvent = document.createElement("div");
+ const htmlDateText = document.createElement("p");
+ htmlDateText.textContent = event.date.toLocaleDateString();
+ const htmlEventTitle = document.createElement("p");
+ htmlEventTitle.textContent = event.title;
+ const htmlEventText = document.createElement("p");
+ htmlEventText.textContent = event.text;
+
+ htmlEvent.appendChild(htmlDateText);
+ htmlEvent.appendChild(htmlEventTitle);
+ htmlEvent.appendChild(htmlEventText);
+
+ return htmlEvent;
+ });
+
+ cardDetails.forEach((cardDetail) => htmlHoverCard.appendChild(cardDetail));
+
+ return htmlHoverCard;
+ }
+
+ protected renderIcon(event: Event | undefined): HTMLSpanElement {
+ const html = document.createElement("span");
+ html.className = "event";
+
+ if (event.icon) {
+ html.textContent = event.icon;
+ }
+
+ return html;
+ }
+
+ public render() {
+ const groups = this.renderGroups();
+
+ groups.forEach((group) => this.container.appendChild(group));
+
+ return this.container;
+ }
+}
diff --git a/src/EventGroupingHtmlRenderer/WeekRenderer.ts b/src/EventGroupingHtmlRenderer/WeekRenderer.ts
new file mode 100644
index 0000000..4dce8a0
--- /dev/null
+++ b/src/EventGroupingHtmlRenderer/WeekRenderer.ts
@@ -0,0 +1,28 @@
+import { getWeek } from "date-fns";
+import EventGrouping from "../EventGrouping";
+import Renderer from "./Renderer";
+
+export default class WeekRenderer extends Renderer {
+ constructor(eventGrouping: EventGrouping) {
+ super(eventGrouping);
+ this.container.className += " scale-week";
+ }
+
+ protected renderDateAsIcon(eventDate, groupIndex): HTMLSpanElement {
+ const html = document.createElement("span");
+ html.className = "event";
+
+ const week = getWeek(eventDate);
+
+ if (week === 1) {
+ html.textContent = eventDate.toLocaleDateString(undefined, {
+ year: "numeric",
+ });
+ html.className += " primary";
+ } else {
+ html.textContent = week.toString();
+ }
+
+ return html;
+ }
+}