-
+
@@ -156,10 +166,12 @@
\ No newline at end of file
+}
+
diff --git a/src/components/trackCampaign/live/Initiative.vue b/src/components/trackCampaign/live/Initiative.vue
index aead6496..46d4b1cd 100644
--- a/src/components/trackCampaign/live/Initiative.vue
+++ b/src/components/trackCampaign/live/Initiative.vue
@@ -80,8 +80,8 @@
-
-
+
+
import("src/views/Admin/ExportCSV.vue"),
+ },
+ ],
+ },
{
path: "export",
component: {
diff --git a/src/utils/exports/BaseDataExport.js b/src/utils/exports/BaseDataExport.js
new file mode 100644
index 00000000..2536f91b
--- /dev/null
+++ b/src/utils/exports/BaseDataExport.js
@@ -0,0 +1,61 @@
+export default class BaseDataExport {
+ constructor() {
+ if (new.target == BaseDataExport) {
+ throw new Error("Cannot instantiate an abstract class.");
+ }
+ this.loading = false;
+ this.header = [];
+ this.rows = [];
+ }
+
+ startLoading() {
+ this.loading = false;
+ }
+
+ stopLoading() {
+ this.loading = false;
+ }
+
+ isLoading() {
+ return this.loading;
+ }
+
+ getFileName() {
+ return "dataExport.csv";
+ }
+
+ // Abstract method to retrieve CSV rows
+ async getCSVRows() {
+ throw new Error("getCSVRows() must be implemented by subclasses.");
+ }
+
+ // Method to export rows to a CSV file
+ exportToCSV() {
+ const filename = this.getFileName();
+ if (!this.rows || this.rows.length === 0) {
+ console.error("No data available for CSV export.");
+ return;
+ }
+
+ const csvRows = [this.header].concat(this.rows);
+ console.log(this.header, this.rows, csvRows);
+
+ // Convert rows array to CSV format
+ const csvContent = csvRows.map((row) => row.join(",")).join("\n");
+
+ // Create a Blob with CSV content and make it downloadable
+ const blob = new Blob([csvContent], { type: "text/csv" });
+ const url = URL.createObjectURL(blob);
+
+ // Create a link element to trigger the download
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", filename);
+ document.body.appendChild(link);
+ link.click();
+
+ // Clean up and revoke the object URL
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ }
+}
diff --git a/src/utils/exports/SignupsPerDay.js b/src/utils/exports/SignupsPerDay.js
new file mode 100644
index 00000000..d719ef81
--- /dev/null
+++ b/src/utils/exports/SignupsPerDay.js
@@ -0,0 +1,69 @@
+import { db } from "src/firebase";
+import { DefaultDict } from "../generalFunctions";
+
+import BaseDataExport from "./BaseDataExport";
+
+export default class UserDataExport extends BaseDataExport {
+ constructor() {
+ super();
+ this.config = {
+ label: "Signups Per Day",
+ value: "signups",
+ fields: [
+ { name: "Date Range", type: "daterange", value: { from: undefined, to: undefined } },
+ ],
+ };
+ }
+
+ date2timestamp(date_str) {
+ const date = new Date(date_str);
+ return date.getTime();
+ }
+
+ timestamp2date(ts) {
+ const date = new Date(ts);
+ return date.toLocaleDateString("en-GB");
+ }
+
+ getFileName() {
+ const range = this.config.fields[0].value;
+ return `signups_in_range_${range.from}-${range.to}.csv`;
+ }
+
+ // Implementing the abstract getCSVRows method
+ async getCSVRows() {
+ this.startLoading();
+
+ const from = this.date2timestamp(this.config.fields[0].value.from);
+ const to = this.date2timestamp(this.config.fields[0].value.to);
+
+ this.header = ["date", "count"];
+ try {
+ const users = await this.fetchUsersInRange(from, to);
+ this.rows = await this.aggregateSignups(users);
+ return this.rows;
+ } catch (error) {
+ console.error("Error fetching user data for CSV export:", error);
+ throw error;
+ } finally {
+ this.stopLoading();
+ }
+ }
+
+ async fetchUsersInRange(from, to) {
+ const user_ref = db.ref("users").orderByChild("created").startAt(from).endAt(to);
+ const payload = await user_ref.once("value");
+ if (!payload.exists()) {
+ throw new Error("No users found");
+ }
+ return Object.values(payload.val());
+ }
+
+ async aggregateSignups(users) {
+ const aggregate = await users.reduce((counts, user) => {
+ counts[this.timestamp2date(user.created)] += 1;
+ return counts;
+ }, new DefaultDict(0));
+ return Object.entries(aggregate);
+ }
+}
diff --git a/src/utils/exports/SubscriptionDataExport.js b/src/utils/exports/SubscriptionDataExport.js
new file mode 100644
index 00000000..9e6de996
--- /dev/null
+++ b/src/utils/exports/SubscriptionDataExport.js
@@ -0,0 +1,94 @@
+import { db } from "src/firebase";
+import { makeDate } from "../generalFunctions";
+import BaseDataExport from "./BaseDataExport";
+
+export default class UserDataExport extends BaseDataExport {
+ constructor() {
+ super();
+ this.config = {
+ label: "Subscription Data",
+ value: "subscriptions",
+ // fields: [
+ // { name: "Get All", type: "checkbox", value: false },
+ // { name: "Date Range", type: "daterange", value: { from: undefined, to: undefined } },
+ // ],
+ };
+ }
+
+ getFileName() {
+ return "subscriptionData.csv";
+ }
+
+ // Implementing the abstract getCSVRows method
+ async getCSVRows() {
+ this.startLoading();
+
+ this.header = [
+ "Created",
+ "Subscribed",
+ "Legacy",
+ "Tier",
+ "Status",
+ "Time till subscription in days",
+ "Time till subscription in hours",
+ "Time till subscription in ms",
+ ];
+
+ try {
+ // Simulate data fetching
+ this.rows = await this.getPatrons();
+ return this.rows;
+ } catch (error) {
+ console.error("Error fetching user data for CSV export:", error);
+ throw error;
+ } finally {
+ this.stopLoading();
+ }
+ }
+
+ async getPatrons() {
+ try {
+ const patreon_ref = db.ref("new_patrons");
+ const payload = await patreon_ref.once("value");
+ if (!payload.exists()) {
+ console.error("No patrons found");
+ }
+ const patrons = Object.values(payload.val());
+
+ return await Promise.all(
+ patrons.map(async (patron) => {
+ const user_payload = await db
+ .ref("users")
+ .orderByChild("patreon_email")
+ .equalTo(patron.email)
+ .once("value");
+ if (!user_payload.exists()) {
+ console.error("No corresponding user found for patron:", patron);
+ }
+ const user = user_payload.val();
+ console.log(user);
+
+ const created = patron.created ? new Date(patron.created) : new Date(2024, 4, 15);
+ const difference = new Date(patron.pledge_start) - created;
+
+ return Object.values({
+ created: makeDate(created, true, true).replace(" at", ""),
+ subscribed: makeDate(patron.pledge_start, true, true).replace(" at", ""),
+ legacy: !patron.created,
+ tier: patron.tiers ? Object.keys(patron.tiers)[0] : null,
+ status: patron.status,
+ time_till_subscription_days: difference.min(0)
+ ? difference.min(0) / 1000 / 60 / 60 / 24
+ : 0,
+ time_till_subscription_hours: difference.min(0)
+ ? difference.min(0) / 1000 / 60 / 60
+ : 0,
+ time_till_subscription_ms: difference.min(0) || 0,
+ });
+ })
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
diff --git a/src/utils/exports/UserDataExport.js b/src/utils/exports/UserDataExport.js
new file mode 100644
index 00000000..c8d00c06
--- /dev/null
+++ b/src/utils/exports/UserDataExport.js
@@ -0,0 +1,41 @@
+import { db, functions } from "src/firebase";
+
+import BaseDataExport from "./BaseDataExport";
+
+export default class UserDataExport extends BaseDataExport {
+ constructor() {
+ super();
+ this.config = {
+ label: "User Data",
+ value: "user_data",
+ };
+ }
+
+ // Implementing the abstract getCSVRows method
+ async getCSVRows() {
+ this.startLoading();
+
+ try {
+ // Simulate data fetching
+ const rows = await this.fetchUserData();
+ return rows;
+ } catch (error) {
+ console.error("Error fetching user data for CSV export:", error);
+ throw error;
+ } finally {
+ this.stopLoading();
+ }
+ }
+
+ async fetchUserData() {
+ try {
+ const getUserInfo = functions.httpsCallable("updateUsersEndpoint");
+ console.log("Functions", getUserInfo);
+ const user_info = await getUserInfo();
+ console.log("User info", user_info);
+ return [[JSON.stringify(user_info)]];
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
diff --git a/src/utils/generalFunctions.js b/src/utils/generalFunctions.js
index f3a2e4ab..40cf9061 100644
--- a/src/utils/generalFunctions.js
+++ b/src/utils/generalFunctions.js
@@ -89,6 +89,13 @@ export function uuid(mask = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx") {
});
}
+/**
+ * Get the value of a CSS variable
+ */
+export function getCssVariable(name) {
+ return getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
+}
+
/**
* Make readable Date
*/
@@ -281,3 +288,14 @@ export function generateYoutubeEmbedUrl(url) {
return `https://www.youtube-nocookie.com/embed/${id}`;
}
+
+export class DefaultDict {
+ constructor(defaultVal) {
+ return new Proxy(
+ {},
+ {
+ get: (target, name) => (name in target ? target[name] : defaultVal),
+ }
+ );
+ }
+}
diff --git a/src/views/Admin/ExportCSV.vue b/src/views/Admin/ExportCSV.vue
new file mode 100644
index 00000000..da32f9af
--- /dev/null
+++ b/src/views/Admin/ExportCSV.vue
@@ -0,0 +1,86 @@
+
+
+
+ Creates a CSV file with data from selected export.
+
+
+
+
+ {{ field.name }}
+
+
+
+
+
+
+
+
+
+ {{ data_export ? `Download ${data_export.label}` : "Select a data to export" }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/Admin/index.vue b/src/views/Admin/index.vue
index af111687..8d923a08 100644
--- a/src/views/Admin/index.vue
+++ b/src/views/Admin/index.vue
@@ -51,6 +51,11 @@ export default {
url: "subscriptions",
icon: "fab fa-patreon",
},
+ data_exports: {
+ name: "Data Exports",
+ url: "export_csv",
+ icon: "fas fa-file-spreadsheet",
+ },
// prerender: {
// name: "Generate prerender paths JSON",
// url: "prerender",
|