chore: format

This commit is contained in:
2025-10-10 16:43:21 +02:00
parent f0aabd63b6
commit 75c29e0ba4
551 changed files with 433948 additions and 94145 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
{ {
"mcpServers": {}, "mcpServers": {},
"isUsingBuiltInNodeForMcp": false "isUsingBuiltInNodeForMcp": false
} }

1
.gitignore vendored
View File

@@ -34,6 +34,7 @@
!.zshenv !.zshenv
!.hushlogin !.hushlogin
!.last_pwd !.last_pwd
!biome.json
!/.github/ !/.github/
!/.github/** !/.github/**

View File

@@ -1,31 +1,29 @@
{ {
"KPackageStructure": "Plasma/LookAndFeel", "KPackageStructure": "Plasma/LookAndFeel",
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Email": "eliverlara@gmail.com", "Email": "eliverlara@gmail.com",
"Name": "EliverLara" "Name": "EliverLara"
} }
], ],
"Category": "Plasma Look And Feel", "Category": "Plasma Look And Feel",
"EnabledByDefault": true, "EnabledByDefault": true,
"Id": "Ant-Dark", "Id": "Ant-Dark",
"License": "GPL 3+", "License": "GPL 3+",
"Name": "Ant-Dark", "Name": "Ant-Dark",
"ServiceTypes": [ "ServiceTypes": ["Plasma/LookAndFeel"],
"Plasma/LookAndFeel" "Version": "0.1",
], "Website": "https://github.com/EliverLara/Ant/tree/master/kde/Dark"
"Version": "0.1", },
"Website": "https://github.com/EliverLara/Ant/tree/master/kde/Dark" "X-KPackage-Dependencies": [
}, "kns://colorschemes.knsrc/api.kde-look.org/1464285",
"X-KPackage-Dependencies": [ "kns://plasma-themes.knsrc/api.kde-look.org/1464321",
"kns://colorschemes.knsrc/api.kde-look.org/1464285", "kns://aurorae.knsrc/api.kde-look.org/1464309",
"kns://plasma-themes.knsrc/api.kde-look.org/1464321", "kns://sddmtheme.knsrc/api.kde-look.org/2139782",
"kns://aurorae.knsrc/api.kde-look.org/1464309", "kns://wallpaper.knsrc/api.kde-look.org/1473492",
"kns://sddmtheme.knsrc/api.kde-look.org/2139782", "kns://icons.knsrc/api.kde-look.org/1640981",
"kns://wallpaper.knsrc/api.kde-look.org/1473492", "kns://plasmoids.knsrc/api.kde-look.org/2144212",
"kns://icons.knsrc/api.kde-look.org/1640981", "kns://plasmoids.knsrc/api.kde-look.org/2196105"
"kns://plasmoids.knsrc/api.kde-look.org/2144212", ]
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
]
} }

View File

@@ -1,34 +1,32 @@
{ {
"KPackageStructure": "Plasma/LookAndFeel", "KPackageStructure": "Plasma/LookAndFeel",
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Email": "eliverlara@gmail.com", "Email": "eliverlara@gmail.com",
"Name": "EliverLara" "Name": "EliverLara"
} }
], ],
"Category": "Plasma Look And Feel", "Category": "Plasma Look And Feel",
"EnabledByDefault": true, "EnabledByDefault": true,
"Id": "Nordic-bluish", "Id": "Nordic-bluish",
"License": "GPL 3+", "License": "GPL 3+",
"Name": "Nordic-bluish", "Name": "Nordic-bluish",
"ServiceTypes": [ "ServiceTypes": ["Plasma/LookAndFeel"],
"Plasma/LookAndFeel" "Version": "0.1",
], "Website": "https://github.com/EliverLara/Nordic"
"Version": "0.1", },
"Website": "https://github.com/EliverLara/Nordic" "X-KPackage-Dependencies": [
}, "kns://colorschemes.knsrc/api.kde-look.org/1801631",
"X-KPackage-Dependencies": [ "kns://plasma-themes.knsrc/api.kde-look.org/1801641",
"kns://colorschemes.knsrc/api.kde-look.org/1801631", "kns://aurorae.knsrc/api.kde-look.org/1326274",
"kns://plasma-themes.knsrc/api.kde-look.org/1801641", "kns://sddmtheme.knsrc/api.kde-look.org/2144235",
"kns://aurorae.knsrc/api.kde-look.org/1326274", "kns://xcursor.knsrc/api.kde-look.org/1662218",
"kns://sddmtheme.knsrc/api.kde-look.org/2144235", "kns://wallpaper.knsrc/api.kde-look.org/1683121",
"kns://xcursor.knsrc/api.kde-look.org/1662218", "kns://icons.knsrc/api.kde-look.org/1733012",
"kns://wallpaper.knsrc/api.kde-look.org/1683121", "kns://plasma-themes.knsrc/api.kde-look.org/1810707",
"kns://icons.knsrc/api.kde-look.org/1733012", "kns://plasmoids.knsrc/api.kde-look.org/2144212",
"kns://plasma-themes.knsrc/api.kde-look.org/1810707", "kns://wallpaper.knsrc/api.kde-look.org/2086140",
"kns://plasmoids.knsrc/api.kde-look.org/2144212", "kns://plasmoids.knsrc/api.kde-look.org/2196105"
"kns://wallpaper.knsrc/api.kde-look.org/2086140", ]
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
]
} }

View File

@@ -1,35 +1,33 @@
{ {
"KPackageStructure": "Plasma/LookAndFeel", "KPackageStructure": "Plasma/LookAndFeel",
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Email": "eliverlara@gmail.com", "Email": "eliverlara@gmail.com",
"Name": "EliverLara" "Name": "EliverLara"
} }
], ],
"Category": "Plasma Look And Feel", "Category": "Plasma Look And Feel",
"EnabledByDefault": true, "EnabledByDefault": true,
"Id": "Nordic-darker", "Id": "Nordic-darker",
"License": "GPL 3+", "License": "GPL 3+",
"Name": "Nordic-darker", "Name": "Nordic-darker",
"ServiceTypes": [ "ServiceTypes": ["Plasma/LookAndFeel"],
"Plasma/LookAndFeel" "Version": "0.1",
], "Website": "https://github.com/EliverLara/Nordic"
"Version": "0.1", },
"Website": "https://github.com/EliverLara/Nordic" "X-KPackage-Dependencies": [
}, "kns://colorschemes.knsrc/api.kde-look.org/1629062",
"X-KPackage-Dependencies": [ "kns://plasma-themes.knsrc/api.kde-look.org/1633673",
"kns://colorschemes.knsrc/api.kde-look.org/1629062", "kns://aurorae.knsrc/api.kde-look.org/1326274",
"kns://plasma-themes.knsrc/api.kde-look.org/1633673", "kns://sddmtheme.knsrc/api.kde-look.org/2146572",
"kns://aurorae.knsrc/api.kde-look.org/1326274", "kns://xcursor.knsrc/api.kde-look.org/1662218",
"kns://sddmtheme.knsrc/api.kde-look.org/2146572", "kns://wallpaper.knsrc/api.kde-look.org/1683121",
"kns://xcursor.knsrc/api.kde-look.org/1662218", "kns://plasma-themes.knsrc/api.kde-look.org/1704303",
"kns://wallpaper.knsrc/api.kde-look.org/1683121", "kns://icons.knsrc/api.kde-look.org/1733012",
"kns://plasma-themes.knsrc/api.kde-look.org/1704303", "kns://lookandfeel.knsrc/api.kde-look.org/2145004",
"kns://icons.knsrc/api.kde-look.org/1733012", "kns://plasmoids.knsrc/api.kde-look.org/2144212",
"kns://lookandfeel.knsrc/api.kde-look.org/2145004", "kns://wallpaper.knsrc/api.kde-look.org/2086140",
"kns://plasmoids.knsrc/api.kde-look.org/2144212", "kns://plasmoids.knsrc/api.kde-look.org/2196105"
"kns://wallpaper.knsrc/api.kde-look.org/2086140", ]
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
]
} }

View File

@@ -1,35 +1,33 @@
{ {
"KPackageStructure": "Plasma/LookAndFeel", "KPackageStructure": "Plasma/LookAndFeel",
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Email": "eliverlara@gmail.com", "Email": "eliverlara@gmail.com",
"Name": "EliverLara" "Name": "EliverLara"
} }
], ],
"Category": "Plasma Look And Feel", "Category": "Plasma Look And Feel",
"EnabledByDefault": true, "EnabledByDefault": true,
"Id": "Nordic", "Id": "Nordic",
"License": "GPL 3+", "License": "GPL 3+",
"Name": "Nordic", "Name": "Nordic",
"ServiceTypes": [ "ServiceTypes": ["Plasma/LookAndFeel"],
"Plasma/LookAndFeel" "Version": "0.1",
], "Website": "https://github.com/EliverLara/Nordic"
"Version": "0.1", },
"Website": "https://github.com/EliverLara/Nordic" "X-KPackage-Dependencies": [
}, "kns://colorschemes.knsrc/api.kde-look.org/1326271",
"X-KPackage-Dependencies": [ "kns://plasma-themes.knsrc/api.kde-look.org/1326896",
"kns://colorschemes.knsrc/api.kde-look.org/1326271", "kns://plasma-themes.knsrc/api.kde-look.org/1416702",
"kns://plasma-themes.knsrc/api.kde-look.org/1326896", "kns://aurorae.knsrc/api.kde-look.org/1326274",
"kns://plasma-themes.knsrc/api.kde-look.org/1416702", "kns://sddmtheme.knsrc/api.kde-look.org/2144235",
"kns://aurorae.knsrc/api.kde-look.org/1326274", "kns://xcursor.knsrc/api.kde-look.org/1662218",
"kns://sddmtheme.knsrc/api.kde-look.org/2144235", "kns://wallpaper.knsrc/api.kde-look.org/1683121",
"kns://xcursor.knsrc/api.kde-look.org/1662218", "kns://icons.knsrc/api.kde-look.org/1733012",
"kns://wallpaper.knsrc/api.kde-look.org/1683121", "kns://plasmoids.knsrc/api.kde-look.org/2144212",
"kns://icons.knsrc/api.kde-look.org/1733012", "kns://wallpaper.knsrc/api.kde-look.org/2086140",
"kns://plasmoids.knsrc/api.kde-look.org/2144212", "kns://plasmoids.knsrc/api.kde-look.org/2196105",
"kns://wallpaper.knsrc/api.kde-look.org/2086140", "kns://lookandfeel.knsrc/api.kde-look.org/2146574"
"kns://plasmoids.knsrc/api.kde-look.org/2196105", ]
"kns://lookandfeel.knsrc/api.kde-look.org/2146574"
]
} }

View File

@@ -1,19 +1,19 @@
function isDark(color) { function isDark(color) {
var r = color.r;
var g = color.g;
var b = color.b;
var r = color.r; // Using the HSP value, determine whether the color is light or dark
var g = color.g; var colorArray = [r, g, b].map((v) => {
var b = color.b; if (v <= 0.03928) {
return v / 12.92;
}
// Using the HSP value, determine whether the color is light or dark return Math.pow((v + 0.055) / 1.055, 2.4);
var colorArray = [r, g , b ].map(v => { });
if (v <= 0.03928) {
return v / 12.92
}
return Math.pow((v + 0.055) / 1.055, 2.4) var luminance =
}) 0.2126 * colorArray[0] + 0.7152 * colorArray[1] + 0.0722 * colorArray[2];
var luminance = 0.2126 * colorArray[0] + 0.7152 * colorArray[1] + 0.0722 * colorArray[2] return luminance <= 0.179;
}
return luminance <= 0.179
}

View File

@@ -1,22 +1,22 @@
{ {
"KPackageStructure": "Plasma/Applet", "KPackageStructure": "Plasma/Applet",
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Email": "eliverlara@gmail.com", "Email": "eliverlara@gmail.com",
"Name": "EliverLara" "Name": "EliverLara"
} }
], ],
"Category": "Application Launchers", "Category": "Application Launchers",
"Description": "A modern Launcher for plasma!", "Description": "A modern Launcher for plasma!",
"EnabledByDefault": true, "EnabledByDefault": true,
"Icon": "start-here-kde", "Icon": "start-here-kde",
"Id": "AndromedaLauncher", "Id": "AndromedaLauncher",
"Name": "Andromeda Launcher", "Name": "Andromeda Launcher",
"Version": "0.6", "Version": "0.6",
"Website": "https://github.com/EliverLara/AndromedaLauncher" "Website": "https://github.com/EliverLara/AndromedaLauncher"
}, },
"X-Plasma-Provides": [ "org.kde.plasma.launchermenu" ], "X-Plasma-Provides": ["org.kde.plasma.launchermenu"],
"X-Plasma-API-Minimum-Version": "6.0" "X-Plasma-API-Minimum-Version": "6.0"
} }

View File

@@ -1,16 +1,15 @@
function updateBrightness(rootItem, source) { function updateBrightness(rootItem, source) {
if (rootItem.updateScreenBrightnessJob) if (rootItem.updateScreenBrightnessJob) return;
return;
if (!source.data["PowerDevil"]) { if (!source.data["PowerDevil"]) {
return; return;
} }
// we don't want passive brightness change send setBrightness call // we don't want passive brightness change send setBrightness call
rootItem.disableBrightnessUpdate = true; rootItem.disableBrightnessUpdate = true;
if (typeof source.data["PowerDevil"]["Screen Brightness"] === 'number') { if (typeof source.data["PowerDevil"]["Screen Brightness"] === "number") {
rootItem.screenBrightness = source.data["PowerDevil"]["Screen Brightness"]; rootItem.screenBrightness = source.data["PowerDevil"]["Screen Brightness"];
} }
rootItem.disableBrightnessUpdate = false; rootItem.disableBrightnessUpdate = false;
} }

View File

@@ -1,21 +1,20 @@
function isDark(color) { function isDark(color) {
//color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
//color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); var r = color.r;
var g = color.g;
var b = color.b;
var r = color.r; var colorArray = [r, g, b].map((v) => {
var g = color.g; if (v <= 0.03928) {
var b = color.b; return v / 12.92;
}
return Math.pow((v + 0.055) / 1.055, 2.4);
});
var colorArray = [r, g , b ].map(v => { var luminance =
if (v <= 0.03928) { 0.2126 * colorArray[0] + 0.7152 * colorArray[1] + 0.0722 * colorArray[2];
return v / 12.92
} return luminance <= 0.179;
}
return Math.pow((v + 0.055) / 1.055, 2.4)
})
var luminance = 0.2126 * colorArray[0] + 0.7152 * colorArray[1] + 0.0722 * colorArray[2]
return luminance <= 0.179
}

View File

@@ -1,163 +1,168 @@
function getBtDevice() { function getBtDevice() {
var connectedDevices = []; var connectedDevices = [];
var status = { var status = {
active: false, active: false,
message: "" message: "",
} };
for (var i = 0; i < btManager.devices.length; ++i) { for (var i = 0; i < btManager.devices.length; ++i) {
var device = btManager.devices[i]; var device = btManager.devices[i];
if (device.connected) { if (device.connected) {
connectedDevices.push(device); connectedDevices.push(device);
} }
} }
if (btManager.bluetoothBlocked) { if (btManager.bluetoothBlocked) {
status.active = false; status.active = false;
status.message = "Disabled"; status.message = "Disabled";
} else if (!btManager.bluetoothOperational) { } else if (!btManager.bluetoothOperational) {
if (!btManager.adapters.length) { if (!btManager.adapters.length) {
status.active = false; status.active = false;
status.message = "Unavailable"; status.message = "Unavailable";
} else { } else {
status.active = false; status.active = false;
status.message = "Offline"; status.message = "Offline";
} }
} else if (connectedDevices.length >= 1) { } else if (connectedDevices.length >= 1) {
status.active = true; status.active = true;
status.message = connectedDevices[0].name; status.message = connectedDevices[0].name;
} else { } else {
status.active = true; status.active = true;
status.message = "Not Connected"; status.message = "Not Connected";
} }
return status; return status;
} }
function toggleBluetooth() function toggleBluetooth() {
{ var enable = !btManager.bluetoothOperational;
var enable = !btManager.bluetoothOperational; btManager.bluetoothBlocked = !enable;
btManager.bluetoothBlocked = !enable;
for (var i = 0; i < btManager.adapters.length; ++i) { for (var i = 0; i < btManager.adapters.length; ++i) {
var adapter = btManager.adapters[i]; var adapter = btManager.adapters[i];
adapter.powered = enable; adapter.powered = enable;
} }
} }
function checkInhibition() { function checkInhibition() {
var inhibited = false; var inhibited = false;
if (!NotificationManager.Server.valid) { if (!NotificationManager.Server.valid) {
return false; return false;
} }
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil; var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
if (!isNaN(inhibitedUntil.getTime())) { if (!isNaN(inhibitedUntil.getTime())) {
inhibited |= (Date.now() < inhibitedUntil.getTime()); inhibited |= Date.now() < inhibitedUntil.getTime();
} }
if (notificationSettings.notificationsInhibitedByApplication) { if (notificationSettings.notificationsInhibitedByApplication) {
inhibited |= true; inhibited |= true;
} }
if (notificationSettings.inhibitNotificationsWhenScreensMirrored) { if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
inhibited |= notificationSettings.screensMirrored; inhibited |= notificationSettings.screensMirrored;
} }
return inhibited; return inhibited;
} }
function toggleDnd() { function toggleDnd() {
if (Funcs.checkInhibition()) { if (Funcs.checkInhibition()) {
notificationSettings.notificationsInhibitedUntil = undefined; notificationSettings.notificationsInhibitedUntil = undefined;
notificationSettings.revokeApplicationInhibitions(); notificationSettings.revokeApplicationInhibitions();
// overrules current mirrored screen setup, updates again when screen configuration // overrules current mirrored screen setup, updates again when screen configuration
notificationSettings.screensMirrored = false; notificationSettings.screensMirrored = false;
notificationSettings.save(); notificationSettings.save();
return; return;
} }
var d = new Date(); var d = new Date();
d.setYear(d.getFullYear()+1) d.setYear(d.getFullYear() + 1);
notificationSettings.notificationsInhibitedUntil = d notificationSettings.notificationsInhibitedUntil = d;
notificationSettings.save() notificationSettings.save();
} }
function revokeInhibitions() { function revokeInhibitions() {
notificationSettings.notificationsInhibitedUntil = undefined; notificationSettings.notificationsInhibitedUntil = undefined;
notificationSettings.revokeApplicationInhibitions(); notificationSettings.revokeApplicationInhibitions();
// overrules current mirrored screen setup, updates again when screen configuration changes // overrules current mirrored screen setup, updates again when screen configuration changes
notificationSettings.screensMirrored = false; notificationSettings.screensMirrored = false;
notificationSettings.save(); notificationSettings.save();
} }
function toggleRedshiftInhibition() { function toggleRedshiftInhibition() {
if (!monitor.available) { if (!monitor.available) {
return; return;
} }
switch (inhibitor.state) { switch (inhibitor.state) {
case Redshift.Inhibitor.Inhibiting: case Redshift.Inhibitor.Inhibiting:
case Redshift.Inhibitor.Inhibited: case Redshift.Inhibitor.Inhibited:
inhibitor.uninhibit(); inhibitor.uninhibit();
break; break;
case Redshift.Inhibitor.Uninhibiting: case Redshift.Inhibitor.Uninhibiting:
case Redshift.Inhibitor.Uninhibited: case Redshift.Inhibitor.Uninhibited:
inhibitor.inhibit(); inhibitor.inhibit();
break; break;
} }
} }
function volumePercent(volume) { function volumePercent(volume) {
return volume / Vol.PulseAudio.NormalVolume * 100 return (volume / Vol.PulseAudio.NormalVolume) * 100;
} }
function boundVolume(volume) { function boundVolume(volume) {
return Math.max(Vol.PulseAudio.MinimalVolume, Math.min(volume, Vol.PulseAudio.NormalVolume)); return Math.max(
Vol.PulseAudio.MinimalVolume,
Math.min(volume, Vol.PulseAudio.NormalVolume),
);
} }
function changeVolumeByPercent(volumeObject, deltaPercent) { function changeVolumeByPercent(volumeObject, deltaPercent) {
const oldVolume = volumeObject.volume; const oldVolume = volumeObject.volume;
const oldPercent = volumePercent(oldVolume); const oldPercent = volumePercent(oldVolume);
const targetPercent = oldPercent + deltaPercent; const targetPercent = oldPercent + deltaPercent;
const newVolume = boundVolume(Math.round(Vol.PulseAudio.NormalVolume * (targetPercent/100))); const newVolume = boundVolume(
const newPercent = volumePercent(newVolume); Math.round(Vol.PulseAudio.NormalVolume * (targetPercent / 100)),
volumeObject.muted = newPercent == 0; );
volumeObject.volume = newVolume; const newPercent = volumePercent(newVolume);
return newPercent; volumeObject.muted = newPercent == 0;
volumeObject.volume = newVolume;
return newPercent;
} }
function volIconName(volume, muted, prefix) { function volIconName(volume, muted, prefix) {
if (!prefix) { if (!prefix) {
prefix = "audio-volume"; prefix = "audio-volume";
} }
var icon = null; var icon = null;
var percent = volume / Vol.PulseAudio.NormalVolume var percent = volume / Vol.PulseAudio.NormalVolume;
if (percent <= 0.0 || muted) { if (percent <= 0.0 || muted) {
icon = prefix + "-muted"; icon = prefix + "-muted";
} else if (percent <= 0.25) { } else if (percent <= 0.25) {
icon = prefix + "-low"; icon = prefix + "-low";
} else if (percent <= 0.75) { } else if (percent <= 0.75) {
icon = prefix + "-medium"; icon = prefix + "-medium";
} else { } else {
icon = prefix + "-high"; icon = prefix + "-high";
} }
return icon; return icon;
} }
function getNetworkConnectionName() { function getNetworkConnectionName() {
var status = network.networkStatus.activeConnections; var status = network.networkStatus.activeConnections;
var statusParts; var statusParts;
if(isAirplane){ return "On"; } if (isAirplane) {
return "On";
}
if(status && status !== "Disconnected") { if (status && status !== "Disconnected") {
statusParts = status.split(":"); statusParts = status.split(":");
var connectionName = statusParts[1]?.trim().split(" ").slice(2).join(" "); var connectionName = statusParts[1]?.trim().split(" ").slice(2).join(" ");
return connectionName || "Connected"; return connectionName || "Connected";
} }
return "Disconnected"; return "Disconnected";
} }

View File

@@ -1,9 +1,6 @@
function listProperty(item) { function listProperty(item) {
for (var p in item) for (var p in item) {
{ if (typeof item[p] != "function")
if( typeof item[p] != "function" ) if (p != "objectName") console.log(p + ":" + item[p]);
if(p != "objectName") }
console.log(p + ":" + item[p]); }
}
}

View File

@@ -18,9 +18,7 @@
"Name[de]": "KDE Kontrollzentrum", "Name[de]": "KDE Kontrollzentrum",
"Name[ko]": "KDE 제어 센터", "Name[ko]": "KDE 제어 센터",
"Name[pt_BR]": "Estação de controle KDE", "Name[pt_BR]": "Estação de controle KDE",
"ServiceTypes": [ "ServiceTypes": ["Plasma/Applet"],
"Plasma/Applet"
],
"Version": "0.1.0", "Version": "0.1.0",
"Website": "https://github.com/EliverLara/kde-control-station/tree/plasma6" "Website": "https://github.com/EliverLara/kde-control-station/tree/plasma6"
}, },

View File

@@ -1,53 +1,53 @@
{ {
"name": "nvm", "name": "nvm",
"version": "0.40.1", "version": "0.40.1",
"description": "Node Version Manager - Simple bash script to manage multiple active node.js versions", "description": "Node Version Manager - Simple bash script to manage multiple active node.js versions",
"directories": { "directories": {
"test": "test" "test": "test"
}, },
"scripts": { "scripts": {
"test": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make test-$shell", "test": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make test-$shell",
"test/fast": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); env -i TERM=\"$TERM\" bash -lc \"make TEST_SUITE=fast test-$shell\"", "test/fast": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); env -i TERM=\"$TERM\" bash -lc \"make TEST_SUITE=fast test-$shell\"",
"test/slow": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=slow test-$shell", "test/slow": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=slow test-$shell",
"test/install_script": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=install_script test-$shell", "test/install_script": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=install_script test-$shell",
"test/installation": "npm run --silent test/installation/node && npm run --silent test/installation/iojs", "test/installation": "npm run --silent test/installation/node && npm run --silent test/installation/iojs",
"test/installation/node": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=installation_node test-$shell", "test/installation/node": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=installation_node test-$shell",
"test/installation/iojs": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=installation_iojs test-$shell", "test/installation/iojs": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=installation_iojs test-$shell",
"test/sourcing": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=sourcing test-$shell", "test/sourcing": "shell=$(basename -- $(ps -o comm= $(ps -o ppid= -p $PPID)) | sed 's/^-//'); make TEST_SUITE=sourcing test-$shell",
"test:check-exec": "(IFS=$'\\n'; for file in $(git ls-files test); do if [ ! -x \"$file\" ] && [[ \"$file\" != *.* ]] && [[ \"$file\" != test/fixtures/* ]]; then echo \"$file\"; fi; done) | tee /dev/stderr | awk 'END {if (NR > 0) exit 1}'", "test:check-exec": "(IFS=$'\\n'; for file in $(git ls-files test); do if [ ! -x \"$file\" ] && [[ \"$file\" != *.* ]] && [[ \"$file\" != test/fixtures/* ]]; then echo \"$file\"; fi; done) | tee /dev/stderr | awk 'END {if (NR > 0) exit 1}'",
"test:check-nonexec": "(IFS=$'\\n'; for file in $(git ls-files test); do if [ -x \"$file\" ] && [ ! -d \"$file\" ] && { [[ \"$file\" =~ '\\.(json|txt|sh|js|log)$' ]] || [[ \"$file\" =~ '^test/(mocks|fixtures)/.*' ]]; }; then echo \"$file\"; fi; done) | tee /dev/stderr | awk 'END {if (NR > 0) exit 1}'", "test:check-nonexec": "(IFS=$'\\n'; for file in $(git ls-files test); do if [ -x \"$file\" ] && [ ! -d \"$file\" ] && { [[ \"$file\" =~ '\\.(json|txt|sh|js|log)$' ]] || [[ \"$file\" =~ '^test/(mocks|fixtures)/.*' ]]; }; then echo \"$file\"; fi; done) | tee /dev/stderr | awk 'END {if (NR > 0) exit 1}'",
"doctoc": "doctoc --title='## Table of Contents' --github README.md", "doctoc": "doctoc --title='## Table of Contents' --github README.md",
"predoctoc:check": "cp README.md v-README.md.orig && npm run doctoc", "predoctoc:check": "cp README.md v-README.md.orig && npm run doctoc",
"doctoc:check": "diff -q README.md v-README.md.orig", "doctoc:check": "diff -q README.md v-README.md.orig",
"postdoctoc:check": "mv v-README.md.orig README.md", "postdoctoc:check": "mv v-README.md.orig README.md",
"eclint": "eclint check $(git ls-tree --name-only HEAD | xargs)", "eclint": "eclint check $(git ls-tree --name-only HEAD | xargs)",
"dockerfile_lint": "dockerfile_lint", "dockerfile_lint": "dockerfile_lint",
"markdown-link-check": "git ls-files | command grep -E '\\.md$' | xargs -n 1 markdown-link-check -p" "markdown-link-check": "git ls-files | command grep -E '\\.md$' | xargs -n 1 markdown-link-check -p"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/nvm-sh/nvm.git" "url": "git://github.com/nvm-sh/nvm.git"
}, },
"keywords": [ "keywords": [
"nvm", "nvm",
"node", "node",
"iojs", "iojs",
"version", "version",
"manager" "manager"
], ],
"author": "Tim Caswell <tim@creationix.com>", "author": "Tim Caswell <tim@creationix.com>",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/nvm-sh/nvm/issues" "url": "https://github.com/nvm-sh/nvm/issues"
}, },
"homepage": "https://github.com/nvm-sh/nvm", "homepage": "https://github.com/nvm-sh/nvm",
"devDependencies": { "devDependencies": {
"dockerfile_lint": "^0.3.4", "dockerfile_lint": "^0.3.4",
"doctoc": "^2.2.1", "doctoc": "^2.2.1",
"eclint": "^2.8.1", "eclint": "^2.8.1",
"markdown-link-check": "^3.12.2", "markdown-link-check": "^3.12.2",
"replace": "^1.2.2", "replace": "^1.2.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"urchin": "^0.0.5" "urchin": "^0.0.5"
} }
} }

View File

@@ -12,9 +12,7 @@
"postCreateCommand": "dir=/workspaces/ohmyzsh; rm -rf $HOME/.oh-my-zsh && ln -s $dir $HOME/.oh-my-zsh && cp $dir/templates/minimal.zshrc $HOME/.zshrc && chgrp -R 1000 $dir && chmod g-w,o-w $dir", "postCreateCommand": "dir=/workspaces/ohmyzsh; rm -rf $HOME/.oh-my-zsh && ln -s $dir $HOME/.oh-my-zsh && cp $dir/templates/minimal.zshrc $HOME/.zshrc && chgrp -R 1000 $dir && chmod g-w,o-w $dir",
"customizations": { "customizations": {
"codespaces": { "codespaces": {
"openFiles": [ "openFiles": ["README.md"]
"README.md"
]
} }
} }
} }

View File

@@ -1,23 +1,23 @@
{ {
"headers": [ "headers": [
{ {
"source": "/(|install.sh)", "source": "/(|install.sh)",
"headers": [ "headers": [
{ {
"key": "Content-Type", "key": "Content-Type",
"value": "text/plain" "value": "text/plain"
}, },
{ {
"key": "Content-Disposition", "key": "Content-Disposition",
"value": "inline; filename=\"install.sh\"" "value": "inline; filename=\"install.sh\""
} }
] ]
} }
], ],
"rewrites": [ "rewrites": [
{ {
"source": "/", "source": "/",
"destination": "/install.sh" "destination": "/install.sh"
} }
] ]
} }

File diff suppressed because it is too large Load Diff

94
.vscode/settings.json vendored
View File

@@ -1,49 +1,49 @@
{ {
"explorer.excludeGitIgnore": true, "explorer.excludeGitIgnore": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[ignore]": { "[ignore]": {
"editor.defaultFormatter": "foxundermoon.shell-format" "editor.defaultFormatter": "foxundermoon.shell-format"
}, },
"[shellscript]": { "[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format" "editor.defaultFormatter": "foxundermoon.shell-format"
}, },
"[xml]": { "[xml]": {
"editor.defaultFormatter": "trunk.io" "editor.defaultFormatter": "trunk.io"
}, },
"[html]": { "[html]": {
"editor.defaultFormatter": "trunk.io" "editor.defaultFormatter": "trunk.io"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[markdown]": { "[markdown]": {
"editor.defaultFormatter": "trunk.io" "editor.defaultFormatter": "trunk.io"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[yaml]": { "[yaml]": {
"editor.defaultFormatter": "trunk.io" "editor.defaultFormatter": "trunk.io"
}, },
"[dockercompose]": { "[dockercompose]": {
"editor.defaultFormatter": "ms-azuretools.vscode-containers" "editor.defaultFormatter": "ms-azuretools.vscode-containers"
}, },
"[svelte]": { "[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode" "editor.defaultFormatter": "svelte.svelte-vscode"
}, },
"[css]": { "[css]": {
"editor.defaultFormatter": "vscode.css-language-features" "editor.defaultFormatter": "vscode.css-language-features"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"
}, },
"[rust]": { "[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer" "editor.defaultFormatter": "rust-lang.rust-analyzer"
}, },
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"
}, },
"[vue]": { "[vue]": {
"editor.defaultFormatter": "Vue.volar" "editor.defaultFormatter": "Vue.volar"
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"devDependencies": { "devDependencies": {
"@electron/asar": "^3.2.1" "@electron/asar": "^3.2.1"
} }
} }

View File

@@ -1,18 +1,19 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
/* Custom animations */ /* Custom animations */
--animate-pulse: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite; --animate-pulse: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
/* Custom keyframes for pulse */ /* Custom keyframes for pulse */
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
opacity: 1; 100% {
} opacity: 1;
50% { }
opacity: 0.5; 50% {
} opacity: 0.5;
} }
}
} }
@layer base { @layer base {
@@ -30,18 +31,18 @@
/* Custom scrollbar styling */ /* Custom scrollbar styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: rgb(17 24 39); background: rgb(17 24 39);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgb(139 92 246); background: rgb(139 92 246);
border-radius: 5px; border-radius: 5px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: rgb(167 139 250); background: rgb(167 139 250);
} }

View File

@@ -1,57 +1,65 @@
import type { Metadata } from 'next' import type { Metadata } from "next";
import { Inter } from 'next/font/google' import { Inter } from "next/font/google";
import './globals.css' import "./globals.css";
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Pivoine Docs - Documentation Hub', title: "Pivoine Docs - Documentation Hub",
description: 'Comprehensive documentation hub for all Pivoine projects by Valknar. Explore technical guides, API references, and tutorials.', description:
keywords: ['documentation', 'pivoine', 'valknar', 'developer', 'guides', 'api'], "Comprehensive documentation hub for all Pivoine projects by Valknar. Explore technical guides, API references, and tutorials.",
authors: [{ name: 'Valknar', url: 'https://pivoine.art' }], keywords: [
creator: 'Valknar', "documentation",
manifest: '/manifest.json', "pivoine",
icons: { "valknar",
icon: [ "developer",
{ url: '/favicon.svg', type: 'image/svg+xml' }, "guides",
{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }, "api",
], ],
apple: [ authors: [{ name: "Valknar", url: "https://pivoine.art" }],
{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' }, creator: "Valknar",
], manifest: "/manifest.json",
}, icons: {
appleWebApp: { icon: [
capable: true, { url: "/favicon.svg", type: "image/svg+xml" },
statusBarStyle: 'black-translucent', { url: "/icon.svg", type: "image/svg+xml", sizes: "any" },
title: 'Pivoine Docs', ],
}, apple: [
openGraph: { { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
type: 'website', ],
locale: 'en_US', },
url: 'https://docs.pivoine.art', appleWebApp: {
title: 'Pivoine Docs - Documentation Hub', capable: true,
description: 'Comprehensive documentation hub for all Pivoine projects', statusBarStyle: "black-translucent",
siteName: 'Pivoine Docs', title: "Pivoine Docs",
}, },
twitter: { openGraph: {
card: 'summary_large_image', type: "website",
title: 'Pivoine Docs - Documentation Hub', locale: "en_US",
description: 'Comprehensive documentation hub for all Pivoine projects', url: "https://docs.pivoine.art",
}, title: "Pivoine Docs - Documentation Hub",
robots: { description: "Comprehensive documentation hub for all Pivoine projects",
index: true, siteName: "Pivoine Docs",
follow: true, },
}, twitter: {
} card: "summary_large_image",
title: "Pivoine Docs - Documentation Hub",
description: "Comprehensive documentation hub for all Pivoine projects",
},
robots: {
index: true,
follow: true,
},
};
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode;
}) { }) {
return ( return (
<html lang="en" className="scroll-smooth"> <html lang="en" className="scroll-smooth">
<body className={inter.className}>{children}</body> <body className={inter.className}>{children}</body>
</html> </html>
) );
} }

View File

@@ -1,204 +1,241 @@
'use client' "use client";
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from "react";
import { BookOpen, Code2, Globe, ChevronRight, Sparkles, Terminal } from 'lucide-react' import {
import KomposeIcon from '@/components/icons/KomposeIcon' BookOpen,
import { PivoineDocsIcon } from '@/components/icons' Code2,
Globe,
ChevronRight,
Sparkles,
Terminal,
} from "lucide-react";
import KomposeIcon from "@/components/icons/KomposeIcon";
import { PivoineDocsIcon } from "@/components/icons";
export default function DocsHub() { export default function DocsHub() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState<string | null>(null) const [isHovering, setIsHovering] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ setMousePosition({
x: (e.clientX / window.innerWidth) * 20 - 10, x: (e.clientX / window.innerWidth) * 20 - 10,
y: (e.clientY / window.innerHeight) * 20 - 10, y: (e.clientY / window.innerHeight) * 20 - 10,
}) });
} };
window.addEventListener('mousemove', handleMouseMove) window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove) return () => window.removeEventListener("mousemove", handleMouseMove);
}, []) }, []);
const projects = [ const projects = [
{ {
name: 'Kompose', name: "Kompose",
status: 'Active', status: "Active",
description: 'Comprehensive documentation for Kompose project', description: "Comprehensive documentation for Kompose project",
url: '/kompose', url: "/kompose",
gradient: 'from-violet-500 to-purple-600' gradient: "from-violet-500 to-purple-600",
} },
] ];
const links = [ const links = [
{ {
title: "Valknar's Blog", title: "Valknar's Blog",
icon: Globe, icon: Globe,
url: 'http://pivoine.art', url: "http://pivoine.art",
gradient: 'from-pink-500 to-rose-600' gradient: "from-pink-500 to-rose-600",
}, },
{ {
title: 'Source Code', title: "Source Code",
icon: Code2, icon: Code2,
url: 'https://code.pivoine.art', url: "https://code.pivoine.art",
gradient: 'from-cyan-500 to-blue-600' gradient: "from-cyan-500 to-blue-600",
} },
] ];
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900 text-white overflow-hidden"> <div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900/20 to-gray-900 text-white overflow-hidden">
{/* Animated background orbs */} {/* Animated background orbs */}
<div className="fixed inset-0 overflow-hidden pointer-events-none"> <div className="fixed inset-0 overflow-hidden pointer-events-none">
<div <div
className="absolute w-96 h-96 bg-purple-500/20 rounded-full blur-3xl top-0 -left-48 animate-pulse" className="absolute w-96 h-96 bg-purple-500/20 rounded-full blur-3xl top-0 -left-48 animate-pulse"
style={{ style={{
transform: `translate(${mousePosition.x}px, ${mousePosition.y}px)`, transform: `translate(${mousePosition.x}px, ${mousePosition.y}px)`,
transition: 'transform 0.3s ease-out' transition: "transform 0.3s ease-out",
}} }}
/> />
<div <div
className="absolute w-96 h-96 bg-pink-500/20 rounded-full blur-3xl bottom-0 -right-48 animate-pulse" className="absolute w-96 h-96 bg-pink-500/20 rounded-full blur-3xl bottom-0 -right-48 animate-pulse"
style={{ style={{
transform: `translate(${-mousePosition.x}px, ${-mousePosition.y}px)`, transform: `translate(${-mousePosition.x}px, ${-mousePosition.y}px)`,
transition: 'transform 0.3s ease-out', transition: "transform 0.3s ease-out",
animationDelay: '1s' animationDelay: "1s",
}} }}
/> />
<div className="absolute w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-pulse" style={{ animationDelay: '0.5s' }} /> <div
</div> className="absolute w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 animate-pulse"
style={{ animationDelay: "0.5s" }}
/>
</div>
{/* Main content */} {/* Main content */}
<div className="relative z-10 container mx-auto px-6 py-12 max-w-6xl"> <div className="relative z-10 container mx-auto px-6 py-12 max-w-6xl">
{/* Header */} {/* Header */}
<header className="text-center mb-20 pt-12"> <header className="text-center mb-20 pt-12">
{/* Hero Icon */} {/* Hero Icon */}
<div className="flex justify-center mb-8"> <div className="flex justify-center mb-8">
<PivoineDocsIcon size="200px" showLabel={false} interactive={true} /> <PivoineDocsIcon
</div> size="200px"
showLabel={false}
<div className="inline-flex items-center gap-2 mb-6 px-4 py-2 bg-white/5 backdrop-blur-sm rounded-full border border-white/10"> interactive={true}
<Sparkles className="w-4 h-4 text-purple-400" /> />
<span className="text-sm text-purple-300">Documentation Hub</span> </div>
</div>
<h1 className="text-7xl font-bold mb-6 bg-gradient-to-r from-white via-purple-200 to-pink-200 bg-clip-text text-transparent animate-pulse">
Pivoine Docs
</h1>
<p className="text-xl text-gray-300 max-w-2xl mx-auto leading-relaxed">
Comprehensive documentation for all projects by <span className="text-purple-400 font-semibold">Valknar</span>.
Explore technical guides, API references, and tutorials.
</p>
</header>
{/* Projects Grid */} <div className="inline-flex items-center gap-2 mb-6 px-4 py-2 bg-white/5 backdrop-blur-sm rounded-full border border-white/10">
<section className="mb-20"> <Sparkles className="w-4 h-4 text-purple-400" />
<div className="flex items-center gap-3 mb-8"> <span className="text-sm text-purple-300">Documentation Hub</span>
<Terminal className="w-6 h-6 text-purple-400" /> </div>
<h2 className="text-3xl font-bold">Project Documentation</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
{projects.map((project, idx) => (
<a
key={idx}
href={project.url}
onMouseEnter={() => setIsHovering(project.name)}
onMouseLeave={() => setIsHovering(null)}
className="group relative bg-white/5 backdrop-blur-md rounded-2xl p-8 border border-white/10 hover:border-purple-500/50 transition-all duration-300 hover:scale-105 hover:shadow-2xl hover:shadow-purple-500/20"
>
<div className="absolute inset-0 bg-gradient-to-br opacity-0 group-hover:opacity-10 rounded-2xl transition-opacity duration-300"
style={{ background: `linear-gradient(135deg, rgb(168, 85, 247), rgb(147, 51, 234))` }} />
<div className="relative">
<div className="flex items-start justify-between mb-4">
{project.name === 'Kompose' ? (
<div className={`relative w-14 h-14 rounded-xl bg-gradient-to-br ${project.gradient} shadow-lg flex items-center justify-center`}>
<KomposeIcon size="36px" interactive={false} className='' />
</div>
) : (
<div className={`p-3 rounded-xl bg-gradient-to-br ${project.gradient} shadow-lg`}>
<BookOpen className="w-8 h-8 text-white" />
</div>
)}
<span className="px-3 py-1 bg-emerald-500/20 text-emerald-300 rounded-full text-sm border border-emerald-500/30">
{project.status}
</span>
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-purple-300 transition-colors">
{project.name}
</h3>
<p className="text-gray-400 mb-4 leading-relaxed">
{project.description}
</p>
<div className="flex items-center text-purple-400 font-semibold group-hover:gap-3 gap-2 transition-all">
Read docs
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</a>
))}
{/* Coming Soon Card */}
<div className="relative bg-white/5 backdrop-blur-md rounded-2xl p-8 border border-dashed border-white/20">
<div className="opacity-60">
<div className="p-3 rounded-xl bg-gradient-to-br from-gray-600 to-gray-700 w-fit mb-4">
<BookOpen className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold mb-3 text-gray-400">More Projects</h3>
<p className="text-gray-500 leading-relaxed">
Additional documentation sites coming soon...
</p>
</div>
</div>
</div>
</section>
{/* External Links */} <h1 className="text-7xl font-bold mb-6 bg-gradient-to-r from-white via-purple-200 to-pink-200 bg-clip-text text-transparent animate-pulse">
<section> Pivoine Docs
<div className="flex items-center gap-3 mb-8"> </h1>
<Sparkles className="w-6 h-6 text-pink-400" />
<h2 className="text-3xl font-bold">Explore More</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
{links.map((link, idx) => {
const Icon = link.icon
return (
<a
key={idx}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="group bg-white/5 backdrop-blur-md rounded-2xl p-6 border border-white/10 hover:border-pink-500/50 transition-all duration-300 hover:scale-105 hover:shadow-2xl hover:shadow-pink-500/20 flex items-center gap-4"
>
<div className={`p-4 rounded-xl bg-gradient-to-br ${link.gradient} shadow-lg group-hover:scale-110 transition-transform`}>
<Icon className="w-7 h-7 text-white" />
</div>
<div className="flex-1">
<h3 className="text-xl font-bold group-hover:text-pink-300 transition-colors">
{link.title}
</h3>
<p className="text-gray-400 text-sm">{link.url}</p>
</div>
<ChevronRight className="w-6 h-6 text-gray-400 group-hover:text-pink-400 group-hover:translate-x-1 transition-all" />
</a>
)
})}
</div>
</section>
{/* Footer */} <p className="text-xl text-gray-300 max-w-2xl mx-auto leading-relaxed">
<footer className="mt-20 pt-8 border-t border-white/10 text-center text-gray-400"> Comprehensive documentation for all projects by{" "}
<p className="text-sm"> <span className="text-purple-400 font-semibold">Valknar</span>.
Crafted with passion by <span className="text-purple-400 font-semibold">Valknar</span> · Explore technical guides, API references, and tutorials.
<a href="http://pivoine.art" className="hover:text-purple-300 transition-colors ml-1">pivoine.art</a> </p>
</p> </header>
</footer>
</div> {/* Projects Grid */}
</div> <section className="mb-20">
) <div className="flex items-center gap-3 mb-8">
<Terminal className="w-6 h-6 text-purple-400" />
<h2 className="text-3xl font-bold">Project Documentation</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
{projects.map((project, idx) => (
<a
key={idx}
href={project.url}
onMouseEnter={() => setIsHovering(project.name)}
onMouseLeave={() => setIsHovering(null)}
className="group relative bg-white/5 backdrop-blur-md rounded-2xl p-8 border border-white/10 hover:border-purple-500/50 transition-all duration-300 hover:scale-105 hover:shadow-2xl hover:shadow-purple-500/20"
>
<div
className="absolute inset-0 bg-gradient-to-br opacity-0 group-hover:opacity-10 rounded-2xl transition-opacity duration-300"
style={{
background: `linear-gradient(135deg, rgb(168, 85, 247), rgb(147, 51, 234))`,
}}
/>
<div className="relative">
<div className="flex items-start justify-between mb-4">
{project.name === "Kompose" ? (
<div
className={`relative w-14 h-14 rounded-xl bg-gradient-to-br ${project.gradient} shadow-lg flex items-center justify-center`}
>
<KomposeIcon
size="36px"
interactive={false}
className=""
/>
</div>
) : (
<div
className={`p-3 rounded-xl bg-gradient-to-br ${project.gradient} shadow-lg`}
>
<BookOpen className="w-8 h-8 text-white" />
</div>
)}
<span className="px-3 py-1 bg-emerald-500/20 text-emerald-300 rounded-full text-sm border border-emerald-500/30">
{project.status}
</span>
</div>
<h3 className="text-2xl font-bold mb-3 group-hover:text-purple-300 transition-colors">
{project.name}
</h3>
<p className="text-gray-400 mb-4 leading-relaxed">
{project.description}
</p>
<div className="flex items-center text-purple-400 font-semibold group-hover:gap-3 gap-2 transition-all">
Read docs
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" />
</div>
</div>
</a>
))}
{/* Coming Soon Card */}
<div className="relative bg-white/5 backdrop-blur-md rounded-2xl p-8 border border-dashed border-white/20">
<div className="opacity-60">
<div className="p-3 rounded-xl bg-gradient-to-br from-gray-600 to-gray-700 w-fit mb-4">
<BookOpen className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-bold mb-3 text-gray-400">
More Projects
</h3>
<p className="text-gray-500 leading-relaxed">
Additional documentation sites coming soon...
</p>
</div>
</div>
</div>
</section>
{/* External Links */}
<section>
<div className="flex items-center gap-3 mb-8">
<Sparkles className="w-6 h-6 text-pink-400" />
<h2 className="text-3xl font-bold">Explore More</h2>
</div>
<div className="grid md:grid-cols-2 gap-6">
{links.map((link, idx) => {
const Icon = link.icon;
return (
<a
key={idx}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="group bg-white/5 backdrop-blur-md rounded-2xl p-6 border border-white/10 hover:border-pink-500/50 transition-all duration-300 hover:scale-105 hover:shadow-2xl hover:shadow-pink-500/20 flex items-center gap-4"
>
<div
className={`p-4 rounded-xl bg-gradient-to-br ${link.gradient} shadow-lg group-hover:scale-110 transition-transform`}
>
<Icon className="w-7 h-7 text-white" />
</div>
<div className="flex-1">
<h3 className="text-xl font-bold group-hover:text-pink-300 transition-colors">
{link.title}
</h3>
<p className="text-gray-400 text-sm">{link.url}</p>
</div>
<ChevronRight className="w-6 h-6 text-gray-400 group-hover:text-pink-400 group-hover:translate-x-1 transition-all" />
</a>
);
})}
</div>
</section>
{/* Footer */}
<footer className="mt-20 pt-8 border-t border-white/10 text-center text-gray-400">
<p className="text-sm">
Crafted with passion by{" "}
<span className="text-purple-400 font-semibold">Valknar</span> ·
<a
href="http://pivoine.art"
className="hover:text-purple-300 transition-colors ml-1"
>
pivoine.art
</a>
</p>
</footer>
</div>
</div>
);
} }

View File

@@ -1,302 +1,407 @@
'use client' "use client";
import PivoineDocsIcon from './PivoineDocsIcon' import PivoineDocsIcon from "./PivoineDocsIcon";
export default function PivoineIconDemo() { export default function PivoineIconDemo() {
return ( return (
<div style={{ <div
minHeight: '100vh', style={{
background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)', minHeight: "100vh",
padding: '4rem 2rem', background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
color: '#fff' padding: "4rem 2rem",
}}> color: "#fff",
<div style={{ }}
maxWidth: '1400px', >
margin: '0 auto' <div
}}> style={{
{/* Header */} maxWidth: "1400px",
<div style={{ textAlign: 'center', marginBottom: '4rem' }}> margin: "0 auto",
<h1 style={{ }}
fontSize: '3rem', >
fontWeight: 'bold', {/* Header */}
background: 'linear-gradient(135deg, #ec4899, #a855f7, #c084fc)', <div style={{ textAlign: "center", marginBottom: "4rem" }}>
backgroundClip: 'text', <h1
WebkitBackgroundClip: 'text', style={{
WebkitTextFillColor: 'transparent', fontSize: "3rem",
marginBottom: '1rem' fontWeight: "bold",
}}> background: "linear-gradient(135deg, #ec4899, #a855f7, #c084fc)",
Pivoine Docs Icon backgroundClip: "text",
</h1> WebkitBackgroundClip: "text",
<p style={{ WebkitTextFillColor: "transparent",
fontSize: '1.25rem', marginBottom: "1rem",
color: '#94a3b8', }}
maxWidth: '600px', >
margin: '0 auto' Pivoine Docs Icon
}}> </h1>
A beautiful animated peony blossom icon with interactive states <p
</p> style={{
</div> fontSize: "1.25rem",
color: "#94a3b8",
maxWidth: "600px",
margin: "0 auto",
}}
>
A beautiful animated peony blossom icon with interactive states
</p>
</div>
{/* Main Showcase */} {/* Main Showcase */}
<div style={{ <div
display: 'grid', style={{
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', display: "grid",
gap: '3rem', gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
marginBottom: '4rem' gap: "3rem",
}}> marginBottom: "4rem",
{/* Large Interactive */} }}
<div style={{ >
background: 'rgba(255, 255, 255, 0.05)', {/* Large Interactive */}
borderRadius: '1rem', <div
padding: '2rem', style={{
textAlign: 'center', background: "rgba(255, 255, 255, 0.05)",
backdropFilter: 'blur(10px)', borderRadius: "1rem",
border: '1px solid rgba(255, 255, 255, 0.1)' padding: "2rem",
}}> textAlign: "center",
<h3 style={{ marginBottom: '1.5rem', color: '#f472b6' }}> backdropFilter: "blur(10px)",
Interactive (Hover & Click) border: "1px solid rgba(255, 255, 255, 0.1)",
</h3> }}
<div style={{ >
display: 'flex', <h3 style={{ marginBottom: "1.5rem", color: "#f472b6" }}>
justifyContent: 'center', Interactive (Hover & Click)
alignItems: 'center', </h3>
minHeight: '320px' <div
}}> style={{
<PivoineDocsIcon size="280px" /> display: "flex",
</div> justifyContent: "center",
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}> alignItems: "center",
Hover to bloom Click to close minHeight: "320px",
</p> }}
</div> >
<PivoineDocsIcon size="280px" />
</div>
<p
style={{
color: "#94a3b8",
fontSize: "0.875rem",
marginTop: "1rem",
}}
>
Hover to bloom Click to close
</p>
</div>
{/* With Label */} {/* With Label */}
<div style={{ <div
background: 'rgba(255, 255, 255, 0.05)', style={{
borderRadius: '1rem', background: "rgba(255, 255, 255, 0.05)",
padding: '2rem', borderRadius: "1rem",
textAlign: 'center', padding: "2rem",
backdropFilter: 'blur(10px)', textAlign: "center",
border: '1px solid rgba(255, 255, 255, 0.1)' backdropFilter: "blur(10px)",
}}> border: "1px solid rgba(255, 255, 255, 0.1)",
<h3 style={{ marginBottom: '1.5rem', color: '#c084fc' }}> }}
With Label >
</h3> <h3 style={{ marginBottom: "1.5rem", color: "#c084fc" }}>
<div style={{ With Label
display: 'flex', </h3>
justifyContent: 'center', <div
alignItems: 'center', style={{
minHeight: '320px' display: "flex",
}}> justifyContent: "center",
<PivoineDocsIcon size="240px" showLabel /> alignItems: "center",
</div> minHeight: "320px",
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}> }}
Perfect for hero sections >
</p> <PivoineDocsIcon size="240px" showLabel />
</div> </div>
<p
style={{
color: "#94a3b8",
fontSize: "0.875rem",
marginTop: "1rem",
}}
>
Perfect for hero sections
</p>
</div>
{/* Non-Interactive */} {/* Non-Interactive */}
<div style={{ <div
background: 'rgba(255, 255, 255, 0.05)', style={{
borderRadius: '1rem', background: "rgba(255, 255, 255, 0.05)",
padding: '2rem', borderRadius: "1rem",
textAlign: 'center', padding: "2rem",
backdropFilter: 'blur(10px)', textAlign: "center",
border: '1px solid rgba(255, 255, 255, 0.1)' backdropFilter: "blur(10px)",
}}> border: "1px solid rgba(255, 255, 255, 0.1)",
<h3 style={{ marginBottom: '1.5rem', color: '#fb7185' }}> }}
Static (Non-Interactive) >
</h3> <h3 style={{ marginBottom: "1.5rem", color: "#fb7185" }}>
<div style={{ Static (Non-Interactive)
display: 'flex', </h3>
justifyContent: 'center', <div
alignItems: 'center', style={{
minHeight: '320px' display: "flex",
}}> justifyContent: "center",
<PivoineDocsIcon size="240px" interactive={false} /> alignItems: "center",
</div> minHeight: "320px",
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}> }}
Ideal for favicons & PWA icons >
</p> <PivoineDocsIcon size="240px" interactive={false} />
</div> </div>
</div> <p
style={{
color: "#94a3b8",
fontSize: "0.875rem",
marginTop: "1rem",
}}
>
Ideal for favicons & PWA icons
</p>
</div>
</div>
{/* Size Variations */} {/* Size Variations */}
<div style={{ <div
background: 'rgba(255, 255, 255, 0.05)', style={{
borderRadius: '1rem', background: "rgba(255, 255, 255, 0.05)",
padding: '3rem', borderRadius: "1rem",
backdropFilter: 'blur(10px)', padding: "3rem",
border: '1px solid rgba(255, 255, 255, 0.1)', backdropFilter: "blur(10px)",
marginBottom: '4rem' border: "1px solid rgba(255, 255, 255, 0.1)",
}}> marginBottom: "4rem",
<h2 style={{ }}
fontSize: '2rem', >
fontWeight: 'bold', <h2
marginBottom: '2rem', style={{
textAlign: 'center', fontSize: "2rem",
color: '#f0abfc' fontWeight: "bold",
}}> marginBottom: "2rem",
Size Variations textAlign: "center",
</h2> color: "#f0abfc",
<div style={{ }}
display: 'flex', >
justifyContent: 'space-around', Size Variations
alignItems: 'flex-end', </h2>
flexWrap: 'wrap', <div
gap: '2rem', style={{
padding: '2rem' display: "flex",
}}> justifyContent: "space-around",
<div style={{ textAlign: 'center' }}> alignItems: "flex-end",
<PivoineDocsIcon size="64px" /> flexWrap: "wrap",
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}> gap: "2rem",
64px<br />Favicon padding: "2rem",
</p> }}
</div> >
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: "center" }}>
<PivoineDocsIcon size="96px" /> <PivoineDocsIcon size="64px" />
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}> <p
96px<br />Small style={{
</p> color: "#94a3b8",
</div> fontSize: "0.75rem",
<div style={{ textAlign: 'center' }}> marginTop: "0.5rem",
<PivoineDocsIcon size="128px" /> }}
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}> >
128px<br />Medium 64px
</p> <br />
</div> Favicon
<div style={{ textAlign: 'center' }}> </p>
<PivoineDocsIcon size="192px" /> </div>
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}> <div style={{ textAlign: "center" }}>
192px<br />Large <PivoineDocsIcon size="96px" />
</p> <p
</div> style={{
<div style={{ textAlign: 'center' }}> color: "#94a3b8",
<PivoineDocsIcon size="256px" /> fontSize: "0.75rem",
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}> marginTop: "0.5rem",
256px<br />X-Large }}
</p> >
</div> 96px
</div> <br />
</div> Small
</p>
</div>
<div style={{ textAlign: "center" }}>
<PivoineDocsIcon size="128px" />
<p
style={{
color: "#94a3b8",
fontSize: "0.75rem",
marginTop: "0.5rem",
}}
>
128px
<br />
Medium
</p>
</div>
<div style={{ textAlign: "center" }}>
<PivoineDocsIcon size="192px" />
<p
style={{
color: "#94a3b8",
fontSize: "0.75rem",
marginTop: "0.5rem",
}}
>
192px
<br />
Large
</p>
</div>
<div style={{ textAlign: "center" }}>
<PivoineDocsIcon size="256px" />
<p
style={{
color: "#94a3b8",
fontSize: "0.75rem",
marginTop: "0.5rem",
}}
>
256px
<br />
X-Large
</p>
</div>
</div>
</div>
{/* Feature List */} {/* Feature List */}
<div style={{ <div
background: 'rgba(255, 255, 255, 0.05)', style={{
borderRadius: '1rem', background: "rgba(255, 255, 255, 0.05)",
padding: '3rem', borderRadius: "1rem",
backdropFilter: 'blur(10px)', padding: "3rem",
border: '1px solid rgba(255, 255, 255, 0.1)' backdropFilter: "blur(10px)",
}}> border: "1px solid rgba(255, 255, 255, 0.1)",
<h2 style={{ }}
fontSize: '2rem', >
fontWeight: 'bold', <h2
marginBottom: '2rem', style={{
textAlign: 'center', fontSize: "2rem",
color: '#f0abfc' fontWeight: "bold",
}}> marginBottom: "2rem",
Features textAlign: "center",
</h2> color: "#f0abfc",
<div style={{ }}
display: 'grid', >
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', Features
gap: '2rem' </h2>
}}> <div
{[ style={{
{ display: "grid",
icon: '🌸', gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
title: 'Realistic Design', gap: "2rem",
description: 'Multi-layered peony with natural gradients' }}
}, >
{ {[
icon: '✨', {
title: 'Smooth Animations', icon: "🌸",
description: 'Gentle breathing in normal state' title: "Realistic Design",
}, description: "Multi-layered peony with natural gradients",
{ },
icon: '🎭', {
title: 'Interactive States', icon: "✨",
description: 'Bloom on hover, close on click' title: "Smooth Animations",
}, description: "Gentle breathing in normal state",
{ },
icon: '💫', {
title: 'Particle Effects', icon: "🎭",
description: '12 bloom particles flying around' title: "Interactive States",
}, description: "Bloom on hover, close on click",
{ },
icon: '🎨', {
title: 'Beautiful Colors', icon: "💫",
description: 'Pink to purple gradient palette' title: "Particle Effects",
}, description: "12 bloom particles flying around",
{ },
icon: '♿', {
title: 'Accessible', icon: "🎨",
description: 'Reduced motion & touch support' title: "Beautiful Colors",
}, description: "Pink to purple gradient palette",
{ },
icon: '📱', {
title: 'Responsive', icon: "♿",
description: 'Works perfectly on all devices' title: "Accessible",
}, description: "Reduced motion & touch support",
{ },
icon: '⚡', {
title: 'High Performance', icon: "📱",
description: 'GPU-accelerated CSS animations' title: "Responsive",
} description: "Works perfectly on all devices",
].map((feature, i) => ( },
<div key={i} style={{ {
padding: '1.5rem', icon: "⚡",
background: 'rgba(255, 255, 255, 0.03)', title: "High Performance",
borderRadius: '0.75rem', description: "GPU-accelerated CSS animations",
border: '1px solid rgba(255, 255, 255, 0.08)' },
}}> ].map((feature, i) => (
<div style={{ fontSize: '2rem', marginBottom: '0.75rem' }}> <div
{feature.icon} key={i}
</div> style={{
<h4 style={{ padding: "1.5rem",
fontSize: '1.125rem', background: "rgba(255, 255, 255, 0.03)",
fontWeight: '600', borderRadius: "0.75rem",
marginBottom: '0.5rem', border: "1px solid rgba(255, 255, 255, 0.08)",
color: '#fda4af' }}
}}> >
{feature.title} <div style={{ fontSize: "2rem", marginBottom: "0.75rem" }}>
</h4> {feature.icon}
<p style={{ </div>
fontSize: '0.875rem', <h4
color: '#94a3b8' style={{
}}> fontSize: "1.125rem",
{feature.description} fontWeight: "600",
</p> marginBottom: "0.5rem",
</div> color: "#fda4af",
))} }}
</div> >
</div> {feature.title}
</h4>
<p
style={{
fontSize: "0.875rem",
color: "#94a3b8",
}}
>
{feature.description}
</p>
</div>
))}
</div>
</div>
{/* Usage Example */} {/* Usage Example */}
<div style={{ <div
marginTop: '4rem', style={{
background: 'rgba(255, 255, 255, 0.05)', marginTop: "4rem",
borderRadius: '1rem', background: "rgba(255, 255, 255, 0.05)",
padding: '2rem', borderRadius: "1rem",
backdropFilter: 'blur(10px)', padding: "2rem",
border: '1px solid rgba(255, 255, 255, 0.1)' backdropFilter: "blur(10px)",
}}> border: "1px solid rgba(255, 255, 255, 0.1)",
<h2 style={{ }}
fontSize: '1.5rem', >
fontWeight: 'bold', <h2
marginBottom: '1rem', style={{
color: '#f0abfc' fontSize: "1.5rem",
}}> fontWeight: "bold",
Quick Start marginBottom: "1rem",
</h2> color: "#f0abfc",
<pre style={{ }}
background: 'rgba(0, 0, 0, 0.3)', >
padding: '1.5rem', Quick Start
borderRadius: '0.5rem', </h2>
overflow: 'auto', <pre
fontSize: '0.875rem', style={{
color: '#e2e8f0' background: "rgba(0, 0, 0, 0.3)",
}}> padding: "1.5rem",
{`import PivoineDocsIcon from '@/components/icons/PivoineDocsIcon' borderRadius: "0.5rem",
overflow: "auto",
fontSize: "0.875rem",
color: "#e2e8f0",
}}
>
{`import PivoineDocsIcon from '@/components/icons/PivoineDocsIcon'
// Basic usage // Basic usage
<PivoineDocsIcon size="256px" /> <PivoineDocsIcon size="256px" />
@@ -306,19 +411,21 @@ export default function PivoineIconDemo() {
// Static for favicon // Static for favicon
<PivoineDocsIcon size="128px" interactive={false} />`} <PivoineDocsIcon size="128px" interactive={false} />`}
</pre> </pre>
</div> </div>
{/* Footer */} {/* Footer */}
<div style={{ <div
marginTop: '4rem', style={{
textAlign: 'center', marginTop: "4rem",
color: '#64748b', textAlign: "center",
fontSize: '0.875rem' color: "#64748b",
}}> fontSize: "0.875rem",
<p>Made with 🌸 for beautiful documentation experiences</p> }}
</div> >
</div> <p>Made with 🌸 for beautiful documentation experiences</p>
</div> </div>
) </div>
</div>
);
} }

View File

@@ -1,333 +1,334 @@
/* Kompose Icon Styles */ /* Kompose Icon Styles */
.kompose-icon-wrapper { .kompose-icon-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
transform-style: preserve-3d; transform-style: preserve-3d;
} }
.kompose-icon-wrapper:not(.is-interactive) { .kompose-icon-wrapper:not(.is-interactive) {
cursor: default; cursor: default;
} }
.kompose-icon { .kompose-icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
filter: drop-shadow(0 4px 20px rgba(0, 220, 130, 0.2)); filter: drop-shadow(0 4px 20px rgba(0, 220, 130, 0.2));
transition: filter 0.4s ease; transition: filter 0.4s ease;
} }
/* Hover Effects */ /* Hover Effects */
.kompose-icon-wrapper.is-interactive:hover { .kompose-icon-wrapper.is-interactive:hover {
transform: scale(1.05) translateY(-2px); transform: scale(1.05) translateY(-2px);
} }
.kompose-icon-wrapper.is-interactive:hover .kompose-icon { .kompose-icon-wrapper.is-interactive:hover .kompose-icon {
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4)); filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
animation: subtle-pulse 2s ease-in-out infinite; animation: subtle-pulse 2s ease-in-out infinite;
} }
.kompose-icon-wrapper.is-interactive:hover .bg-rect { .kompose-icon-wrapper.is-interactive:hover .bg-rect {
animation: bg-glow 2s ease-in-out infinite; animation: bg-glow 2s ease-in-out infinite;
} }
.kompose-icon-wrapper.is-interactive:hover .k-letter { .kompose-icon-wrapper.is-interactive:hover .k-letter {
animation: letter-glow 1.5s ease-in-out infinite; animation: letter-glow 1.5s ease-in-out infinite;
} }
.kompose-icon-wrapper.is-interactive:hover .k-vertical { .kompose-icon-wrapper.is-interactive:hover .k-vertical {
animation: line-slide-vertical 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); animation: line-slide-vertical 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
.kompose-icon-wrapper.is-interactive:hover .k-diagonal-top { .kompose-icon-wrapper.is-interactive:hover .k-diagonal-top {
animation: line-slide-diagonal-top 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s; animation: line-slide-diagonal-top 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s;
} }
.kompose-icon-wrapper.is-interactive:hover .k-diagonal-bottom { .kompose-icon-wrapper.is-interactive:hover .k-diagonal-bottom {
animation: line-slide-diagonal-bottom 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s; animation: line-slide-diagonal-bottom 0.8s cubic-bezier(0.34, 1.56, 0.64, 1)
0.2s;
} }
.kompose-icon-wrapper.is-interactive:hover .status-dot { .kompose-icon-wrapper.is-interactive:hover .status-dot {
animation: pulse-expand 1s ease-in-out infinite; animation: pulse-expand 1s ease-in-out infinite;
} }
.kompose-icon-wrapper.is-interactive:hover .status-ring { .kompose-icon-wrapper.is-interactive:hover .status-ring {
animation: ring-pulse 1.5s ease-in-out infinite; animation: ring-pulse 1.5s ease-in-out infinite;
} }
.kompose-icon-wrapper.is-interactive:hover .corner { .kompose-icon-wrapper.is-interactive:hover .corner {
opacity: 1 !important; opacity: 1 !important;
animation: corner-extend 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); animation: corner-extend 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
/* Click/Active Effects */ /* Click/Active Effects */
.kompose-icon-wrapper.is-clicked { .kompose-icon-wrapper.is-clicked {
animation: click-bounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); animation: click-bounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
} }
.kompose-icon-wrapper.is-clicked .kompose-icon { .kompose-icon-wrapper.is-clicked .kompose-icon {
animation: rotate-3d 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); animation: rotate-3d 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
filter: drop-shadow(0 12px 40px rgba(0, 220, 130, 0.6)); filter: drop-shadow(0 12px 40px rgba(0, 220, 130, 0.6));
} }
.kompose-icon-wrapper.is-clicked .k-letter { .kompose-icon-wrapper.is-clicked .k-letter {
animation: letter-flash 0.6s ease-out; animation: letter-flash 0.6s ease-out;
filter: url(#intenseglow192); filter: url(#intenseglow192);
} }
.kompose-icon-wrapper.is-clicked .status-dot { .kompose-icon-wrapper.is-clicked .status-dot {
animation: dot-burst 0.6s ease-out; animation: dot-burst 0.6s ease-out;
} }
/* Ripple Effect */ /* Ripple Effect */
.ripple { .ripple {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
background: radial-gradient( background: radial-gradient(
circle, circle,
rgba(0, 220, 130, 0.6) 0%, rgba(0, 220, 130, 0.6) 0%,
rgba(0, 220, 130, 0) 70% rgba(0, 220, 130, 0) 70%
); );
transform: translate(-50%, -50%) scale(0); transform: translate(-50%, -50%) scale(0);
animation: ripple-expand 0.8s ease-out; animation: ripple-expand 0.8s ease-out;
pointer-events: none; pointer-events: none;
} }
/* Default animations for status dot */ /* Default animations for status dot */
.status-dot { .status-dot {
animation: default-pulse 2s ease-in-out infinite; animation: default-pulse 2s ease-in-out infinite;
} }
.status-ring { .status-ring {
animation: default-ring-pulse 2s ease-in-out infinite; animation: default-ring-pulse 2s ease-in-out infinite;
} }
/* Keyframe Animations */ /* Keyframe Animations */
@keyframes subtle-pulse { @keyframes subtle-pulse {
0%, 0%,
100% { 100% {
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4)); filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
} }
50% { 50% {
filter: drop-shadow(0 8px 35px rgba(0, 220, 130, 0.6)); filter: drop-shadow(0 8px 35px rgba(0, 220, 130, 0.6));
} }
} }
@keyframes bg-glow { @keyframes bg-glow {
0%, 0%,
100% { 100% {
filter: brightness(1); filter: brightness(1);
} }
50% { 50% {
filter: brightness(1.1); filter: brightness(1.1);
} }
} }
@keyframes letter-glow { @keyframes letter-glow {
0%, 0%,
100% { 100% {
filter: url(#glow192); filter: url(#glow192);
} }
50% { 50% {
filter: url(#intenseglow192); filter: url(#intenseglow192);
} }
} }
@keyframes line-slide-vertical { @keyframes line-slide-vertical {
0% { 0% {
stroke-dasharray: 96; stroke-dasharray: 96;
stroke-dashoffset: 96; stroke-dashoffset: 96;
} }
100% { 100% {
stroke-dasharray: 96; stroke-dasharray: 96;
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }
@keyframes line-slide-diagonal-top { @keyframes line-slide-diagonal-top {
0% { 0% {
stroke-dasharray: 68; stroke-dasharray: 68;
stroke-dashoffset: 68; stroke-dashoffset: 68;
} }
100% { 100% {
stroke-dasharray: 68; stroke-dasharray: 68;
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }
@keyframes line-slide-diagonal-bottom { @keyframes line-slide-diagonal-bottom {
0% { 0% {
stroke-dasharray: 68; stroke-dasharray: 68;
stroke-dashoffset: 68; stroke-dashoffset: 68;
} }
100% { 100% {
stroke-dasharray: 68; stroke-dasharray: 68;
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }
@keyframes pulse-expand { @keyframes pulse-expand {
0%, 0%,
100% { 100% {
r: 11.52; r: 11.52;
opacity: 0.9; opacity: 0.9;
} }
50% { 50% {
r: 14; r: 14;
opacity: 1; opacity: 1;
} }
} }
@keyframes ring-pulse { @keyframes ring-pulse {
0%, 0%,
100% { 100% {
r: 17.28; r: 17.28;
opacity: 0.3; opacity: 0.3;
stroke-width: 3; stroke-width: 3;
} }
50% { 50% {
r: 20; r: 20;
opacity: 0.6; opacity: 0.6;
stroke-width: 2; stroke-width: 2;
} }
} }
@keyframes corner-extend { @keyframes corner-extend {
0% { 0% {
stroke-dasharray: 13.44; stroke-dasharray: 13.44;
stroke-dashoffset: 13.44; stroke-dashoffset: 13.44;
} }
100% { 100% {
stroke-dasharray: 13.44; stroke-dasharray: 13.44;
stroke-dashoffset: 0; stroke-dashoffset: 0;
} }
} }
@keyframes click-bounce { @keyframes click-bounce {
0% { 0% {
transform: scale(1) translateY(0) rotateY(0deg); transform: scale(1) translateY(0) rotateY(0deg);
} }
30% { 30% {
transform: scale(0.92) translateY(0) rotateY(0deg); transform: scale(0.92) translateY(0) rotateY(0deg);
} }
50% { 50% {
transform: scale(1.08) translateY(-4px) rotateY(180deg); transform: scale(1.08) translateY(-4px) rotateY(180deg);
} }
70% { 70% {
transform: scale(0.98) translateY(0) rotateY(360deg); transform: scale(0.98) translateY(0) rotateY(360deg);
} }
100% { 100% {
transform: scale(1) translateY(0) rotateY(360deg); transform: scale(1) translateY(0) rotateY(360deg);
} }
} }
@keyframes rotate-3d { @keyframes rotate-3d {
0% { 0% {
transform: perspective(800px) rotateY(0deg); transform: perspective(800px) rotateY(0deg);
} }
50% { 50% {
transform: perspective(800px) rotateY(180deg); transform: perspective(800px) rotateY(180deg);
} }
100% { 100% {
transform: perspective(800px) rotateY(360deg); transform: perspective(800px) rotateY(360deg);
} }
} }
@keyframes letter-flash { @keyframes letter-flash {
0%, 0%,
100% { 100% {
opacity: 1; opacity: 1;
} }
20%, 20%,
60% { 60% {
opacity: 0.7; opacity: 0.7;
} }
40%, 40%,
80% { 80% {
opacity: 1; opacity: 1;
} }
} }
@keyframes dot-burst { @keyframes dot-burst {
0% { 0% {
r: 11.52; r: 11.52;
opacity: 0.9; opacity: 0.9;
} }
50% { 50% {
r: 20; r: 20;
opacity: 1; opacity: 1;
} }
100% { 100% {
r: 11.52; r: 11.52;
opacity: 0.9; opacity: 0.9;
} }
} }
@keyframes ripple-expand { @keyframes ripple-expand {
0% { 0% {
transform: translate(-50%, -50%) scale(0); transform: translate(-50%, -50%) scale(0);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translate(-50%, -50%) scale(2.5); transform: translate(-50%, -50%) scale(2.5);
opacity: 0; opacity: 0;
} }
} }
@keyframes default-pulse { @keyframes default-pulse {
0%, 0%,
100% { 100% {
opacity: 0.6; opacity: 0.6;
r: 11.52; r: 11.52;
} }
50% { 50% {
opacity: 1; opacity: 1;
r: 13.44; r: 13.44;
} }
} }
@keyframes default-ring-pulse { @keyframes default-ring-pulse {
0%, 0%,
100% { 100% {
opacity: 0.3; opacity: 0.3;
} }
50% { 50% {
opacity: 0.5; opacity: 0.5;
} }
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.kompose-icon-wrapper.is-interactive:hover { .kompose-icon-wrapper.is-interactive:hover {
transform: scale(1.03) translateY(-1px); transform: scale(1.03) translateY(-1px);
} }
} }
/* Reduced motion support */ /* Reduced motion support */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.kompose-icon-wrapper, .kompose-icon-wrapper,
.kompose-icon, .kompose-icon,
.kompose-icon *, .kompose-icon *,
.ripple { .ripple {
animation: none !important; animation: none !important;
transition: none !important; transition: none !important;
} }
.kompose-icon-wrapper.is-interactive:hover { .kompose-icon-wrapper.is-interactive:hover {
transform: scale(1.02); transform: scale(1.02);
} }
} }
/* Touch device optimizations */ /* Touch device optimizations */
@media (hover: none) and (pointer: coarse) { @media (hover: none) and (pointer: coarse) {
.kompose-icon-wrapper.is-interactive:active { .kompose-icon-wrapper.is-interactive:active {
transform: scale(0.95); transform: scale(0.95);
} }
} }

View File

@@ -1,119 +1,254 @@
'use client' "use client";
import React, { useState } from 'react' import React, { useState } from "react";
import './KomposeIcon.css' import "./KomposeIcon.css";
interface KomposeIconProps { interface KomposeIconProps {
size?: string size?: string;
interactive?: boolean interactive?: boolean;
className?: string className?: string;
} }
export default function KomposeIcon({ export default function KomposeIcon({
size = '192px', size = "192px",
interactive = true, interactive = true,
className = '' className = "",
}: KomposeIconProps) { }: KomposeIconProps) {
const [isClicked, setIsClicked] = useState(false) const [isClicked, setIsClicked] = useState(false);
const [showRipple, setShowRipple] = useState(false) const [showRipple, setShowRipple] = useState(false);
const handleClick = () => { const handleClick = () => {
if (!interactive) return if (!interactive) return;
setIsClicked(true) setIsClicked(true);
setShowRipple(true) setShowRipple(true);
setTimeout(() => { setTimeout(() => {
setIsClicked(false) setIsClicked(false);
}, 600) }, 600);
setTimeout(() => { setTimeout(() => {
setShowRipple(false) setShowRipple(false);
}, 800) }, 800);
} };
const handleTouch = (e: React.TouchEvent) => { const handleTouch = (e: React.TouchEvent) => {
if (!interactive) return if (!interactive) return;
handleClick() handleClick();
} };
const wrapperClasses = [ const wrapperClasses = [
'kompose-icon-wrapper', "kompose-icon-wrapper",
isClicked && 'is-clicked', isClicked && "is-clicked",
interactive && 'is-interactive', interactive && "is-interactive",
className className,
].filter(Boolean).join(' ') ]
.filter(Boolean)
.join(" ");
return ( return (
<div <div
className={wrapperClasses} className={wrapperClasses}
onClick={handleClick} onClick={handleClick}
onTouchStart={handleTouch} onTouchStart={handleTouch}
style={{ width: size, height: size }} style={{ width: size, height: size }}
> >
<svg <svg
className="kompose-icon" className="kompose-icon"
viewBox="0 0 192 192" viewBox="0 0 192 192"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<defs> <defs>
<pattern id="carbon192" x="0" y="0" width="7.68" height="7.68" patternUnits="userSpaceOnUse"> <pattern
<rect width="7.68" height="7.68" fill="#0a0e27"></rect> id="carbon192"
<path d="M0,0 L3.84,3.84 M3.84,0 L7.68,3.84 M0,3.84 L3.84,7.68" stroke="#060815" strokeWidth="1.5" opacity="0.5"></path> x="0"
</pattern> y="0"
width="7.68"
height="7.68"
patternUnits="userSpaceOnUse"
>
<rect width="7.68" height="7.68" fill="#0a0e27"></rect>
<path
d="M0,0 L3.84,3.84 M3.84,0 L7.68,3.84 M0,3.84 L3.84,7.68"
stroke="#060815"
strokeWidth="1.5"
opacity="0.5"
></path>
</pattern>
<linearGradient id="bgGrad192" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient id="bgGrad192" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style={{ stopColor: '#1a1d2e', stopOpacity: 1 }}></stop> <stop
<stop offset="100%" style={{ stopColor: '#0a0e27', stopOpacity: 1 }}></stop> offset="0%"
</linearGradient> style={{ stopColor: "#1a1d2e", stopOpacity: 1 }}
></stop>
<stop
offset="100%"
style={{ stopColor: "#0a0e27", stopOpacity: 1 }}
></stop>
</linearGradient>
<linearGradient id="primaryGrad192" x1="0%" y1="0%" x2="100%" y2="100%"> <linearGradient
<stop offset="0%" className="gradient-start" style={{ stopColor: '#00DC82', stopOpacity: 1 }}></stop> id="primaryGrad192"
<stop offset="100%" className="gradient-end" style={{ stopColor: '#00a86b', stopOpacity: 1 }}></stop> x1="0%"
</linearGradient> y1="0%"
x2="100%"
y2="100%"
>
<stop
offset="0%"
className="gradient-start"
style={{ stopColor: "#00DC82", stopOpacity: 1 }}
></stop>
<stop
offset="100%"
className="gradient-end"
style={{ stopColor: "#00a86b", stopOpacity: 1 }}
></stop>
</linearGradient>
<filter id="glow192"> <filter id="glow192">
<feGaussianBlur stdDeviation="6" result="coloredBlur"></feGaussianBlur> <feGaussianBlur
<feMerge> stdDeviation="6"
<feMergeNode in="coloredBlur"></feMergeNode> result="coloredBlur"
<feMergeNode in="SourceGraphic"></feMergeNode> ></feGaussianBlur>
</feMerge> <feMerge>
</filter> <feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<filter id="intenseglow192"> <filter id="intenseglow192">
<feGaussianBlur stdDeviation="12" result="coloredBlur"></feGaussianBlur> <feGaussianBlur
<feMerge> stdDeviation="12"
<feMergeNode in="coloredBlur"></feMergeNode> result="coloredBlur"
<feMergeNode in="SourceGraphic"></feMergeNode> ></feGaussianBlur>
</feMerge> <feMerge>
</filter> <feMergeNode in="coloredBlur"></feMergeNode>
</defs> <feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
{/* Background */} {/* Background */}
<rect className="bg-rect" width="192" height="192" rx="24" fill="url(#bgGrad192)"></rect> <rect
<rect className="carbon-pattern" width="192" height="192" rx="24" fill="url(#carbon192)" opacity="0.4"></rect> className="bg-rect"
width="192"
height="192"
rx="24"
fill="url(#bgGrad192)"
></rect>
<rect
className="carbon-pattern"
width="192"
height="192"
rx="24"
fill="url(#carbon192)"
opacity="0.4"
></rect>
{/* Stylized K */} {/* Stylized K */}
<g className="k-letter" transform="translate(48, 48)"> <g className="k-letter" transform="translate(48, 48)">
<line className="k-line k-vertical" x1="0" y1="0" x2="0" y2="96" stroke="url(#primaryGrad192)" strokeWidth="15" strokeLinecap="round" filter="url(#glow192)"></line> <line
<line className="k-line k-diagonal-top" x1="0" y1="48" x2="57.6" y2="0" stroke="url(#primaryGrad192)" strokeWidth="15" strokeLinecap="round" filter="url(#glow192)"></line> className="k-line k-vertical"
<line className="k-line k-diagonal-bottom" x1="0" y1="48" x2="57.6" y2="96" stroke="url(#primaryGrad192)" strokeWidth="15" strokeLinecap="round" filter="url(#glow192)"></line> x1="0"
</g> y1="0"
x2="0"
y2="96"
stroke="url(#primaryGrad192)"
strokeWidth="15"
strokeLinecap="round"
filter="url(#glow192)"
></line>
<line
className="k-line k-diagonal-top"
x1="0"
y1="48"
x2="57.6"
y2="0"
stroke="url(#primaryGrad192)"
strokeWidth="15"
strokeLinecap="round"
filter="url(#glow192)"
></line>
<line
className="k-line k-diagonal-bottom"
x1="0"
y1="48"
x2="57.6"
y2="96"
stroke="url(#primaryGrad192)"
strokeWidth="15"
strokeLinecap="round"
filter="url(#glow192)"
></line>
</g>
{/* Animated status dot */} {/* Animated status dot */}
<circle className="status-dot" cx="163.2" cy="163.2" r="11.52" fill="#00DC82" opacity="0.9"></circle> <circle
<circle className="status-ring" cx="163.2" cy="163.2" r="17.28" fill="none" stroke="#00DC82" strokeWidth="3" opacity="0.3"></circle> className="status-dot"
cx="163.2"
cy="163.2"
r="11.52"
fill="#00DC82"
opacity="0.9"
></circle>
<circle
className="status-ring"
cx="163.2"
cy="163.2"
r="17.28"
fill="none"
stroke="#00DC82"
strokeWidth="3"
opacity="0.3"
></circle>
{/* Tech corners */} {/* Tech corners */}
<line className="corner corner-tl-h" x1="15.36" y1="15.36" x2="28.8" y2="15.36" stroke="#00DC82" strokeWidth="3" opacity="0.4"></line> <line
<line className="corner corner-tl-v" x1="15.36" y1="15.36" x2="15.36" y2="28.8" stroke="#00DC82" strokeWidth="3" opacity="0.4"></line> className="corner corner-tl-h"
<line className="corner corner-tr-h" x1="176.64" y1="15.36" x2="163.2" y2="15.36" stroke="#00DC82" strokeWidth="3" opacity="0.4"></line> x1="15.36"
<line className="corner corner-tr-v" x1="176.64" y1="15.36" x2="176.64" y2="28.8" stroke="#00DC82" strokeWidth="3" opacity="0.4"></line> y1="15.36"
</svg> x2="28.8"
y2="15.36"
stroke="#00DC82"
strokeWidth="3"
opacity="0.4"
></line>
<line
className="corner corner-tl-v"
x1="15.36"
y1="15.36"
x2="15.36"
y2="28.8"
stroke="#00DC82"
strokeWidth="3"
opacity="0.4"
></line>
<line
className="corner corner-tr-h"
x1="176.64"
y1="15.36"
x2="163.2"
y2="15.36"
stroke="#00DC82"
strokeWidth="3"
opacity="0.4"
></line>
<line
className="corner corner-tr-v"
x1="176.64"
y1="15.36"
x2="176.64"
y2="28.8"
stroke="#00DC82"
strokeWidth="3"
opacity="0.4"
></line>
</svg>
{/* Ripple effect container */} {/* Ripple effect container */}
{showRipple && <div className="ripple"></div>} {showRipple && <div className="ripple"></div>}
</div> </div>
) );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,338 +1,494 @@
'use client' "use client";
import React, { useState } from 'react' import React, { useState } from "react";
import './PivoineDocsIcon.css' import "./PivoineDocsIcon.css";
interface PivoineDocsIconProps { interface PivoineDocsIconProps {
size?: string size?: string;
interactive?: boolean interactive?: boolean;
className?: string className?: string;
showLabel?: boolean showLabel?: boolean;
} }
export default function PivoineDocsIcon({ export default function PivoineDocsIcon({
size = '256px', size = "256px",
interactive = true, interactive = true,
className = '', className = "",
showLabel = false showLabel = false,
}: PivoineDocsIconProps) { }: PivoineDocsIconProps) {
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false);
const [isClicked, setIsClicked] = useState(false) const [isClicked, setIsClicked] = useState(false);
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (!interactive) return if (!interactive) return;
setIsHovered(true) setIsHovered(true);
} };
const handleMouseLeave = () => { const handleMouseLeave = () => {
if (!interactive) return if (!interactive) return;
setIsHovered(false) setIsHovered(false);
} };
const handleClick = () => { const handleClick = () => {
if (!interactive) return if (!interactive) return;
setIsClicked(true) setIsClicked(true);
setTimeout(() => { setTimeout(() => {
setIsClicked(false) setIsClicked(false);
}, 1200) }, 1200);
} };
const handleTouch = (e: React.TouchEvent) => { const handleTouch = (e: React.TouchEvent) => {
if (!interactive) return if (!interactive) return;
e.preventDefault() e.preventDefault();
setIsHovered(true) setIsHovered(true);
setTimeout(() => {
handleClick()
}, 50)
setTimeout(() => { setTimeout(() => {
setIsHovered(false) handleClick();
}, 1500) }, 50);
}
const wrapperClasses = [ setTimeout(() => {
'pivoine-docs-icon-wrapper', setIsHovered(false);
isHovered && 'is-hovered', }, 1500);
isClicked && 'is-clicked', };
interactive && 'is-interactive',
className
].filter(Boolean).join(' ')
// Generate bloom particles with varied properties const wrapperClasses = [
const bloomParticles = Array.from({ length: 12 }, (_, i) => ({ "pivoine-docs-icon-wrapper",
id: i, isHovered && "is-hovered",
angle: (360 / 12) * i, isClicked && "is-clicked",
distance: 80 + Math.random() * 20, interactive && "is-interactive",
size: 2 + Math.random() * 2, className,
delay: i * 0.08, ]
})) .filter(Boolean)
.join(" ");
return ( // Generate bloom particles with varied properties
<div const bloomParticles = Array.from({ length: 12 }, (_, i) => ({
className={wrapperClasses} id: i,
onMouseEnter={handleMouseEnter} angle: (360 / 12) * i,
onMouseLeave={handleMouseLeave} distance: 80 + Math.random() * 20,
onClick={handleClick} size: 2 + Math.random() * 2,
onTouchStart={handleTouch} delay: i * 0.08,
style={{ width: size, height: size, rotate: '5deg' }} }));
>
<svg
className="pivoine-docs-icon"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Enhanced Gradients for natural peony colors */}
<radialGradient id="petal-gradient-1" cx="30%" cy="30%">
<stop offset="0%" style={{ stopColor: '#fdf4ff', stopOpacity: 1 }} />
<stop offset="40%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} />
<stop offset="70%" style={{ stopColor: '#f0abfc', stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: '#d946ef', stopOpacity: 0.95 }} />
</radialGradient>
<radialGradient id="petal-gradient-2" cx="30%" cy="30%"> return (
<stop offset="0%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} /> <div
<stop offset="40%" style={{ stopColor: '#f3e8ff', stopOpacity: 1 }} /> className={wrapperClasses}
<stop offset="70%" style={{ stopColor: '#e9d5ff', stopOpacity: 1 }} /> onMouseEnter={handleMouseEnter}
<stop offset="100%" style={{ stopColor: '#c084fc', stopOpacity: 0.95 }} /> onMouseLeave={handleMouseLeave}
</radialGradient> onClick={handleClick}
onTouchStart={handleTouch}
style={{ width: size, height: size, rotate: "5deg" }}
>
<svg
className="pivoine-docs-icon"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
{/* Enhanced Gradients for natural peony colors */}
<radialGradient id="petal-gradient-1" cx="30%" cy="30%">
<stop
offset="0%"
style={{ stopColor: "#fdf4ff", stopOpacity: 1 }}
/>
<stop
offset="40%"
style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
/>
<stop
offset="70%"
style={{ stopColor: "#f0abfc", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#d946ef", stopOpacity: 0.95 }}
/>
</radialGradient>
<radialGradient id="petal-gradient-3" cx="30%" cy="30%"> <radialGradient id="petal-gradient-2" cx="30%" cy="30%">
<stop offset="0%" style={{ stopColor: '#fdf4ff', stopOpacity: 1 }} /> <stop
<stop offset="40%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} /> offset="0%"
<stop offset="70%" style={{ stopColor: '#f0abfc', stopOpacity: 1 }} /> style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
<stop offset="100%" style={{ stopColor: '#d946ef', stopOpacity: 0.95 }} /> />
</radialGradient> <stop
offset="40%"
style={{ stopColor: "#f3e8ff", stopOpacity: 1 }}
/>
<stop
offset="70%"
style={{ stopColor: "#e9d5ff", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#c084fc", stopOpacity: 0.95 }}
/>
</radialGradient>
<radialGradient id="petal-gradient-4" cx="30%" cy="30%"> <radialGradient id="petal-gradient-3" cx="30%" cy="30%">
<stop offset="0%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} /> <stop
<stop offset="40%" style={{ stopColor: '#f3e8ff', stopOpacity: 1 }} /> offset="0%"
<stop offset="70%" style={{ stopColor: '#e9d5ff', stopOpacity: 1 }} /> style={{ stopColor: "#fdf4ff", stopOpacity: 1 }}
<stop offset="100%" style={{ stopColor: '#c084fc', stopOpacity: 0.95 }} /> />
</radialGradient> <stop
offset="40%"
style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
/>
<stop
offset="70%"
style={{ stopColor: "#f0abfc", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#d946ef", stopOpacity: 0.95 }}
/>
</radialGradient>
<radialGradient id="center-gradient" cx="50%" cy="50%"> <radialGradient id="petal-gradient-4" cx="30%" cy="30%">
<stop offset="0%" style={{ stopColor: '#fef3c7', stopOpacity: 1 }} /> <stop
<stop offset="30%" style={{ stopColor: '#fde68a', stopOpacity: 1 }} /> offset="0%"
<stop offset="60%" style={{ stopColor: '#fbbf24', stopOpacity: 1 }} /> style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
<stop offset="100%" style={{ stopColor: '#f59e0b', stopOpacity: 1 }} /> />
</radialGradient> <stop
offset="40%"
style={{ stopColor: "#f3e8ff", stopOpacity: 1 }}
/>
<stop
offset="70%"
style={{ stopColor: "#e9d5ff", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#c084fc", stopOpacity: 0.95 }}
/>
</radialGradient>
<radialGradient id="center-inner-gradient" cx="50%" cy="50%"> <radialGradient id="center-gradient" cx="50%" cy="50%">
<stop offset="0%" style={{ stopColor: '#fffbeb', stopOpacity: 1 }} /> <stop
<stop offset="50%" style={{ stopColor: '#fef3c7', stopOpacity: 1 }} /> offset="0%"
<stop offset="100%" style={{ stopColor: '#fde68a', stopOpacity: 1 }} /> style={{ stopColor: "#fef3c7", stopOpacity: 1 }}
</radialGradient> />
<stop
offset="30%"
style={{ stopColor: "#fde68a", stopOpacity: 1 }}
/>
<stop
offset="60%"
style={{ stopColor: "#fbbf24", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#f59e0b", stopOpacity: 1 }}
/>
</radialGradient>
<linearGradient id="page-gradient" x1="0%" y1="0%" x2="100%" y2="100%"> <radialGradient id="center-inner-gradient" cx="50%" cy="50%">
<stop offset="0%" style={{ stopColor: '#ffffff', stopOpacity: 0.98 }} /> <stop
<stop offset="100%" style={{ stopColor: '#f3f4f6', stopOpacity: 0.98 }} /> offset="0%"
</linearGradient> style={{ stopColor: "#fffbeb", stopOpacity: 1 }}
/>
<stop
offset="50%"
style={{ stopColor: "#fef3c7", stopOpacity: 1 }}
/>
<stop
offset="100%"
style={{ stopColor: "#fde68a", stopOpacity: 1 }}
/>
</radialGradient>
{/* Enhanced Filters */} <linearGradient
<filter id="petal-glow"> id="page-gradient"
<feGaussianBlur stdDeviation="2.5" result="coloredBlur" /> x1="0%"
<feMerge> y1="0%"
<feMergeNode in="coloredBlur" /> x2="100%"
<feMergeNode in="SourceGraphic" /> y2="100%"
</feMerge> >
</filter> <stop
offset="0%"
style={{ stopColor: "#ffffff", stopOpacity: 0.98 }}
/>
<stop
offset="100%"
style={{ stopColor: "#f3f4f6", stopOpacity: 0.98 }}
/>
</linearGradient>
<filter id="intense-glow"> {/* Enhanced Filters */}
<feGaussianBlur stdDeviation="8" result="coloredBlur" /> <filter id="petal-glow">
<feComponentTransfer in="coloredBlur" result="brightBlur"> <feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
<feFuncA type="linear" slope="1.5" /> <feMerge>
</feComponentTransfer> <feMergeNode in="coloredBlur" />
<feMerge> <feMergeNode in="SourceGraphic" />
<feMergeNode in="brightBlur" /> </feMerge>
<feMergeNode in="SourceGraphic" /> </filter>
</feMerge>
</filter>
<filter id="center-glow"> <filter id="intense-glow">
<feGaussianBlur stdDeviation="4" result="coloredBlur" /> <feGaussianBlur stdDeviation="8" result="coloredBlur" />
<feMerge> <feComponentTransfer in="coloredBlur" result="brightBlur">
<feMergeNode in="coloredBlur" /> <feFuncA type="linear" slope="1.5" />
<feMergeNode in="coloredBlur" /> </feComponentTransfer>
<feMergeNode in="SourceGraphic" /> <feMerge>
</feMerge> <feMergeNode in="brightBlur" />
</filter> <feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="sparkle-glow"> <filter id="center-glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur" /> <feGaussianBlur stdDeviation="4" result="coloredBlur" />
<feMerge> <feMerge>
<feMergeNode in="coloredBlur" /> <feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" /> <feMergeNode in="coloredBlur" />
</feMerge> <feMergeNode in="SourceGraphic" />
</filter> </feMerge>
</filter>
<filter id="page-shadow"> <filter id="sparkle-glow">
<feDropShadow dx="0" dy="2" stdDeviation="4" floodOpacity="0.15" /> <feGaussianBlur stdDeviation="2" result="coloredBlur" />
</filter> <feMerge>
</defs> <feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
{/* Subtle background glow */} <filter id="page-shadow">
<circle className="bg-glow" cx="128" cy="128" r="120" fill="url(#petal-gradient-3)" opacity="0.08" /> <feDropShadow dx="0" dy="2" stdDeviation="4" floodOpacity="0.15" />
</filter>
</defs>
{/* Outer layer - Large petals (8 petals) */} {/* Subtle background glow */}
<g className="outer-petals"> <circle
{[ className="bg-glow"
{ angle: 0, scaleX: 1.1, scaleY: 1, gradient: 1 }, cx="128"
{ angle: 45, scaleX: 1, scaleY: 1.05, gradient: 2 }, cy="128"
{ angle: 90, scaleX: 1.05, scaleY: 1, gradient: 3 }, r="120"
{ angle: 135, scaleX: 1, scaleY: 1.1, gradient: 4 }, fill="url(#petal-gradient-3)"
{ angle: 180, scaleX: 1.08, scaleY: 1, gradient: 1 }, opacity="0.08"
{ angle: 225, scaleX: 1, scaleY: 1.02, gradient: 2 }, />
{ angle: 270, scaleX: 1.02, scaleY: 1, gradient: 3 },
{ angle: 315, scaleX: 1, scaleY: 1.06, gradient: 4 },
].map((petal, i) => (
<ellipse
key={`outer-${i}`}
className={`petal outer-petal petal-${i}`}
cx="128"
cy="70"
rx="40"
ry="68"
fill={`url(#petal-gradient-${petal.gradient})`}
filter="url(#petal-glow)"
style={{rotate: `${petal.angle}deg`, width: `${128 * petal.scaleX}px`, height: `${70 * petal.scaleY}px`}}
/>
))}
</g>
{/* Middle layer - Medium petals (8 petals, offset) */} {/* Outer layer - Large petals (8 petals) */}
<g className="middle-petals"> <g className="outer-petals">
{[ {[
{ angle: 22.5, scaleX: 1, scaleY: 1, gradient: 2 }, { angle: 0, scaleX: 1.1, scaleY: 1, gradient: 1 },
{ angle: 67.5, scaleX: 1.05, scaleY: 1, gradient: 3 }, { angle: 45, scaleX: 1, scaleY: 1.05, gradient: 2 },
{ angle: 112.5, scaleX: 1, scaleY: 1.02, gradient: 4 }, { angle: 90, scaleX: 1.05, scaleY: 1, gradient: 3 },
{ angle: 157.5, scaleX: 1.02, scaleY: 1, gradient: 1 }, { angle: 135, scaleX: 1, scaleY: 1.1, gradient: 4 },
{ angle: 202.5, scaleX: 1, scaleY: 1.05, gradient: 2 }, { angle: 180, scaleX: 1.08, scaleY: 1, gradient: 1 },
{ angle: 247.5, scaleX: 1.03, scaleY: 1, gradient: 3 }, { angle: 225, scaleX: 1, scaleY: 1.02, gradient: 2 },
{ angle: 292.5, scaleX: 1, scaleY: 1, gradient: 4 }, { angle: 270, scaleX: 1.02, scaleY: 1, gradient: 3 },
{ angle: 337.5, scaleX: 1.02, scaleY: 1, gradient: 1 }, { angle: 315, scaleX: 1, scaleY: 1.06, gradient: 4 },
].map((petal, i) => ( ].map((petal, i) => (
<ellipse <ellipse
key={`middle-${i}`} key={`outer-${i}`}
className={`petal middle-petal petal-m-${i}`} className={`petal outer-petal petal-${i}`}
cx="128" cx="128"
cy="78" cy="70"
rx="34" rx="40"
ry="56" ry="68"
fill={`url(#petal-gradient-${petal.gradient})`} fill={`url(#petal-gradient-${petal.gradient})`}
filter="url(#petal-glow)" filter="url(#petal-glow)"
style={{rotate: `${petal.angle}deg`, width: `${128 * petal.scaleX}px`, height: `${70 * petal.scaleY}px`}} style={{
/> rotate: `${petal.angle}deg`,
))} width: `${128 * petal.scaleX}px`,
</g> height: `${70 * petal.scaleY}px`,
}}
/>
))}
</g>
{/* Inner layer - Small petals (10 petals) */} {/* Middle layer - Medium petals (8 petals, offset) */}
<g className="inner-petals"> <g className="middle-petals">
{[ {[
{ angle: 0, gradient: 3 }, { angle: 22.5, scaleX: 1, scaleY: 1, gradient: 2 },
{ angle: 45, gradient: 4 }, { angle: 67.5, scaleX: 1.05, scaleY: 1, gradient: 3 },
{ angle: 90, gradient: 1 }, { angle: 112.5, scaleX: 1, scaleY: 1.02, gradient: 4 },
{ angle: 135, gradient: 2 }, { angle: 157.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
{ angle: 180, gradient: 3 }, { angle: 202.5, scaleX: 1, scaleY: 1.05, gradient: 2 },
{ angle: 225, gradient: 4 }, { angle: 247.5, scaleX: 1.03, scaleY: 1, gradient: 3 },
{ angle: 270, gradient: 1 }, { angle: 292.5, scaleX: 1, scaleY: 1, gradient: 4 },
{ angle: 315, gradient: 2 }, { angle: 337.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
].map((petal, i) => ( ].map((petal, i) => (
<ellipse <ellipse
key={`inner-${i}`} key={`middle-${i}`}
className={`petal inner-petal petal-i-${i}`} className={`petal middle-petal petal-m-${i}`}
cx="128" cx="128"
cy="88" cy="78"
rx="28" rx="34"
ry="44" ry="56"
fill={`url(#petal-gradient-${petal.gradient})`} fill={`url(#petal-gradient-${petal.gradient})`}
filter="url(#petal-glow)" filter="url(#petal-glow)"
style={{rotate: `${petal.angle}deg`}} style={{
/> rotate: `${petal.angle}deg`,
))} width: `${128 * petal.scaleX}px`,
</g> height: `${70 * petal.scaleY}px`,
}}
/>
))}
</g>
{/* Center circles - Flower stamen */} {/* Inner layer - Small petals (10 petals) */}
<circle <g className="inner-petals">
className="center-circle-outer" {[
cx="128" { angle: 0, gradient: 3 },
cy="128" { angle: 45, gradient: 4 },
r="12" { angle: 90, gradient: 1 },
fill="url(#center-gradient)" { angle: 135, gradient: 2 },
filter="url(#center-glow)" { angle: 180, gradient: 3 },
/> { angle: 225, gradient: 4 },
<circle { angle: 270, gradient: 1 },
className="center-circle-inner" { angle: 315, gradient: 2 },
cx="128" ].map((petal, i) => (
cy="128" <ellipse
r="2" key={`inner-${i}`}
fill="url(#center-inner-gradient)" className={`petal inner-petal petal-i-${i}`}
opacity="0.9" cx="128"
/> cy="88"
rx="28"
ry="44"
fill={`url(#petal-gradient-${petal.gradient})`}
filter="url(#petal-glow)"
style={{ rotate: `${petal.angle}deg` }}
/>
))}
</g>
{/* Center details - tiny stamens */} {/* Center circles - Flower stamen */}
<g className="center-stamens"> <circle
{Array.from({ length: 8 }).map((_, i) => { className="center-circle-outer"
const angle = (360 / 8) * i cx="128"
const x = 128 + Math.cos((angle * Math.PI) / 180) * 10 cy="128"
const y = 128 + Math.sin((angle * Math.PI) / 180) * 10 r="12"
return ( fill="url(#center-gradient)"
<circle filter="url(#center-glow)"
key={`stamen-${i}`} />
className={`stamen stamen-${i}`} <circle
cx={x} className="center-circle-inner"
cy={y} cx="128"
r="2" cy="128"
fill="#d97706" r="2"
opacity="0.8" fill="url(#center-inner-gradient)"
/> opacity="0.9"
) />
})}
</g>
{/* Sparkles - ambient magical effect */} {/* Center details - tiny stamens */}
<g className="sparkles"> <g className="center-stamens">
<circle className="sparkle sparkle-1" cx="180" cy="75" r="3" fill="#fbbf24" filter="url(#sparkle-glow)" /> {Array.from({ length: 8 }).map((_, i) => {
<circle className="sparkle sparkle-2" cx="76" cy="76" r="2.5" fill="#a855f7" filter="url(#sparkle-glow)" /> const angle = (360 / 8) * i;
<circle className="sparkle sparkle-3" cx="180" cy="180" r="2.5" fill="#ec4899" filter="url(#sparkle-glow)" /> const x = 128 + Math.cos((angle * Math.PI) / 180) * 10;
<circle className="sparkle sparkle-4" cx="76" cy="180" r="3" fill="#c026d3" filter="url(#sparkle-glow)" /> const y = 128 + Math.sin((angle * Math.PI) / 180) * 10;
<circle className="sparkle sparkle-5" cx="128" cy="50" r="2" fill="#f0abfc" filter="url(#sparkle-glow)" /> return (
<circle className="sparkle sparkle-6" cx="206" cy="128" r="2" fill="#fb7185" filter="url(#sparkle-glow)" /> <circle
<circle className="sparkle sparkle-7" cx="128" cy="206" r="2.5" fill="#fbbf24" filter="url(#sparkle-glow)" /> key={`stamen-${i}`}
<circle className="sparkle sparkle-8" cx="50" cy="128" r="2" fill="#c084fc" filter="url(#sparkle-glow)" /> className={`stamen stamen-${i}`}
</g> cx={x}
cy={y}
r="2"
fill="#d97706"
opacity="0.8"
/>
);
})}
</g>
{/* Flying bloom particles (visible on hover) */} {/* Sparkles - ambient magical effect */}
<g className="bloom-particles"> <g className="sparkles">
{bloomParticles.map((particle) => ( <circle
<circle className="sparkle sparkle-1"
key={`bloom-particle-${particle.id}`} cx="180"
className={`bloom-particle bloom-particle-${particle.id}`} cy="75"
cx="128" r="3"
cy="128" fill="#fbbf24"
r={particle.size} filter="url(#sparkle-glow)"
fill={`url(#petal-gradient-${(particle.id % 4) + 1})`} />
opacity="0" <circle
filter="url(#sparkle-glow)" className="sparkle sparkle-2"
style={{ cx="76"
'--particle-angle': `${particle.angle}deg`, cy="76"
'--particle-distance': `${particle.distance}px`, r="2.5"
'--particle-delay': `${particle.delay}s`, fill="#a855f7"
} as React.CSSProperties} filter="url(#sparkle-glow)"
/> />
))} <circle
</g> className="sparkle sparkle-3"
</svg> cx="180"
cy="180"
r="2.5"
fill="#ec4899"
filter="url(#sparkle-glow)"
/>
<circle
className="sparkle sparkle-4"
cx="76"
cy="180"
r="3"
fill="#c026d3"
filter="url(#sparkle-glow)"
/>
<circle
className="sparkle sparkle-5"
cx="128"
cy="50"
r="2"
fill="#f0abfc"
filter="url(#sparkle-glow)"
/>
<circle
className="sparkle sparkle-6"
cx="206"
cy="128"
r="2"
fill="#fb7185"
filter="url(#sparkle-glow)"
/>
<circle
className="sparkle sparkle-7"
cx="128"
cy="206"
r="2.5"
fill="#fbbf24"
filter="url(#sparkle-glow)"
/>
<circle
className="sparkle sparkle-8"
cx="50"
cy="128"
r="2"
fill="#c084fc"
filter="url(#sparkle-glow)"
/>
</g>
{/* Optional label */} {/* Flying bloom particles (visible on hover) */}
{showLabel && ( <g className="bloom-particles">
<div className="icon-label"> {bloomParticles.map((particle) => (
<span className="label-text">Pivoine Docs</span> <circle
</div> key={`bloom-particle-${particle.id}`}
)} className={`bloom-particle bloom-particle-${particle.id}`}
</div> cx="128"
) cy="128"
r={particle.size}
fill={`url(#petal-gradient-${(particle.id % 4) + 1})`}
opacity="0"
filter="url(#sparkle-glow)"
style={
{
"--particle-angle": `${particle.angle}deg`,
"--particle-distance": `${particle.distance}px`,
"--particle-delay": `${particle.delay}s`,
} as React.CSSProperties
}
/>
))}
</g>
</svg>
{/* Optional label */}
{showLabel && (
<div className="icon-label">
<span className="label-text">Pivoine Docs</span>
</div>
)}
</div>
);
} }

View File

@@ -1,2 +1,2 @@
export { default as KomposeIcon } from './KomposeIcon' export { default as KomposeIcon } from "./KomposeIcon";
export { default as PivoineDocsIcon } from './PivoineDocsIcon' export { default as PivoineDocsIcon } from "./PivoineDocsIcon";

View File

@@ -6,11 +6,11 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
}); });
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends("next/core-web-vitals", "next/typescript"),
]; ];
export default eslintConfig; export default eslintConfig;

View File

@@ -1,57 +1,57 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'export', output: "export",
reactStrictMode: true, reactStrictMode: true,
// Next.js 15 uses turbopack by default for dev
// No need to explicitly enable swcMinify anymore
// Optimize production build
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
// Image optimization // Next.js 15 uses turbopack by default for dev
images: { // No need to explicitly enable swcMinify anymore
formats: ['image/avif', 'image/webp'],
},
// Headers for security // Optimize production build
async headers() { compiler: {
return [ removeConsole: process.env.NODE_ENV === "production",
{ },
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
}
]
}
]
},
// Enable experimental features if needed // Image optimization
experimental: { images: {
// turbo is now stable in Next.js 15 formats: ["image/avif", "image/webp"],
// Add other experimental features here if needed },
},
turbopack: { // Headers for security
root: '.' async headers() {
} return [
} {
source: "/:path*",
headers: [
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "origin-when-cross-origin",
},
],
},
];
},
export default nextConfig // Enable experimental features if needed
experimental: {
// turbo is now stable in Next.js 15
// Add other experimental features here if needed
},
turbopack: {
root: ".",
},
};
export default nextConfig;

View File

@@ -1,36 +1,36 @@
{ {
"name": "pivoine-docs-hub", "name": "pivoine-docs-hub",
"version": "1.0.0", "version": "1.0.0",
"description": "Documentation hub for Pivoine projects by Valknar", "description": "Documentation hub for Pivoine projects by Valknar",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"type-check": "tsc --noEmit" "type-check": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"lucide-react": "^0.263.1", "lucide-react": "^0.263.1",
"next": "^15.0.3", "next": "^15.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.0.3",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"typescript": "^5.6.0" "typescript": "^5.6.0"
}, },
"engines": { "engines": {
"node": ">=18.18.0", "node": ">=18.18.0",
"pnpm": ">=8.0.0" "pnpm": ">=8.0.0"
}, },
"packageManager": "pnpm@9.0.0" "packageManager": "pnpm@9.0.0"
} }

View File

@@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
"@tailwindcss/postcss": {}, "@tailwindcss/postcss": {},
}, },
} };

View File

@@ -1,45 +1,45 @@
{ {
"name": "Pivoine Docs Hub", "name": "Pivoine Docs Hub",
"short_name": "Pivoine Docs", "short_name": "Pivoine Docs",
"description": "Documentation hub for all Pivoine projects by Valknar", "description": "Documentation hub for all Pivoine projects by Valknar",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#0f172a", "background_color": "#0f172a",
"theme_color": "#a855f7", "theme_color": "#a855f7",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"icons": [ "icons": [
{ {
"src": "/icon.svg", "src": "/icon.svg",
"sizes": "any", "sizes": "any",
"type": "image/svg+xml", "type": "image/svg+xml",
"purpose": "any maskable" "purpose": "any maskable"
}, },
{ {
"src": "/icon-192.png", "src": "/icon-192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
}, },
{ {
"src": "/icon-512.png", "src": "/icon-512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any maskable"
} }
], ],
"categories": ["documentation", "developer", "tools"], "categories": ["documentation", "developer", "tools"],
"screenshots": [ "screenshots": [
{ {
"src": "/screenshot-wide.png", "src": "/screenshot-wide.png",
"sizes": "1280x720", "sizes": "1280x720",
"type": "image/png", "type": "image/png",
"form_factor": "wide" "form_factor": "wide"
}, },
{ {
"src": "/screenshot-narrow.png", "src": "/screenshot-narrow.png",
"sizes": "750x1334", "sizes": "750x1334",
"type": "image/png", "type": "image/png",
"form_factor": "narrow" "form_factor": "narrow"
} }
] ]
} }

View File

@@ -1,8 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}', "./pages/**/*.{js,ts,jsx,tsx,mdx}",
'./components/**/*.{js,ts,jsx,tsx,mdx}', "./components/**/*.{js,ts,jsx,tsx,mdx}",
'./app/**/*.{js,ts,jsx,tsx,mdx}', "./app/**/*.{js,ts,jsx,tsx,mdx}",
], ],
} };

View File

@@ -1,27 +1,27 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noEmit": true, "noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
"name": "next" "name": "next"
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@@ -1,30 +1,30 @@
{ {
"buildCommand": "pnpm build", "buildCommand": "pnpm build",
"devCommand": "pnpm dev", "devCommand": "pnpm dev",
"installCommand": "pnpm install", "installCommand": "pnpm install",
"framework": "nextjs", "framework": "nextjs",
"regions": ["iad1"], "regions": ["iad1"],
"headers": [ "headers": [
{ {
"source": "/(.*)", "source": "/(.*)",
"headers": [ "headers": [
{ {
"key": "X-Content-Type-Options", "key": "X-Content-Type-Options",
"value": "nosniff" "value": "nosniff"
}, },
{ {
"key": "X-Frame-Options", "key": "X-Frame-Options",
"value": "SAMEORIGIN" "value": "SAMEORIGIN"
}, },
{ {
"key": "X-XSS-Protection", "key": "X-XSS-Protection",
"value": "1; mode=block" "value": "1; mode=block"
}, },
{ {
"key": "Referrer-Policy", "key": "Referrer-Policy",
"value": "origin-when-cross-origin" "value": "origin-when-cross-origin"
} }
] ]
} }
] ]
} }

View File

@@ -4,60 +4,55 @@
// gem install scss-lint // gem install scss-lint
module.exports = function (grunt) { module.exports = function (grunt) {
'use strict'; "use strict";
// Project configuration // Project configuration
grunt.initConfig({ grunt.initConfig({
// Metadata // Metadata
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON("package.json"),
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' + banner:
'<%= grunt.template.today("yyyy-mm-dd") %>\n' + "/*! <%= pkg.name %> - v<%= pkg.version %> - " +
'<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
'* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
' Licensed <%= props.license %> */\n', '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
" Licensed <%= props.license %> */\n",
webfont: { webfont: {
icons: { icons: {
src: [ src: ["icons/sbed/*.svg", "icons/lorc/*.svg"],
'icons/sbed/*.svg', dest: "fonts",
'icons/lorc/*.svg' options: {
], styles: "font,icon,extra",
dest: 'fonts', fontFilename: "game-icons",
options: { types: ["eot", "woff2", "woff", "ttf", "svg"],
styles: 'font,icon,extra', syntax: "bootstrap",
fontFilename: 'game-icons', destCss: "css",
types: ['eot', 'woff2', 'woff', 'ttf', 'svg'], destScss: "scss",
syntax: 'bootstrap', templateOptions: {
destCss: 'css', baseClass: "gi",
destScss: 'scss', classPrefix: "gi-",
templateOptions: { },
baseClass: 'gi', fontFamilyName: "GameIcons",
classPrefix: 'gi-' font: "game-icons",
}, stylesheets: ["css", "scss"],
fontFamilyName: 'GameIcons', fontPathVariables: true,
font: 'game-icons', htmlDemo: false,
stylesheets: ['css', 'scss'], },
fontPathVariables: true, },
htmlDemo: false, },
} // CSS Min
} // =======
}, cssmin: {
// CSS Min target: {
// ======= files: {
cssmin: { "css/game-icons.min.css": "css/game-icons.css",
target: { },
files: { },
'css/game-icons.min.css': 'css/game-icons.css' },
} });
}
}
});
// These plugins provide necessary tasks // These plugins provide necessary tasks
grunt.loadNpmTasks('grunt-webfont'); grunt.loadNpmTasks("grunt-webfont");
grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks("grunt-contrib-cssmin");
grunt.registerTask('default', [ grunt.registerTask("default", ["webfont", "cssmin"]);
'webfont',
'cssmin'
]);
}; };

View File

@@ -1,22 +1,22 @@
{ {
"name": "game-icons", "name": "game-icons",
"style": "css/game-icons.css", "style": "css/game-icons.css",
"sass": "scss/game-icons.scss", "sass": "scss/game-icons.scss",
"version": "0.1.1", "version": "0.1.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/nagoshiashumari/game-icons.git" "url": "git://github.com/nagoshiashumari/game-icons.git"
}, },
"devDependencies": { "devDependencies": {
"grunt": "^1.6.1", "grunt": "^1.6.1",
"grunt-contrib-cssmin": "^5.0.0", "grunt-contrib-cssmin": "^5.0.0",
"grunt-webfont": "^1.7.2" "grunt-webfont": "^1.7.2"
}, },
"scripts": { "scripts": {
"build": "grunt", "build": "grunt",
"test": "grunt scsslint" "test": "grunt scsslint"
}, },
"dependencies": { "dependencies": {
"grunt-cli": "^1.5.0" "grunt-cli": "^1.5.0"
} }
} }

View File

@@ -2,3 +2,4 @@
.DS_Store .DS_Store
*.log* *.log*

View File

@@ -1,58 +1,63 @@
export default defineAppConfig({ export default defineAppConfig({
ui: { ui: {
colors: { colors: {
primary: 'emerald', primary: "emerald",
secondary: 'fuchsia', secondary: "fuchsia",
neutral: 'zinc' neutral: "zinc",
}, },
footer: { footer: {
slots: { slots: {
root: 'border-t border-default', root: "border-t border-default",
left: 'text-sm text-muted' left: "text-sm text-muted",
} },
} },
}, },
seo: { seo: {
siteName: 'Kompose' siteName: "Kompose",
}, },
header: { header: {
title: '', title: "",
to: '/', to: "/",
logo: { logo: {
alt: '', alt: "",
light: '', light: "",
dark: '' dark: "",
}, },
search: true, search: true,
colorMode: true, colorMode: true,
links: [{ links: [
'icon': 'i-simple-icons-github', {
'to': 'https://github.com/nuxt-ui-templates/docs', icon: "i-simple-icons-github",
'target': '_blank', to: "https://github.com/nuxt-ui-templates/docs",
'aria-label': 'GitHub' target: "_blank",
}] "aria-label": "GitHub",
}, },
footer: { ],
credits: `kompose © Valknar ${new Date().getFullYear()}`, },
colorMode: false, footer: {
links: [{ credits: `kompose © Valknar ${new Date().getFullYear()}`,
'icon': 'i-simple-icons-x', colorMode: false,
'to': 'https://x.com/bordeaux1981', links: [
'target': '_blank', {
'aria-label': 'Nuxt on X' icon: "i-simple-icons-x",
}, { to: "https://x.com/bordeaux1981",
'icon': 'i-simple-icons-github', target: "_blank",
'to': 'https://github.com/valknarogg', "aria-label": "Nuxt on X",
'target': '_blank', },
'aria-label': 'Valknar on GitHub' {
}] icon: "i-simple-icons-github",
}, to: "https://github.com/valknarogg",
toc: { target: "_blank",
title: 'Table of Contents', "aria-label": "Valknar on GitHub",
bottom: { },
title: 'Community', ],
edit: 'https://code.pivoine.art/valknar/kompose/src/branch/main/docs/content', },
links: [] toc: {
} title: "Table of Contents",
} bottom: {
}) title: "Community",
edit: "https://code.pivoine.art/valknar/kompose/src/branch/main/docs/content",
links: [],
},
},
});

View File

@@ -1,27 +1,31 @@
<script setup lang="ts"> <script setup lang="ts">
const { seo } = useAppConfig() const { seo } = useAppConfig();
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs')) const { data: navigation } = await useAsyncData("navigation", () =>
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), { queryCollectionNavigation("docs"),
server: false );
}) const { data: files } = useLazyAsyncData(
"search",
() => queryCollectionSearchSections("docs"),
{
server: false,
},
);
useHead({ useHead({
meta: [ meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
{ name: 'viewport', content: 'width=device-width, initial-scale=1' } htmlAttrs: {
], lang: "en",
htmlAttrs: { },
lang: 'en' });
}
})
useSeoMeta({ useSeoMeta({
titleTemplate: `%s - ${seo?.siteName}`, titleTemplate: `%s - ${seo?.siteName}`,
ogSiteName: seo?.siteName, ogSiteName: seo?.siteName,
twitterCard: 'summary_large_image' twitterCard: "summary_large_image",
}) });
provide('navigation', navigation) provide("navigation", navigation);
</script> </script>
<template> <template>

View File

@@ -4,30 +4,30 @@
@source "../../../content/**/*"; @source "../../../content/**/*";
@theme static { @theme static {
--container-8xl: 90rem; --container-8xl: 90rem;
--font-sans: 'Public Sans', sans-serif; --font-sans: "Public Sans", sans-serif;
--color-green-50: #EFFDF5; --color-green-50: #effdf5;
--color-green-100: #D9FBE8; --color-green-100: #d9fbe8;
--color-green-200: #B3F5D1; --color-green-200: #b3f5d1;
--color-green-300: #75EDAE; --color-green-300: #75edae;
--color-green-400: #00DC82; --color-green-400: #00dc82;
--color-green-500: #00C16A; --color-green-500: #00c16a;
--color-green-600: #00A155; --color-green-600: #00a155;
--color-green-700: #007F45; --color-green-700: #007f45;
--color-green-800: #016538; --color-green-800: #016538;
--color-green-900: #0A5331; --color-green-900: #0a5331;
--color-green-950: #052E16; --color-green-950: #052e16;
} }
:root { :root {
--ui-container: var(--container-8xl); --ui-container: var(--container-8xl);
} }
h2 > a > span + span { h2 > a > span + span {
@apply size-6 align-text-top; @apply size-6 align-text-top;
} }
h3 > a > span + span { h3 > a > span + span {
@apply size-5 align-text-top; @apply size-5 align-text-top;
} }

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const { footer } = useAppConfig() const { footer } = useAppConfig();
</script> </script>
<template> <template>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content' import type { ContentNavigationItem } from "@nuxt/content";
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation') const navigation = inject<Ref<ContentNavigationItem[]>>("navigation");
const { header } = useAppConfig() const { header } = useAppConfig();
</script> </script>
<template> <template>

View File

@@ -75,50 +75,50 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from "vue";
interface Props { interface Props {
size?: string size?: string;
interactive?: boolean interactive?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
size: '192px', size: "192px",
interactive: true interactive: true,
}) });
const isClicked = ref(false) const isClicked = ref(false);
const showRipple = ref(false) const showRipple = ref(false);
const handleClick = () => { const handleClick = () => {
if (!props.interactive) return if (!props.interactive) return;
isClicked.value = true isClicked.value = true;
showRipple.value = true showRipple.value = true;
setTimeout(() => { setTimeout(() => {
isClicked.value = false isClicked.value = false;
}, 600) }, 600);
setTimeout(() => { setTimeout(() => {
showRipple.value = false showRipple.value = false;
}, 800) }, 800);
} };
const handleHover = () => { const handleHover = () => {
if (!props.interactive) return if (!props.interactive) return;
// Hover animations are handled by CSS // Hover animations are handled by CSS
} };
const handleLeave = () => { const handleLeave = () => {
if (!props.interactive) return if (!props.interactive) return;
// Leave animations are handled by CSS // Leave animations are handled by CSS
} };
const handleTouch = (e: TouchEvent) => { const handleTouch = (e: TouchEvent) => {
if (!props.interactive) return if (!props.interactive) return;
handleClick() handleClick();
} };
</script> </script>
<style scoped> <style scoped>

View File

@@ -6,7 +6,7 @@
--> -->
<script setup> <script setup>
import AppIcon from './AppIcon.vue' import AppIcon from "./AppIcon.vue";
</script> </script>
<template> <template>

View File

@@ -1,21 +1,22 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from "vue";
const props = defineProps({ const props = defineProps({
size: { size: {
type: String, type: String,
default: '42px' // Can be: '24px', '32px', '42px', '56px', etc. default: "42px", // Can be: '24px', '32px', '42px', '56px', etc.
} },
}) });
const isHovered = ref(false) const isHovered = ref(false);
// Load Google Font // Load Google Font
if (typeof document !== 'undefined') { if (typeof document !== "undefined") {
const link = document.createElement('link') const link = document.createElement("link");
link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@800;900&display=swap' link.href =
link.rel = 'stylesheet' "https://fonts.googleapis.com/css2?family=Inter:wght@800;900&display=swap";
document.head.appendChild(link) link.rel = "stylesheet";
document.head.appendChild(link);
} }
</script> </script>

View File

@@ -1,11 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
const props = withDefaults(defineProps<{ title?: string, description?: string, headline?: string }>(), { const props = withDefaults(
title: 'title', defineProps<{ title?: string; description?: string; headline?: string }>(),
description: 'description' {
}) title: "title",
description: "description",
},
);
const title = computed(() => (props.title || '').slice(0, 60)) const title = computed(() => (props.title || "").slice(0, 60));
const description = computed(() => (props.description || '').slice(0, 200)) const description = computed(() => (props.description || "").slice(0, 200));
</script> </script>
<template> <template>

View File

@@ -1,51 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import { useClipboard } from '@vueuse/core' import { useClipboard } from "@vueuse/core";
const route = useRoute() const route = useRoute();
const toast = useToast() const toast = useToast();
const { copy, copied } = useClipboard() const { copy, copied } = useClipboard();
const site = useSiteConfig() const site = useSiteConfig();
const isCopying = ref(false) const isCopying = ref(false);
console.log(site) console.log(site);
const mdPath = computed(() => `${site.url}/raw${route.path}.md`) const mdPath = computed(() => `${site.url}/raw${route.path}.md`);
const items = [ const items = [
{ {
label: 'Copy Markdown link', label: "Copy Markdown link",
icon: 'i-lucide-link', icon: "i-lucide-link",
onSelect() { onSelect() {
copy(mdPath.value) copy(mdPath.value);
toast.add({ toast.add({
title: 'Copied to clipboard', title: "Copied to clipboard",
icon: 'i-lucide-check-circle' icon: "i-lucide-check-circle",
}) });
} },
}, },
{ {
label: 'View as Markdown', label: "View as Markdown",
icon: 'i-simple-icons:markdown', icon: "i-simple-icons:markdown",
target: '_blank', target: "_blank",
to: `/raw${route.path}.md` to: `/raw${route.path}.md`,
}, },
{ {
label: 'Open in ChatGPT', label: "Open in ChatGPT",
icon: 'i-simple-icons:openai', icon: "i-simple-icons:openai",
target: '_blank', target: "_blank",
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}` to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
}, },
{ {
label: 'Open in Claude', label: "Open in Claude",
icon: 'i-simple-icons:anthropic', icon: "i-simple-icons:anthropic",
target: '_blank', target: "_blank",
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}` to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
} },
] ];
async function copyPage() { async function copyPage() {
isCopying.value = true isCopying.value = true;
copy(await $fetch<string>(`/raw${route.path}.md`)) copy(await $fetch<string>(`/raw${route.path}.md`));
isCopying.value = false isCopying.value = false;
} }
</script> </script>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
const { isLoading } = useLoadingIndicator() const { isLoading } = useLoadingIndicator();
const appear = ref(false) const appear = ref(false);
const appeared = ref(false) const appeared = ref(false);
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
appear.value = true appear.value = true;
setTimeout(() => { setTimeout(() => {
appeared.value = true appeared.value = true;
}, 1000) }, 1000);
}, 0) }, 0);
}) });
</script> </script>
<template> <template>

View File

@@ -1,58 +1,67 @@
<script setup lang="ts"> <script setup lang="ts">
interface Star { interface Star {
x: number x: number;
y: number y: number;
size: number size: number;
} }
const props = withDefaults(defineProps<{ const props = withDefaults(
starCount?: number defineProps<{
color?: string starCount?: number;
speed?: 'slow' | 'normal' | 'fast' color?: string;
size?: { min: number, max: number } speed?: "slow" | "normal" | "fast";
}>(), { size?: { min: number; max: number };
starCount: 300, }>(),
color: 'var(--ui-primary)', {
speed: 'normal', starCount: 300,
size: () => ({ color: "var(--ui-primary)",
min: 1, speed: "normal",
max: 2 size: () => ({
}) min: 1,
}) max: 2,
}),
},
);
// Generate random star positions and sizes // Generate random star positions and sizes
const generateStars = (count: number): Star[] => { const generateStars = (count: number): Star[] => {
return Array.from({ length: count }, () => ({ return Array.from({ length: count }, () => ({
x: Math.floor(Math.random() * 2000), x: Math.floor(Math.random() * 2000),
y: Math.floor(Math.random() * 2000), y: Math.floor(Math.random() * 2000),
size: typeof props.size === 'number' size:
? props.size typeof props.size === "number"
: Math.random() * (props.size.max - props.size.min) + props.size.min ? props.size
})) : Math.random() * (props.size.max - props.size.min) + props.size.min,
} }));
};
// Define speed configurations once // Define speed configurations once
const speedMap = { const speedMap = {
slow: { duration: 200, opacity: 0.5, ratio: 0.3 }, slow: { duration: 200, opacity: 0.5, ratio: 0.3 },
normal: { duration: 150, opacity: 0.75, ratio: 0.3 }, normal: { duration: 150, opacity: 0.75, ratio: 0.3 },
fast: { duration: 100, opacity: 1, ratio: 0.4 } fast: { duration: 100, opacity: 1, ratio: 0.4 },
} };
// Use a more efficient approach to generate and store stars // Use a more efficient approach to generate and store stars
const stars = useState<{ slow: Star[], normal: Star[], fast: Star[] }>('stars', () => { const stars = useState<{ slow: Star[]; normal: Star[]; fast: Star[] }>(
return { "stars",
slow: generateStars(Math.floor(props.starCount * speedMap.slow.ratio)), () => {
normal: generateStars(Math.floor(props.starCount * speedMap.normal.ratio)), return {
fast: generateStars(Math.floor(props.starCount * speedMap.fast.ratio)) slow: generateStars(Math.floor(props.starCount * speedMap.slow.ratio)),
} normal: generateStars(
}) Math.floor(props.starCount * speedMap.normal.ratio),
),
fast: generateStars(Math.floor(props.starCount * speedMap.fast.ratio)),
};
},
);
// Compute star layers with different speeds and opacities // Compute star layers with different speeds and opacities
const starLayers = computed(() => [ const starLayers = computed(() => [
{ stars: stars.value.fast, ...speedMap.fast }, { stars: stars.value.fast, ...speedMap.fast },
{ stars: stars.value.normal, ...speedMap.normal }, { stars: stars.value.normal, ...speedMap.normal },
{ stars: stars.value.slow, ...speedMap.slow } { stars: stars.value.slow, ...speedMap.slow },
]) ]);
</script> </script>
<template> <template>

View File

@@ -1,27 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NuxtError } from '#app' import type { NuxtError } from "#app";
defineProps<{ defineProps<{
error: NuxtError error: NuxtError;
}>() }>();
useHead({ useHead({
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: "en",
} },
}) });
useSeoMeta({ useSeoMeta({
title: 'Page not found', title: "Page not found",
description: 'We are sorry but this page could not be found.' description: "We are sorry but this page could not be found.",
}) });
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs')) const { data: navigation } = await useAsyncData("navigation", () =>
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), { queryCollectionNavigation("docs"),
server: false );
}) const { data: files } = useLazyAsyncData(
"search",
() => queryCollectionSearchSections("docs"),
{
server: false,
},
);
provide('navigation', navigation) provide("navigation", navigation);
</script> </script>
<template> <template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content' import type { ContentNavigationItem } from "@nuxt/content";
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation') const navigation = inject<Ref<ContentNavigationItem[]>>("navigation");
</script> </script>
<template> <template>

View File

@@ -1,55 +1,63 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content' import type { ContentNavigationItem } from "@nuxt/content";
import { findPageHeadline } from '@nuxt/content/utils' import { findPageHeadline } from "@nuxt/content/utils";
definePageMeta({ definePageMeta({
layout: 'docs' layout: "docs",
}) });
const route = useRoute() const route = useRoute();
const { toc } = useAppConfig() const { toc } = useAppConfig();
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation') const navigation = inject<Ref<ContentNavigationItem[]>>("navigation");
const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first()) const { data: page } = await useAsyncData(route.path, () =>
queryCollection("docs").path(route.path).first(),
);
if (!page.value) { if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) throw createError({
statusCode: 404,
statusMessage: "Page not found",
fatal: true,
});
} }
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => { const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryCollectionItemSurroundings('docs', route.path, { return queryCollectionItemSurroundings("docs", route.path, {
fields: ['description'] fields: ["description"],
}) });
}) });
const title = page.value.seo?.title || page.value.title const title = page.value.seo?.title || page.value.title;
const description = page.value.seo?.description || page.value.description const description = page.value.seo?.description || page.value.description;
useSeoMeta({ useSeoMeta({
title, title,
ogTitle: title, ogTitle: title,
description, description,
ogDescription: description ogDescription: description,
}) });
const headline = computed(() => findPageHeadline(navigation?.value, page.value?.path)) const headline = computed(() =>
findPageHeadline(navigation?.value, page.value?.path),
);
defineOgImageComponent('Docs', { defineOgImageComponent("Docs", {
headline: headline.value headline: headline.value,
}) });
const links = computed(() => { const links = computed(() => {
const links = [] const links = [];
if (toc?.bottom?.edit) { if (toc?.bottom?.edit) {
links.push({ links.push({
icon: 'i-lucide-external-link', icon: "i-lucide-external-link",
label: 'Edit this page', label: "Edit this page",
to: `${toc.bottom.edit}/${page?.value?.stem}.${page?.value?.extension}`, to: `${toc.bottom.edit}/${page?.value?.stem}.${page?.value?.extension}`,
target: '_blank' target: "_blank",
}) });
} }
return [...links, ...(toc?.bottom?.links || [])].filter(Boolean) return [...links, ...(toc?.bottom?.links || [])].filter(Boolean);
}) });
</script> </script>
<template> <template>

View File

@@ -1,24 +1,28 @@
import { defineContentConfig, defineCollection, z } from '@nuxt/content' import { defineContentConfig, defineCollection, z } from "@nuxt/content";
export default defineContentConfig({ export default defineContentConfig({
collections: { collections: {
landing: defineCollection({ landing: defineCollection({
type: 'page', type: "page",
source: 'index.md' source: "index.md",
}), }),
docs: defineCollection({ docs: defineCollection({
type: 'page', type: "page",
source: { source: {
include: '**', include: "**",
}, },
schema: z.object({ schema: z.object({
links: z.array(z.object({ links: z
label: z.string(), .array(
icon: z.string(), z.object({
to: z.string(), label: z.string(),
target: z.string().optional() icon: z.string(),
})).optional() to: z.string(),
}) target: z.string().optional(),
}) }),
} )
}) .optional(),
}),
}),
},
});

View File

@@ -1,6 +1,6 @@
// @ts-check // @ts-check
import withNuxt from './.nuxt/eslint.config.mjs' import withNuxt from "./.nuxt/eslint.config.mjs";
export default withNuxt( export default withNuxt(
// Your custom configs here // Your custom configs here
) );

View File

@@ -1,103 +1,102 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
baseURL: '/kompose/', baseURL: "/kompose/",
}, },
modules: [ modules: [
'@nuxt/eslint', "@nuxt/eslint",
'@nuxt/image', "@nuxt/image",
'@nuxt/ui', "@nuxt/ui",
'@nuxt/content', "@nuxt/content",
'nuxt-og-image', "nuxt-og-image",
'nuxt-llms' "nuxt-llms",
], ],
// content: { // content: {
// build: { // build: {
// markdown: { // markdown: {
// // Object syntax can be used to override default options // // Object syntax can be used to override default options
// remarkPlugins: { // remarkPlugins: {
// // Override remark-emoji options // // Override remark-emoji options
// 'remark-emoji': { // 'remark-emoji': {
// options: { // options: {
// emoticon: true // emoticon: true
// } // }
// }, // },
// // Disable remark-gfm // // Disable remark-gfm
// 'remark-gfm': false, // 'remark-gfm': false,
// // Add remark-oembed // // Add remark-oembed
// 'remark-oembed': { // 'remark-oembed': {
// // Options // // Options
// } // }
// }, // },
// } // }
// } // }
// }, // },
devtools: { devtools: {
enabled: false enabled: false,
}, },
css: ['~/assets/css/main.css'], css: ["~/assets/css/main.css"],
content: { content: {
build: { build: {
markdown: { markdown: {
toc: { toc: {
searchDepth: 1 searchDepth: 1,
} },
} },
} },
}, },
compatibilityDate: '2024-07-11', compatibilityDate: "2024-07-11",
nitro: { nitro: {
prerender: { prerender: {
routes: [ routes: ["/"],
'/' crawlLinks: true,
], autoSubfolderIndex: false,
crawlLinks: true, },
autoSubfolderIndex: false },
}
},
eslint: { eslint: {
config: { config: {
stylistic: { stylistic: {
commaDangle: 'never', commaDangle: "never",
braceStyle: '1tbs' braceStyle: "1tbs",
} },
} },
}, },
icon: { icon: {
provider: 'iconify' provider: "iconify",
}, },
llms: { llms: {
domain: 'https://docs-template.nuxt.dev/', domain: "https://docs-template.nuxt.dev/",
title: 'Nuxt Docs Template', title: "Nuxt Docs Template",
description: 'A template for building documentation with Nuxt UI and Nuxt Content.', description:
full: { "A template for building documentation with Nuxt UI and Nuxt Content.",
title: 'Nuxt Docs Template - Full Documentation', full: {
description: 'This is the full documentation for the Nuxt Docs Template.' title: "Nuxt Docs Template - Full Documentation",
}, description: "This is the full documentation for the Nuxt Docs Template.",
sections: [ },
{ sections: [
title: 'Getting Started', {
contentCollection: 'docs', title: "Getting Started",
contentFilters: [ contentCollection: "docs",
{ field: 'path', operator: 'LIKE', value: '/getting-started%' } contentFilters: [
] { field: "path", operator: "LIKE", value: "/getting-started%" },
}, ],
{ },
title: 'Essentials', {
contentCollection: 'docs', title: "Essentials",
contentFilters: [ contentCollection: "docs",
{ field: 'path', operator: 'LIKE', value: '/essentials%' } contentFilters: [
] { field: "path", operator: "LIKE", value: "/essentials%" },
} ],
] },
} ],
}) },
});

View File

@@ -1,40 +1,40 @@
{ {
"name": "nuxt-ui-template-docs", "name": "nuxt-ui-template-docs",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"generate": "nuxi generate", "generate": "nuxi generate",
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare", "postinstall": "nuxt prepare",
"lint": "eslint .", "lint": "eslint .",
"typecheck": "nuxt typecheck" "typecheck": "nuxt typecheck"
}, },
"dependencies": { "dependencies": {
"@iconify-json/lucide": "^1.2.68", "@iconify-json/lucide": "^1.2.68",
"@iconify-json/simple-icons": "^1.2.54", "@iconify-json/simple-icons": "^1.2.54",
"@iconify-json/vscode-icons": "^1.2.30", "@iconify-json/vscode-icons": "^1.2.30",
"@nuxt/content": "^3.7.1", "@nuxt/content": "^3.7.1",
"@nuxt/image": "^1.11.0", "@nuxt/image": "^1.11.0",
"@nuxt/ui": "^4.0.1", "@nuxt/ui": "^4.0.1",
"@nuxtjs/mdc": "^0.17.4", "@nuxtjs/mdc": "^0.17.4",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@vite-pwa/nuxt": "^1.0.4", "@vite-pwa/nuxt": "^1.0.4",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"nuxt": "^4.1.2", "nuxt": "^4.1.2",
"nuxt-llms": "0.1.3", "nuxt-llms": "0.1.3",
"nuxt-og-image": "^5.1.11", "nuxt-og-image": "^5.1.11",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "^1.9.0", "@nuxt/eslint": "^1.9.0",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vue-tsc": "^3.1.0" "vue-tsc": "^3.1.0"
}, },
"resolutions": { "resolutions": {
"unimport": "4.1.1" "unimport": "4.1.1"
} }
} }

View File

@@ -1,13 +1,13 @@
{ {
"extends": [ "extends": ["github>nuxt/renovate-config-nuxt"],
"github>nuxt/renovate-config-nuxt" "lockFileMaintenance": {
], "enabled": true
"lockFileMaintenance": { },
"enabled": true "packageRules": [
}, {
"packageRules": [{ "matchDepTypes": ["resolutions"],
"matchDepTypes": ["resolutions"], "enabled": false
"enabled": false }
}], ],
"postUpdateOptions": ["pnpmDedupe"] "postUpdateOptions": ["pnpmDedupe"]
} }

View File

@@ -1,27 +1,40 @@
import { withLeadingSlash } from 'ufo' import { withLeadingSlash } from "ufo";
import { stringify } from 'minimark/stringify' import { stringify } from "minimark/stringify";
import { queryCollection } from '@nuxt/content/nitro' import { queryCollection } from "@nuxt/content/nitro";
import type { Collections } from '@nuxt/content' import type { Collections } from "@nuxt/content";
export default eventHandler(async (event) => { export default eventHandler(async (event) => {
const slug = getRouterParams(event)['slug.md'] const slug = getRouterParams(event)["slug.md"];
if (!slug?.endsWith('.md')) { if (!slug?.endsWith(".md")) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) throw createError({
} statusCode: 404,
statusMessage: "Page not found",
fatal: true,
});
}
const path = withLeadingSlash(slug.replace('.md', '')) const path = withLeadingSlash(slug.replace(".md", ""));
const page = await queryCollection(event, 'docs' as keyof Collections).path(path).first() const page = await queryCollection(event, "docs" as keyof Collections)
if (!page) { .path(path)
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true }) .first();
} if (!page) {
throw createError({
statusCode: 404,
statusMessage: "Page not found",
fatal: true,
});
}
// Add title and description to the top of the page if missing // Add title and description to the top of the page if missing
if (page.body.value[0]?.[0] !== 'h1') { if (page.body.value[0]?.[0] !== "h1") {
page.body.value.unshift(['blockquote', {}, page.description]) page.body.value.unshift(["blockquote", {}, page.description]);
page.body.value.unshift(['h1', {}, page.title]) page.body.value.unshift(["h1", {}, page.title]);
} }
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8') setHeader(event, "Content-Type", "text/markdown; charset=utf-8");
return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' }) return stringify(
}) { ...page.body, type: "minimark" },
{ format: "markdown/html" },
);
});

View File

@@ -1,4 +1,4 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json" "extends": "./.nuxt/tsconfig.json"
} }

View File

@@ -1,15 +1,15 @@
{ {
"eslint.useFlatConfig": true, "eslint.useFlatConfig": true,
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"javascriptreact", "javascriptreact",
"typescript", "typescript",
"typescriptreact" "typescriptreact"
], ],
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ {
"mode": "auto" "mode": "auto"
} }
], ],
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

View File

@@ -1,25 +1,25 @@
import globals from "globals" import globals from "globals";
import pluginJs from "@eslint/js" import pluginJs from "@eslint/js";
import * as tseslint from "typescript-eslint" import * as tseslint from "typescript-eslint";
export default tseslint.config({ export default tseslint.config({
files: ["src/**/*.{js,mjs,cjs,ts}"], files: ["src/**/*.{js,mjs,cjs,ts}"],
extends: [pluginJs.configs.recommended], extends: [pluginJs.configs.recommended],
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.node, ...globals.node,
}, },
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
project: true, project: true,
}, },
}, },
plugins: { plugins: {
"@typescript-eslint": tseslint.plugin, "@typescript-eslint": tseslint.plugin,
}, },
rules: { rules: {
"@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }], "@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }],
"no-unused-vars": "off", "no-unused-vars": "off",
}, },
}) });

View File

@@ -1,71 +1,71 @@
{ {
"name": "backend", "name": "backend",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"start": "bun run src/index.ts", "start": "bun run src/index.ts",
"dev": "watchexec -r -e ts bun run src/index.ts", "dev": "watchexec -r -e ts bun run src/index.ts",
"build": "rm -rf dist && tsc -b tsconfig.build.json", "build": "rm -rf dist && tsc -b tsconfig.build.json",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"generate": "prisma generate", "generate": "prisma generate",
"generate:sql": "prisma generate --sql && pnpm exec prettier --write prisma/client", "generate:sql": "prisma generate --sql && pnpm exec prettier --write prisma/client",
"test": "dotenv -e .env.test -- vitest" "test": "dotenv -e .env.test -- vitest"
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./shared": "./src/shared.ts" "./shared": "./src/shared.ts"
}, },
"prisma": { "prisma": {
"seed": "bun prisma/seed.ts" "seed": "bun prisma/seed.ts"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^6.7.0", "@prisma/client": "^6.7.0",
"@trpc/server": "11.0.0-rc.730", "@trpc/server": "11.0.0-rc.730",
"bcryptjs": "^3.0.0", "bcryptjs": "^3.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"csv-parse": "^5.6.0", "csv-parse": "^5.6.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"express": "^4.21.2", "express": "^4.21.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"p-map": "^7.0.3", "p-map": "^7.0.3",
"superjson": "^2.2.2", "superjson": "^2.2.2",
"uuid": "^11.0.5", "uuid": "^11.0.5",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.19.0",
"@faker-js/faker": "^9.5.0", "@faker-js/faker": "^9.5.0",
"@repo/eslint-config": "workspace:*", "@repo/eslint-config": "workspace:*",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/dotenv": "^8.2.3", "@types/dotenv": "^8.2.3",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.8", "@types/jsonwebtoken": "^9.0.8",
"@types/node": "^22.12.0", "@types/node": "^22.12.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.7", "@types/swagger-ui-express": "^4.1.7",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitest/ui": "3.0.5", "@vitest/ui": "3.0.5",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.19.0", "eslint": "^9.19.0",
"globals": "^15.14.0", "globals": "^15.14.0",
"prisma": "^6.7.0", "prisma": "^6.7.0",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.22.0",
"vitest": "^3.0.5" "vitest": "^3.0.5"
} }
} }

View File

@@ -1,19 +1,19 @@
export interface $DbEnums {} export interface $DbEnums {}
export namespace $DbEnums { export namespace $DbEnums {
type CampaignStatus = type CampaignStatus =
| "DRAFT" | "DRAFT"
| "SCHEDULED" | "SCHEDULED"
| "SENDING" | "SENDING"
| "COMPLETED" | "COMPLETED"
| "CANCELLED"; | "CANCELLED";
type MessageStatus = type MessageStatus =
| "QUEUED" | "QUEUED"
| "PENDING" | "PENDING"
| "SENT" | "SENT"
| "OPENED" | "OPENED"
| "CLICKED" | "CLICKED"
| "FAILED" | "FAILED"
| "RETRYING"; | "RETRYING";
type SmtpEncryption = "STARTTLS" | "SSL_TLS" | "NONE"; type SmtpEncryption = "STARTTLS" | "SSL_TLS" | "NONE";
} }

View File

@@ -4,19 +4,19 @@ import * as $runtime from "../runtime/library";
* @param text * @param text
*/ */
export const countDbSize: ( export const countDbSize: (
text: string, text: string,
) => $runtime.TypedSql<countDbSize.Parameters, countDbSize.Result>; ) => $runtime.TypedSql<countDbSize.Parameters, countDbSize.Result>;
export namespace countDbSize { export namespace countDbSize {
export type Parameters = [text: string]; export type Parameters = [text: string];
export type Result = { export type Result = {
organization_id: string; organization_id: string;
organization_name: string; organization_name: string;
campaign_count: bigint | null; campaign_count: bigint | null;
template_count: bigint | null; template_count: bigint | null;
message_count: bigint | null; message_count: bigint | null;
subscriber_count: bigint | null; subscriber_count: bigint | null;
list_count: bigint | null; list_count: bigint | null;
total_size_mb: $runtime.Decimal | null; total_size_mb: $runtime.Decimal | null;
}; };
} }

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
exports.countDbSize = /*#__PURE__*/ $mkFactory( exports.countDbSize = /*#__PURE__*/ $mkFactory(
'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;', 'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
export const countDbSize = /*#__PURE__*/ $mkFactory( export const countDbSize = /*#__PURE__*/ $mkFactory(
'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;', 'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;',
); );

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
exports.countDbSize = /*#__PURE__*/ $mkFactory( exports.countDbSize = /*#__PURE__*/ $mkFactory(
'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;', 'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
export const countDbSize = /*#__PURE__*/ $mkFactory( export const countDbSize = /*#__PURE__*/ $mkFactory(
'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;', 'WITH organization_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(c.content)), 0) as campaign_content_size,\nCOALESCE(SUM(LENGTH(c.subject)), 0) as campaign_subject_size,\nCOALESCE(SUM(LENGTH(c.title)), 0) as campaign_title_size,\nCOALESCE(SUM(LENGTH(c.description)), 0) as campaign_description_size,\nCOUNT(*) as campaign_count\nFROM "Campaign" c\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\ntemplate_storage AS (\nSELECT\nt."organizationId",\nCOALESCE(SUM(LENGTH(t.content)), 0) as template_content_size,\nCOALESCE(SUM(LENGTH(t.name)), 0) as template_name_size,\nCOALESCE(SUM(LENGTH(t.description)), 0) as template_description_size,\nCOUNT(*) as template_count\nFROM "Template" t\nWHERE t."organizationId" = $1\nGROUP BY t."organizationId"\n),\nmessage_storage AS (\nSELECT\nc."organizationId",\nCOALESCE(SUM(LENGTH(m.content)), 0) as message_content_size,\nCOALESCE(SUM(LENGTH(m.error)), 0) as message_error_size,\nCOUNT(*) as message_count\nFROM "Message" m\nJOIN "Campaign" c ON c.id = m."campaignId"\nWHERE c."organizationId" = $1\nGROUP BY c."organizationId"\n),\nsubscriber_storage AS (\nSELECT\ns."organizationId",\nCOALESCE(SUM(LENGTH(s.email)), 0) as subscriber_email_size,\nCOALESCE(SUM(LENGTH(s.name)), 0) as subscriber_name_size,\nCOUNT(*) as subscriber_count\nFROM "Subscriber" s\nWHERE s."organizationId" = $1\nGROUP BY s."organizationId"\n),\nlist_storage AS (\nSELECT\nl."organizationId",\nCOALESCE(SUM(LENGTH(l.name)), 0) as list_name_size,\nCOALESCE(SUM(LENGTH(l.description)), 0) as list_description_size,\nCOUNT(*) as list_count\nFROM "List" l\nWHERE l."organizationId" = $1\nGROUP BY l."organizationId"\n)\n\nSELECT\no.id as organization_id,\no.name as organization_name,\nCOALESCE(os.campaign_count, 0) as campaign_count,\nCOALESCE(ts.template_count, 0) as template_count,\nCOALESCE(ms.message_count, 0) as message_count,\nCOALESCE(ss.subscriber_count, 0) as subscriber_count,\nCOALESCE(ls.list_count, 0) as list_count,\n(\nCOALESCE(os.campaign_content_size, 0) +\nCOALESCE(os.campaign_subject_size, 0) +\nCOALESCE(os.campaign_title_size, 0) +\nCOALESCE(os.campaign_description_size, 0) +\nCOALESCE(ts.template_content_size, 0) +\nCOALESCE(ts.template_name_size, 0) +\nCOALESCE(ts.template_description_size, 0) +\nCOALESCE(ms.message_content_size, 0) +\nCOALESCE(ms.message_error_size, 0) +\nCOALESCE(ss.subscriber_email_size, 0) +\nCOALESCE(ss.subscriber_name_size, 0) +\nCOALESCE(ls.list_name_size, 0) +\nCOALESCE(ls.list_description_size, 0)\n) / 1024.0 / 1024.0 as total_size_mb\nFROM "Organization" o\nLEFT JOIN organization_storage os ON o.id = os."organizationId"\nLEFT JOIN template_storage ts ON o.id = ts."organizationId"\nLEFT JOIN message_storage ms ON o.id = ms."organizationId"\nLEFT JOIN subscriber_storage ss ON o.id = ss."organizationId"\nLEFT JOIN list_storage ls ON o.id = ls."organizationId"\nWHERE o.id = $1;',
); );

View File

@@ -4,15 +4,15 @@ import * as $runtime from "../runtime/library";
* @param text * @param text
*/ */
export const countDistinctRecipients: ( export const countDistinctRecipients: (
text: string, text: string,
) => $runtime.TypedSql< ) => $runtime.TypedSql<
countDistinctRecipients.Parameters, countDistinctRecipients.Parameters,
countDistinctRecipients.Result countDistinctRecipients.Result
>; >;
export namespace countDistinctRecipients { export namespace countDistinctRecipients {
export type Parameters = [text: string]; export type Parameters = [text: string];
export type Result = { export type Result = {
count: bigint | null; count: bigint | null;
}; };
} }

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
exports.countDistinctRecipients = /*#__PURE__*/ $mkFactory( exports.countDistinctRecipients = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
export const countDistinctRecipients = /*#__PURE__*/ $mkFactory( export const countDistinctRecipients = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;',
); );

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
exports.countDistinctRecipients = /*#__PURE__*/ $mkFactory( exports.countDistinctRecipients = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
export const countDistinctRecipients = /*#__PURE__*/ $mkFactory( export const countDistinctRecipients = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1;',
); );

View File

@@ -6,17 +6,17 @@ import * as $runtime from "../runtime/library";
* @param timestamp * @param timestamp
*/ */
export const countDistinctRecipientsInTimeRange: ( export const countDistinctRecipientsInTimeRange: (
text: string, text: string,
timestamp: Date, timestamp: Date,
timestamp: Date, timestamp: Date,
) => $runtime.TypedSql< ) => $runtime.TypedSql<
countDistinctRecipientsInTimeRange.Parameters, countDistinctRecipientsInTimeRange.Parameters,
countDistinctRecipientsInTimeRange.Result countDistinctRecipientsInTimeRange.Result
>; >;
export namespace countDistinctRecipientsInTimeRange { export namespace countDistinctRecipientsInTimeRange {
export type Parameters = [text: string, timestamp: Date, timestamp: Date]; export type Parameters = [text: string, timestamp: Date, timestamp: Date];
export type Result = { export type Result = {
count: bigint | null; count: bigint | null;
}; };
} }

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
exports.countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory( exports.countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
export const countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory( export const countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;',
); );

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
exports.countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory( exports.countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
export const countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory( export const countDistinctRecipientsInTimeRange = /*#__PURE__*/ $mkFactory(
'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;', 'SELECT COUNT(DISTINCT "subscriberId")\nFROM "Message" m\nJOIN "Campaign" c ON m."campaignId" = c.id\nWHERE c."organizationId" = $1\nAND m."createdAt" >= $2\nAND m."createdAt" <= $3;',
); );

View File

@@ -3,8 +3,8 @@
"use strict"; "use strict";
exports.countDbSize = require("./countDbSize.edge.js").countDbSize; exports.countDbSize = require("./countDbSize.edge.js").countDbSize;
exports.countDistinctRecipients = exports.countDistinctRecipients =
require("./countDistinctRecipients.edge.js").countDistinctRecipients; require("./countDistinctRecipients.edge.js").countDistinctRecipients;
exports.countDistinctRecipientsInTimeRange = exports.countDistinctRecipientsInTimeRange =
require("./countDistinctRecipientsInTimeRange.edge.js").countDistinctRecipientsInTimeRange; require("./countDistinctRecipientsInTimeRange.edge.js").countDistinctRecipientsInTimeRange;
exports.subscriberGrowthQuery = exports.subscriberGrowthQuery =
require("./subscriberGrowthQuery.edge.js").subscriberGrowthQuery; require("./subscriberGrowthQuery.edge.js").subscriberGrowthQuery;

View File

@@ -3,8 +3,8 @@
"use strict"; "use strict";
exports.countDbSize = require("./countDbSize.js").countDbSize; exports.countDbSize = require("./countDbSize.js").countDbSize;
exports.countDistinctRecipients = exports.countDistinctRecipients =
require("./countDistinctRecipients.js").countDistinctRecipients; require("./countDistinctRecipients.js").countDistinctRecipients;
exports.countDistinctRecipientsInTimeRange = exports.countDistinctRecipientsInTimeRange =
require("./countDistinctRecipientsInTimeRange.js").countDistinctRecipientsInTimeRange; require("./countDistinctRecipientsInTimeRange.js").countDistinctRecipientsInTimeRange;
exports.subscriberGrowthQuery = exports.subscriberGrowthQuery =
require("./subscriberGrowthQuery.js").subscriberGrowthQuery; require("./subscriberGrowthQuery.js").subscriberGrowthQuery;

View File

@@ -6,18 +6,18 @@ import * as $runtime from "../runtime/library";
* @param timestamp * @param timestamp
*/ */
export const subscriberGrowthQuery: ( export const subscriberGrowthQuery: (
text: string, text: string,
timestamp: Date, timestamp: Date,
timestamp: Date, timestamp: Date,
) => $runtime.TypedSql< ) => $runtime.TypedSql<
subscriberGrowthQuery.Parameters, subscriberGrowthQuery.Parameters,
subscriberGrowthQuery.Result subscriberGrowthQuery.Result
>; >;
export namespace subscriberGrowthQuery { export namespace subscriberGrowthQuery {
export type Parameters = [text: string, timestamp: Date, timestamp: Date]; export type Parameters = [text: string, timestamp: Date, timestamp: Date];
export type Result = { export type Result = {
date: Date | null; date: Date | null;
count: bigint | null; count: bigint | null;
}; };
} }

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
exports.subscriberGrowthQuery = /*#__PURE__*/ $mkFactory( exports.subscriberGrowthQuery = /*#__PURE__*/ $mkFactory(
'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC', 'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
export const subscriberGrowthQuery = /*#__PURE__*/ $mkFactory( export const subscriberGrowthQuery = /*#__PURE__*/ $mkFactory(
'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC', 'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC',
); );

View File

@@ -3,5 +3,5 @@
"use strict"; "use strict";
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library"); const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
exports.subscriberGrowthQuery = /*#__PURE__*/ $mkFactory( exports.subscriberGrowthQuery = /*#__PURE__*/ $mkFactory(
'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC', 'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC',
); );

View File

@@ -2,5 +2,5 @@
/* eslint-disable */ /* eslint-disable */
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library"; import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
export const subscriberGrowthQuery = /*#__PURE__*/ $mkFactory( export const subscriberGrowthQuery = /*#__PURE__*/ $mkFactory(
'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC', 'SELECT\nDATE_TRUNC(\'day\', "createdAt") as date,\nCOUNT(*) as count\nFROM "public"."Subscriber"\nWHERE "organizationId" = $1\nAND "createdAt" >= $2\nAND "createdAt" <= $3\nGROUP BY DATE_TRUNC(\'day\', "createdAt")\nORDER BY date ASC',
); );

View File

@@ -1,99 +1,99 @@
import { hashPassword } from "../src/utils/auth" import { hashPassword } from "../src/utils/auth";
import { prisma } from "../src/utils/prisma" import { prisma } from "../src/utils/prisma";
import { SmtpEncryption } from "./client" import { SmtpEncryption } from "./client";
import dayjs from "dayjs" import dayjs from "dayjs";
async function seed() { async function seed() {
if (!(await prisma.organization.findFirst())) { if (!(await prisma.organization.findFirst())) {
await prisma.organization.create({ await prisma.organization.create({
data: { data: {
name: "Test Organization", name: "Test Organization",
description: "Test Description", description: "Test Description",
GeneralSettings: { GeneralSettings: {
create: {}, create: {},
}, },
EmailDeliverySettings: { EmailDeliverySettings: {
create: { create: {
rateLimit: 100, rateLimit: 100,
}, },
}, },
SmtpSettings: { SmtpSettings: {
create: { create: {
host: "smtp.test.com", host: "smtp.test.com",
port: 587, port: 587,
username: "test", username: "test",
password: "test", password: "test",
encryption: SmtpEncryption.STARTTLS, encryption: SmtpEncryption.STARTTLS,
}, },
}, },
}, },
}) });
} }
const orgId = ( const orgId = (
await prisma.organization.findFirst({ await prisma.organization.findFirst({
orderBy: { orderBy: {
createdAt: "asc", createdAt: "asc",
}, },
}) })
)?.id )?.id;
if (!orgId) { if (!orgId) {
throw new Error("not reachable") throw new Error("not reachable");
} }
if (!(await prisma.user.findFirst())) { if (!(await prisma.user.findFirst())) {
await prisma.user.create({ await prisma.user.create({
data: { data: {
name: "Admin", name: "Admin",
email: "admin@example.com", email: "admin@example.com",
password: await hashPassword("password123"), password: await hashPassword("password123"),
UserOrganizations: { UserOrganizations: {
create: { create: {
organizationId: orgId, organizationId: orgId,
}, },
}, },
}, },
}) });
} }
// Create 5000 subscribers // Create 5000 subscribers
const subscribers = Array.from({ length: 5000 }, (_, i) => ({ const subscribers = Array.from({ length: 5000 }, (_, i) => ({
name: `Subscriber ${i + 1}`, name: `Subscriber ${i + 1}`,
email: `subscriber${i + 1}@example.com`, email: `subscriber${i + 1}@example.com`,
organizationId: orgId, organizationId: orgId,
createdAt: dayjs().subtract(12, "days").toDate(), createdAt: dayjs().subtract(12, "days").toDate(),
})) }));
await prisma.subscriber.createMany({ await prisma.subscriber.createMany({
data: subscribers, data: subscribers,
skipDuplicates: true, skipDuplicates: true,
}) });
// Then 10 more for each day for 10 days // Then 10 more for each day for 10 days
const now = new Date() const now = new Date();
for (let d = 0; d < 10; d++) { for (let d = 0; d < 10; d++) {
const day = dayjs(now) const day = dayjs(now)
.subtract(d + 1, "day") .subtract(d + 1, "day")
.toDate() .toDate();
const dailySubs = Array.from({ length: 10 }, (_, i) => ({ const dailySubs = Array.from({ length: 10 }, (_, i) => ({
name: `DailySub ${d + 1}-${i + 1}`, name: `DailySub ${d + 1}-${i + 1}`,
email: `dailysub${d + 1}-${i + 1}@example.com`, email: `dailysub${d + 1}-${i + 1}@example.com`,
organizationId: orgId, organizationId: orgId,
createdAt: day, createdAt: day,
updatedAt: day, updatedAt: day,
})) }));
await prisma.subscriber.createMany({ await prisma.subscriber.createMany({
data: dailySubs, data: dailySubs,
skipDuplicates: true, skipDuplicates: true,
}) });
} }
} }
seed() seed()
.then(async () => { .then(async () => {
await prisma.$disconnect() await prisma.$disconnect();
}) })
.catch(async (e) => { .catch(async (e) => {
console.error(e) console.error(e);
await prisma.$disconnect() await prisma.$disconnect();
}) });

View File

@@ -1,46 +1,46 @@
import { prisma } from "../utils/prisma" import { prisma } from "../utils/prisma";
import express, { NextFunction } from "express" import express, { NextFunction } from "express";
export const authenticateApiKey = async ( export const authenticateApiKey = async (
req: express.Request, req: express.Request,
res: express.Response, res: express.Response,
next: NextFunction next: NextFunction,
) => { ) => {
const apiKey = req.header("x-api-key") const apiKey = req.header("x-api-key");
if (!apiKey) { if (!apiKey) {
res.status(401).json({ error: "Missing API Key" }) res.status(401).json({ error: "Missing API Key" });
return return;
} }
try { try {
const keyRecord = await prisma.apiKey.findUnique({ const keyRecord = await prisma.apiKey.findUnique({
where: { key: apiKey }, where: { key: apiKey },
select: { id: true, Organization: true }, select: { id: true, Organization: true },
}) });
if (!keyRecord) { if (!keyRecord) {
res.status(401).json({ error: "Invalid API Key" }) res.status(401).json({ error: "Invalid API Key" });
return return;
} }
// Update lastUsed timestamp asynchronously, don't await // Update lastUsed timestamp asynchronously, don't await
prisma.apiKey prisma.apiKey
.update({ .update({
where: { id: keyRecord.id }, where: { id: keyRecord.id },
data: { lastUsed: new Date() }, data: { lastUsed: new Date() },
}) })
.catch((updateError) => { .catch((updateError) => {
// Log the error but don't block the request // Log the error but don't block the request
console.error( console.error(
"Failed to update API key lastUsed timestamp", "Failed to update API key lastUsed timestamp",
updateError updateError,
) );
}) });
req.organization = keyRecord.Organization req.organization = keyRecord.Organization;
next() next();
} catch (error) { } catch (error) {
console.error("Error validating API key", error) console.error("Error validating API key", error);
res.status(500).json({ error: "Server error" }) res.status(500).json({ error: "Server error" });
} }
} };

File diff suppressed because it is too large Load Diff

View File

@@ -1,165 +1,165 @@
import * as trpcExpress from "@trpc/server/adapters/express" import * as trpcExpress from "@trpc/server/adapters/express";
import path from "path" import path from "path";
import express from "express" import express from "express";
import cors from "cors" import cors from "cors";
import { prisma } from "./utils/prisma" import { prisma } from "./utils/prisma";
import swaggerUi from "swagger-ui-express" import swaggerUi from "swagger-ui-express";
import { createContext, router } from "./trpc" import { createContext, router } from "./trpc";
import { userRouter } from "./user/router" import { userRouter } from "./user/router";
import { listRouter } from "./list/router" import { listRouter } from "./list/router";
import { organizationRouter } from "./organization/router" import { organizationRouter } from "./organization/router";
import { subscriberRouter } from "./subscriber/router" import { subscriberRouter } from "./subscriber/router";
import { templateRouter } from "./template/router" import { templateRouter } from "./template/router";
import { campaignRouter } from "./campaign/router" import { campaignRouter } from "./campaign/router";
import { messageRouter } from "./message/router" import { messageRouter } from "./message/router";
import { settingsRouter } from "./settings/router" import { settingsRouter } from "./settings/router";
import swaggerSpec from "./swagger" import swaggerSpec from "./swagger";
import { apiRouter } from "./api/server" import { apiRouter } from "./api/server";
import { dashboardRouter } from "./dashboard/router" import { dashboardRouter } from "./dashboard/router";
import { statsRouter } from "./stats/router" import { statsRouter } from "./stats/router";
import { ONE_PX_PNG } from "./constants" import { ONE_PX_PNG } from "./constants";
const appRouter = router({ const appRouter = router({
user: userRouter, user: userRouter,
list: listRouter, list: listRouter,
organization: organizationRouter, organization: organizationRouter,
subscriber: subscriberRouter, subscriber: subscriberRouter,
template: templateRouter, template: templateRouter,
campaign: campaignRouter, campaign: campaignRouter,
message: messageRouter, message: messageRouter,
settings: settingsRouter, settings: settingsRouter,
dashboard: dashboardRouter, dashboard: dashboardRouter,
stats: statsRouter, stats: statsRouter,
}) });
export type AppRouter = typeof appRouter export type AppRouter = typeof appRouter;
export const app = express() export const app = express();
app.use( app.use(
cors({ cors({
origin: ["http://localhost:3000", "http://localhost:4173"], origin: ["http://localhost:3000", "http://localhost:4173"],
}) }),
) );
app.use(express.json()) app.use(express.json());
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec)) app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
app.get("/t/:id", async (req, res) => { app.get("/t/:id", async (req, res) => {
try { try {
const { id } = req.params const { id } = req.params;
const subscriberId = req.query.sid const subscriberId = req.query.sid;
const trackedLink = await prisma.trackedLink.findUnique({ const trackedLink = await prisma.trackedLink.findUnique({
where: { id }, where: { id },
}) });
if (!trackedLink) { if (!trackedLink) {
res.status(404).send("Link not found") res.status(404).send("Link not found");
return return;
} }
res.redirect(trackedLink.url) res.redirect(trackedLink.url);
if (subscriberId && typeof subscriberId === "string") { if (subscriberId && typeof subscriberId === "string") {
await prisma await prisma
.$transaction(async (tx) => { .$transaction(async (tx) => {
// add a new click // add a new click
await tx.click.create({ await tx.click.create({
data: { data: {
subscriberId, subscriberId,
trackedLinkId: trackedLink.id, trackedLinkId: trackedLink.id,
}, },
}) });
if (!trackedLink.campaignId) return if (!trackedLink.campaignId) return;
const message = await tx.message.findFirst({ const message = await tx.message.findFirst({
where: { where: {
campaignId: trackedLink.campaignId, campaignId: trackedLink.campaignId,
subscriberId, subscriberId,
status: { status: {
not: "CLICKED", not: "CLICKED",
}, },
}, },
}) });
if (!message) return if (!message) return;
await tx.message.update({ await tx.message.update({
where: { where: {
id: message.id, id: message.id,
}, },
data: { data: {
status: "CLICKED", status: "CLICKED",
}, },
}) });
}) })
.catch((error) => { .catch((error) => {
console.error("Error updating message status", error) console.error("Error updating message status", error);
}) });
} }
} catch (error) { } catch (error) {
res.status(404).send("Link not found") res.status(404).send("Link not found");
} }
}) });
app.get("/img/:id/img.png", async (req, res) => { app.get("/img/:id/img.png", async (req, res) => {
// Send pixel immediately // Send pixel immediately
const pixel = Buffer.from(ONE_PX_PNG, "base64") const pixel = Buffer.from(ONE_PX_PNG, "base64");
res.setHeader("Content-Type", "image/png") res.setHeader("Content-Type", "image/png");
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache") res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0") res.setHeader("Expires", "0");
res.end(pixel) res.end(pixel);
const id = req.params.id const id = req.params.id;
try { try {
await prisma.$transaction(async (tx) => { await prisma.$transaction(async (tx) => {
const message = await tx.message.findUnique({ const message = await tx.message.findUnique({
where: { where: {
id, id,
Campaign: { Campaign: {
openTracking: true, openTracking: true,
}, },
}, },
}) });
if (!message) { if (!message) {
return return;
} }
if (message.status !== "SENT") return if (message.status !== "SENT") return;
await tx.message.update({ await tx.message.update({
where: { id }, where: { id },
data: { data: {
status: "OPENED", status: "OPENED",
}, },
}) });
}) });
} catch (error) { } catch (error) {
console.error("Error updating message status", error) console.error("Error updating message status", error);
} }
}) });
app.use("/api", apiRouter) app.use("/api", apiRouter);
app.use( app.use(
"/trpc", "/trpc",
trpcExpress.createExpressMiddleware({ trpcExpress.createExpressMiddleware({
router: appRouter, router: appRouter,
createContext, createContext,
}) }),
) );
const staticPath = path.join(__dirname, "..", "..", "web", "dist") const staticPath = path.join(__dirname, "..", "..", "web", "dist");
// serve SPA content // serve SPA content
app.use(express.static(staticPath)) app.use(express.static(staticPath));
app.get("*", (_, res) => { app.get("*", (_, res) => {
res.sendFile(path.join(staticPath, "index.html")) res.sendFile(path.join(staticPath, "index.html"));
}) });

File diff suppressed because it is too large Load Diff

View File

@@ -1,249 +1,249 @@
import { z } from "zod" import { z } from "zod";
import { authProcedure } from "../trpc" import { authProcedure } from "../trpc";
import { prisma } from "../utils/prisma" import { prisma } from "../utils/prisma";
import { TRPCError } from "@trpc/server" import { TRPCError } from "@trpc/server";
import { paginationSchema } from "../utils/schemas" import { paginationSchema } from "../utils/schemas";
import { Prisma } from "../../prisma/client" import { Prisma } from "../../prisma/client";
import { resolveProps } from "../utils/pProps" import { resolveProps } from "../utils/pProps";
export const listCampaigns = authProcedure export const listCampaigns = authProcedure
.input(z.object({ organizationId: z.string() }).merge(paginationSchema)) .input(z.object({ organizationId: z.string() }).merge(paginationSchema))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const userOrganization = await prisma.userOrganization.findFirst({ const userOrganization = await prisma.userOrganization.findFirst({
where: { where: {
userId: ctx.user.id, userId: ctx.user.id,
organizationId: input.organizationId, organizationId: input.organizationId,
}, },
}) });
if (!userOrganization) { if (!userOrganization) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Organization not found", message: "Organization not found",
}) });
} }
const where: Prisma.CampaignWhereInput = { const where: Prisma.CampaignWhereInput = {
organizationId: input.organizationId, organizationId: input.organizationId,
...(input.search ...(input.search
? { ? {
OR: [ OR: [
{ title: { contains: input.search, mode: "insensitive" } }, { title: { contains: input.search, mode: "insensitive" } },
{ description: { contains: input.search, mode: "insensitive" } }, { description: { contains: input.search, mode: "insensitive" } },
{ subject: { contains: input.search, mode: "insensitive" } }, { subject: { contains: input.search, mode: "insensitive" } },
], ],
} }
: {}), : {}),
} };
const [total, campaigns] = await prisma.$transaction([ const [total, campaigns] = await prisma.$transaction([
prisma.campaign.count({ where }), prisma.campaign.count({ where }),
prisma.campaign.findMany({ prisma.campaign.findMany({
where, where,
orderBy: [{ createdAt: "desc" }, { id: "desc" }], orderBy: [{ createdAt: "desc" }, { id: "desc" }],
skip: (input.page - 1) * input.perPage, skip: (input.page - 1) * input.perPage,
take: input.perPage, take: input.perPage,
include: { include: {
Template: { Template: {
select: { select: {
id: true, id: true,
name: true, name: true,
}, },
}, },
CampaignLists: { CampaignLists: {
include: { include: {
List: { List: {
select: { select: {
id: true, id: true,
name: true, name: true,
}, },
}, },
}, },
}, },
_count: { _count: {
select: { select: {
Messages: true, Messages: true,
}, },
}, },
}, },
}), }),
]) ]);
const totalPages = Math.ceil(total / input.perPage) const totalPages = Math.ceil(total / input.perPage);
return { return {
campaigns, campaigns,
pagination: { pagination: {
total, total,
totalPages, totalPages,
page: input.page, page: input.page,
perPage: input.perPage, perPage: input.perPage,
hasMore: input.page < totalPages, hasMore: input.page < totalPages,
}, },
} };
}) });
export const getCampaign = authProcedure export const getCampaign = authProcedure
.input( .input(
z.object({ z.object({
id: z.string(), id: z.string(),
organizationId: z.string(), organizationId: z.string(),
}) }),
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const userOrganization = await prisma.userOrganization.findFirst({ const userOrganization = await prisma.userOrganization.findFirst({
where: { where: {
userId: ctx.user.id, userId: ctx.user.id,
organizationId: input.organizationId, organizationId: input.organizationId,
}, },
}) });
if (!userOrganization) { if (!userOrganization) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "Organization not found", message: "Organization not found",
}) });
} }
const campaign = await prisma.campaign.findFirst({ const campaign = await prisma.campaign.findFirst({
where: { where: {
id: input.id, id: input.id,
organizationId: input.organizationId, organizationId: input.organizationId,
}, },
include: { include: {
Template: true, Template: true,
CampaignLists: { CampaignLists: {
include: { include: {
List: true, List: true,
}, },
}, },
}, },
}) });
if (!campaign) { if (!campaign) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Campaign not found", message: "Campaign not found",
}) });
} }
const listSubscribers = await prisma.listSubscriber.findMany({ const listSubscribers = await prisma.listSubscriber.findMany({
where: { where: {
listId: { listId: {
in: campaign.CampaignLists.map((cl) => cl.listId), in: campaign.CampaignLists.map((cl) => cl.listId),
}, },
unsubscribedAt: null, unsubscribedAt: null,
}, },
select: { select: {
id: true, id: true,
}, },
distinct: ["subscriberId"], distinct: ["subscriberId"],
}) });
// Add the count to each list for backward compatibility // Add the count to each list for backward compatibility
const campaignWithCounts = { const campaignWithCounts = {
...campaign, ...campaign,
CampaignLists: await Promise.all( CampaignLists: await Promise.all(
campaign.CampaignLists.map(async (cl) => { campaign.CampaignLists.map(async (cl) => {
const count = await prisma.listSubscriber.count({ const count = await prisma.listSubscriber.count({
where: { where: {
listId: cl.listId, listId: cl.listId,
unsubscribedAt: null, unsubscribedAt: null,
}, },
}) });
return { return {
...cl, ...cl,
List: { List: {
...cl.List, ...cl.List,
_count: { _count: {
ListSubscribers: count, ListSubscribers: count,
}, },
}, },
} };
}) }),
), ),
// Add the unique subscriber count directly to the campaign object // Add the unique subscriber count directly to the campaign object
uniqueRecipientCount: listSubscribers.length, uniqueRecipientCount: listSubscribers.length,
} };
const promises = { const promises = {
totalMessages: prisma.message.count({ totalMessages: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
}, },
}), }),
queuedMessages: prisma.message.count({ queuedMessages: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: "QUEUED", status: "QUEUED",
}, },
}), }),
pendingMessages: prisma.message.count({ pendingMessages: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: "PENDING", status: "PENDING",
}, },
}), }),
sentMessages: prisma.message.count({ sentMessages: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: { status: {
in: ["SENT", "OPENED", "CLICKED"], in: ["SENT", "OPENED", "CLICKED"],
}, },
}, },
}), }),
failedMessages: prisma.message.count({ failedMessages: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: "FAILED", status: "FAILED",
}, },
}), }),
processed: prisma.message.count({ processed: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: { status: {
not: "QUEUED", not: "QUEUED",
}, },
}, },
}), }),
clicked: prisma.message.count({ clicked: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: "CLICKED", status: "CLICKED",
}, },
}), }),
opened: prisma.message.count({ opened: prisma.message.count({
where: { where: {
campaignId: campaign.id, campaignId: campaign.id,
status: { status: {
in: ["OPENED", "CLICKED"], in: ["OPENED", "CLICKED"],
}, },
}, },
}), }),
} };
const result = await resolveProps(promises) const result = await resolveProps(promises);
return { return {
campaign: campaignWithCounts, campaign: campaignWithCounts,
stats: { stats: {
totalMessages: result.totalMessages, totalMessages: result.totalMessages,
queuedMessages: result.queuedMessages, queuedMessages: result.queuedMessages,
pendingMessages: result.pendingMessages, pendingMessages: result.pendingMessages,
sentMessages: result.sentMessages, sentMessages: result.sentMessages,
failedMessages: result.failedMessages, failedMessages: result.failedMessages,
processed: result.processed, processed: result.processed,
clicked: result.clicked, clicked: result.clicked,
opened: result.opened, opened: result.opened,
clickRate: clickRate:
result.sentMessages > 0 result.sentMessages > 0
? (result.clicked / result.sentMessages) * 100 ? (result.clicked / result.sentMessages) * 100
: 0, : 0,
openRate: openRate:
result.sentMessages > 0 result.sentMessages > 0
? (result.opened / result.sentMessages) * 100 ? (result.opened / result.sentMessages) * 100
: 0, : 0,
}, },
} };
}) });

View File

@@ -1,23 +1,23 @@
import { router } from "../trpc" import { router } from "../trpc";
import { import {
createCampaign, createCampaign,
updateCampaign, updateCampaign,
deleteCampaign, deleteCampaign,
startCampaign, startCampaign,
cancelCampaign, cancelCampaign,
sendTestEmail, sendTestEmail,
duplicateCampaign, duplicateCampaign,
} from "./mutation" } from "./mutation";
import { getCampaign, listCampaigns } from "./query" import { getCampaign, listCampaigns } from "./query";
export const campaignRouter = router({ export const campaignRouter = router({
create: createCampaign, create: createCampaign,
update: updateCampaign, update: updateCampaign,
delete: deleteCampaign, delete: deleteCampaign,
get: getCampaign, get: getCampaign,
list: listCampaigns, list: listCampaigns,
start: startCampaign, start: startCampaign,
cancel: cancelCampaign, cancel: cancelCampaign,
sendTestEmail, sendTestEmail,
duplicate: duplicateCampaign, duplicate: duplicateCampaign,
}) });

View File

@@ -1,11 +1,11 @@
import { z } from "zod" import { z } from "zod";
export const env = z export const env = z
.object({ .object({
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"), DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
}) })
.parse(process.env) .parse(process.env);
export const ONE_PX_PNG = export const ONE_PX_PNG =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";

View File

@@ -1,57 +1,57 @@
import cron from "node-cron" import cron from "node-cron";
import { sendMessagesCron } from "./sendMessages" import { sendMessagesCron } from "./sendMessages";
import { dailyMaintenanceCron } from "./dailyMaintenance" import { dailyMaintenanceCron } from "./dailyMaintenance";
import { processQueuedCampaigns } from "./processQueuedCampaigns" import { processQueuedCampaigns } from "./processQueuedCampaigns";
type CronJob = { type CronJob = {
name: string name: string;
schedule: string schedule: string;
job: () => Promise<void> job: () => Promise<void>;
enabled: boolean enabled: boolean;
} };
const sendMessagesJob: CronJob = { const sendMessagesJob: CronJob = {
name: "send-queued-messages", name: "send-queued-messages",
schedule: "*/5 * * * * *", // Runs every 5 seconds schedule: "*/5 * * * * *", // Runs every 5 seconds
job: sendMessagesCron, job: sendMessagesCron,
enabled: true, enabled: true,
} };
const dailyMaintenanceJob: CronJob = { const dailyMaintenanceJob: CronJob = {
name: "daily-maintenance", name: "daily-maintenance",
schedule: "0 0 * * *", // Runs daily at midnight schedule: "0 0 * * *", // Runs daily at midnight
job: dailyMaintenanceCron, job: dailyMaintenanceCron,
enabled: true, enabled: true,
} };
const processQueuedCampaignsJob: CronJob = { const processQueuedCampaignsJob: CronJob = {
name: "process-queued-campaigns", name: "process-queued-campaigns",
schedule: "* * * * * *", // Runs every second schedule: "* * * * * *", // Runs every second
job: processQueuedCampaigns, job: processQueuedCampaigns,
enabled: true, enabled: true,
} };
const cronJobs: CronJob[] = [ const cronJobs: CronJob[] = [
sendMessagesJob, sendMessagesJob,
dailyMaintenanceJob, dailyMaintenanceJob,
processQueuedCampaignsJob, processQueuedCampaignsJob,
] ];
export const initializeCronJobs = () => { export const initializeCronJobs = () => {
const scheduledJobs = cronJobs const scheduledJobs = cronJobs
.filter((job) => job.enabled) .filter((job) => job.enabled)
.map((job) => { .map((job) => {
const task = cron.schedule(job.schedule, job.job) const task = cron.schedule(job.schedule, job.job);
console.log( console.log(
`Cron job '${job.name}' scheduled with cron expression: ${job.schedule}` `Cron job '${job.name}' scheduled with cron expression: ${job.schedule}`,
) );
return { name: job.name, task } return { name: job.name, task };
}) });
console.log(`${scheduledJobs.length} cron jobs initialized`) console.log(`${scheduledJobs.length} cron jobs initialized`);
return { return {
jobs: scheduledJobs, jobs: scheduledJobs,
stop: () => scheduledJobs.forEach(({ task }) => task.stop()), stop: () => scheduledJobs.forEach(({ task }) => task.stop()),
} };
} };

View File

@@ -1,22 +1,22 @@
const runningJobs = new Map<string, boolean>() const runningJobs = new Map<string, boolean>();
/** /**
* A wrapper for cron jobs * A wrapper for cron jobs
*/ */
export function cronJob(name: string, cronFn: () => Promise<void>) { export function cronJob(name: string, cronFn: () => Promise<void>) {
return async () => { return async () => {
if (runningJobs.get(name)) { if (runningJobs.get(name)) {
return return;
} }
runningJobs.set(name, true) runningJobs.set(name, true);
try { try {
await cronFn() await cronFn();
} catch (error) { } catch (error) {
console.error("Cron Error:", `[${name}]`, error) console.error("Cron Error:", `[${name}]`, error);
} finally { } finally {
runningJobs.set(name, false) runningJobs.set(name, false);
} }
} };
} }

View File

@@ -1,68 +1,68 @@
import { cronJob } from "./cron.utils" import { cronJob } from "./cron.utils";
import { prisma } from "../utils/prisma" import { prisma } from "../utils/prisma";
import dayjs from "dayjs" import dayjs from "dayjs";
export const dailyMaintenanceCron = cronJob("daily-maintenance", async () => { export const dailyMaintenanceCron = cronJob("daily-maintenance", async () => {
const organizations = await prisma.organization.findMany({ const organizations = await prisma.organization.findMany({
include: { include: {
GeneralSettings: true, GeneralSettings: true,
}, },
}) });
let totalDeletedMessages = 0 let totalDeletedMessages = 0;
for (const org of organizations) { for (const org of organizations) {
const cleanupIntervalDays = org.GeneralSettings?.cleanupInterval ?? 30 const cleanupIntervalDays = org.GeneralSettings?.cleanupInterval ?? 30;
const cleanupOlderThanDate = dayjs() const cleanupOlderThanDate = dayjs()
.subtract(cleanupIntervalDays, "days") .subtract(cleanupIntervalDays, "days")
.toDate() .toDate();
try { try {
const messagesToClean = await prisma.message.findMany({ const messagesToClean = await prisma.message.findMany({
where: { where: {
Campaign: { Campaign: {
organizationId: org.id, organizationId: org.id,
}, },
status: { status: {
in: ["SENT", "OPENED", "CLICKED", "FAILED"], in: ["SENT", "OPENED", "CLICKED", "FAILED"],
}, },
createdAt: { createdAt: {
lt: cleanupOlderThanDate, lt: cleanupOlderThanDate,
}, },
}, },
select: { select: {
id: true, id: true,
}, },
}) });
await prisma.message.updateMany({ await prisma.message.updateMany({
data: { data: {
content: null, content: null,
}, },
where: { where: {
id: { id: {
in: messagesToClean.map((msg) => msg.id), in: messagesToClean.map((msg) => msg.id),
}, },
}, },
}) });
if (messagesToClean.length > 0) { if (messagesToClean.length > 0) {
console.log( console.log(
`Daily maintenance for org ${org.id}: Deleted ${messagesToClean.length} messages older than ${cleanupIntervalDays} days.` `Daily maintenance for org ${org.id}: Deleted ${messagesToClean.length} messages older than ${cleanupIntervalDays} days.`,
) );
totalDeletedMessages += messagesToClean.length totalDeletedMessages += messagesToClean.length;
} }
} catch (error) { } catch (error) {
console.error(`Error deleting messages for org ${org.id}: ${error}`) console.error(`Error deleting messages for org ${org.id}: ${error}`);
continue continue;
} }
} }
if (totalDeletedMessages > 0) { if (totalDeletedMessages > 0) {
console.log( console.log(
`Daily maintenance job finished. Total deleted messages: ${totalDeletedMessages}.` `Daily maintenance job finished. Total deleted messages: ${totalDeletedMessages}.`,
) );
} else { } else {
console.log("Daily maintenance job finished. No messages to delete.") console.log("Daily maintenance job finished. No messages to delete.");
} }
}) });

View File

@@ -1,285 +1,285 @@
import { prisma } from "../utils/prisma" import { prisma } from "../utils/prisma";
import { LinkTracker } from "../lib/LinkTracker" import { LinkTracker } from "../lib/LinkTracker";
import { v4 as uuidV4 } from "uuid" import { v4 as uuidV4 } from "uuid";
import { import {
replacePlaceholders, replacePlaceholders,
PlaceholderDataKey, PlaceholderDataKey,
} from "../utils/placeholder-parser" } from "../utils/placeholder-parser";
import pMap from "p-map" import pMap from "p-map";
import { Subscriber, Prisma, SubscriberMetadata } from "../../prisma/client" import { Subscriber, Prisma, SubscriberMetadata } from "../../prisma/client";
import { cronJob } from "./cron.utils" import { cronJob } from "./cron.utils";
// TODO: Make this a config // TODO: Make this a config
const BATCH_SIZE = 100 const BATCH_SIZE = 100;
async function getSubscribersForCampaign( async function getSubscribersForCampaign(
campaignId: string, campaignId: string,
selectedListIds: string[] selectedListIds: string[],
): Promise<Map<string, Subscriber & { Metadata: SubscriberMetadata[] }>> { ): Promise<Map<string, Subscriber & { Metadata: SubscriberMetadata[] }>> {
if (selectedListIds.length === 0) { if (selectedListIds.length === 0) {
return new Map() return new Map();
} }
const subscribers = await prisma.subscriber.findMany({ const subscribers = await prisma.subscriber.findMany({
where: { where: {
Messages: { none: { campaignId } }, Messages: { none: { campaignId } },
ListSubscribers: { ListSubscribers: {
some: { some: {
listId: { in: selectedListIds }, listId: { in: selectedListIds },
unsubscribedAt: null, unsubscribedAt: null,
}, },
}, },
}, },
take: BATCH_SIZE, take: BATCH_SIZE,
include: { include: {
Metadata: true, Metadata: true,
}, },
}) });
if (!subscribers.length) return new Map() if (!subscribers.length) return new Map();
const subscribersMap = new Map< const subscribersMap = new Map<
string, string,
Subscriber & { Metadata: SubscriberMetadata[] } Subscriber & { Metadata: SubscriberMetadata[] }
>() >();
await pMap(subscribers, async (subscriber) => { await pMap(subscribers, async (subscriber) => {
subscribersMap.set(subscriber.id, subscriber) subscribersMap.set(subscriber.id, subscriber);
}) });
return subscribersMap return subscribersMap;
} }
const logged = { const logged = {
noQueuedCampaigns: false, noQueuedCampaigns: false,
missingCampaignData: false, missingCampaignData: false,
noSubscribers: false, noSubscribers: false,
missingCampaignContent: false, missingCampaignContent: false,
missingCampaignSubject: false, missingCampaignSubject: false,
errorProcessingCampaign: false, errorProcessingCampaign: false,
} };
const oneTimeLogger = (key: keyof typeof logged, ...messages: unknown[]) => { const oneTimeLogger = (key: keyof typeof logged, ...messages: unknown[]) => {
if (!logged[key]) { if (!logged[key]) {
console.log(...messages) console.log(...messages);
logged[key] = true logged[key] = true;
} }
} };
const turnOnLogger = (key: keyof typeof logged) => { const turnOnLogger = (key: keyof typeof logged) => {
logged[key] = false logged[key] = false;
} };
export const processQueuedCampaigns = cronJob( export const processQueuedCampaigns = cronJob(
"process-queued-campaigns", "process-queued-campaigns",
async () => { async () => {
const queuedCampaigns = await prisma.campaign.findMany({ const queuedCampaigns = await prisma.campaign.findMany({
where: { where: {
status: "CREATING", status: "CREATING",
}, },
include: { include: {
CampaignLists: { CampaignLists: {
select: { listId: true }, select: { listId: true },
}, },
Organization: { Organization: {
include: { include: {
GeneralSettings: true, GeneralSettings: true,
SmtpSettings: true, SmtpSettings: true,
}, },
}, },
Template: true, Template: true,
}, },
}) });
if (queuedCampaigns.length === 0) { if (queuedCampaigns.length === 0) {
oneTimeLogger( oneTimeLogger(
"noQueuedCampaigns", "noQueuedCampaigns",
"Cron job: No queued campaigns to process." "Cron job: No queued campaigns to process.",
) );
return return;
} }
turnOnLogger("noQueuedCampaigns") turnOnLogger("noQueuedCampaigns");
for (const campaign of queuedCampaigns) { for (const campaign of queuedCampaigns) {
try { try {
if ( if (
!campaign || !campaign ||
!campaign.content || !campaign.content ||
!campaign.subject || !campaign.subject ||
!campaign.Organization || !campaign.Organization ||
!campaign.Organization.GeneralSettings?.baseURL !campaign.Organization.GeneralSettings?.baseURL
) { ) {
oneTimeLogger( oneTimeLogger(
"missingCampaignData", "missingCampaignData",
`Cron job: Campaign ${campaign.id} is missing required data (content, subject, organization, or baseURL). Skipping.` `Cron job: Campaign ${campaign.id} is missing required data (content, subject, organization, or baseURL). Skipping.`,
) );
// Optionally, update status to FAILED or similar // Optionally, update status to FAILED or similar
// await prisma.campaign.update({ where: { id: campaign.id }, data: { status: 'FAILED', statusReason: 'Missing critical data for processing' } }); // await prisma.campaign.update({ where: { id: campaign.id }, data: { status: 'FAILED', statusReason: 'Missing critical data for processing' } });
continue continue;
} }
turnOnLogger("missingCampaignData") turnOnLogger("missingCampaignData");
const generalSettings = campaign.Organization.GeneralSettings const generalSettings = campaign.Organization.GeneralSettings;
const selectedListIds = campaign.CampaignLists.map((cl) => cl.listId) const selectedListIds = campaign.CampaignLists.map((cl) => cl.listId);
const allSubscribersMap = await getSubscribersForCampaign( const allSubscribersMap = await getSubscribersForCampaign(
campaign.id, campaign.id,
selectedListIds selectedListIds,
) );
if (allSubscribersMap.size === 0) { if (allSubscribersMap.size === 0) {
oneTimeLogger( oneTimeLogger(
"noSubscribers", "noSubscribers",
`Cron job: Campaign ${campaign.id} has no subscribers. Skipping.` `Cron job: Campaign ${campaign.id} has no subscribers. Skipping.`,
) );
continue continue;
} }
turnOnLogger("noSubscribers") turnOnLogger("noSubscribers");
const messageSubscriberIds = ( const messageSubscriberIds = (
await prisma.message.findMany({ await prisma.message.findMany({
where: { campaignId: campaign.id }, where: { campaignId: campaign.id },
select: { subscriberId: true }, select: { subscriberId: true },
}) })
).map((m) => m.subscriberId) ).map((m) => m.subscriberId);
const subscribersWithMessage = new Set(messageSubscriberIds) const subscribersWithMessage = new Set(messageSubscriberIds);
const subscribersToProcess = Array.from( const subscribersToProcess = Array.from(
allSubscribersMap.values() allSubscribersMap.values(),
).filter((sub) => !subscribersWithMessage.has(sub.id)) ).filter((sub) => !subscribersWithMessage.has(sub.id));
if (subscribersToProcess.length === 0) { if (subscribersToProcess.length === 0) {
continue continue;
} }
await prisma.$transaction( await prisma.$transaction(
async (tx) => { async (tx) => {
const linkTracker = new LinkTracker(tx) const linkTracker = new LinkTracker(tx);
const messagesToCreate: Prisma.MessageCreateManyInput[] = [] const messagesToCreate: Prisma.MessageCreateManyInput[] = [];
for (const subscriber of subscribersToProcess) { for (const subscriber of subscribersToProcess) {
const messageId = uuidV4() const messageId = uuidV4();
if (!campaign.content) { if (!campaign.content) {
oneTimeLogger( oneTimeLogger(
"missingCampaignContent", "missingCampaignContent",
`Cron job: Campaign ${campaign.id} has no content. Skipping.` `Cron job: Campaign ${campaign.id} has no content. Skipping.`,
) );
continue continue;
} }
turnOnLogger("missingCampaignContent") turnOnLogger("missingCampaignContent");
let emailContent = campaign.Template let emailContent = campaign.Template
? campaign.Template.content.replace( ? campaign.Template.content.replace(
/{{content}}/g, /{{content}}/g,
campaign.content campaign.content,
) )
: campaign.content : campaign.content;
if (!campaign.subject) { if (!campaign.subject) {
oneTimeLogger( oneTimeLogger(
"missingCampaignSubject", "missingCampaignSubject",
`Cron job: Campaign ${campaign.id} has no subject. Skipping.` `Cron job: Campaign ${campaign.id} has no subject. Skipping.`,
) );
continue continue;
} }
turnOnLogger("missingCampaignSubject") turnOnLogger("missingCampaignSubject");
const placeholderData: Partial< const placeholderData: Partial<
Record<PlaceholderDataKey, string> Record<PlaceholderDataKey, string>
> = { > = {
"subscriber.email": subscriber.email, "subscriber.email": subscriber.email,
"campaign.name": campaign.title, "campaign.name": campaign.title,
"campaign.subject": campaign.subject, "campaign.subject": campaign.subject,
"organization.name": campaign.Organization.name, "organization.name": campaign.Organization.name,
unsubscribe_link: `${generalSettings.baseURL}/unsubscribe?sid=${subscriber.id}&cid=${campaign.id}&mid=${messageId}`, unsubscribe_link: `${generalSettings.baseURL}/unsubscribe?sid=${subscriber.id}&cid=${campaign.id}&mid=${messageId}`,
current_date: new Date().toLocaleDateString("en-CA"), current_date: new Date().toLocaleDateString("en-CA"),
} };
if (campaign.openTracking) { if (campaign.openTracking) {
emailContent += `<img src="${generalSettings.baseURL}/img/${messageId}/img.png" alt="" width="1" height="1" style="display:none" />` emailContent += `<img src="${generalSettings.baseURL}/img/${messageId}/img.png" alt="" width="1" height="1" style="display:none" />`;
} }
if (subscriber.name) { if (subscriber.name) {
placeholderData["subscriber.name"] = subscriber.name placeholderData["subscriber.name"] = subscriber.name;
} }
if (subscriber.Metadata) { if (subscriber.Metadata) {
for (const meta of subscriber.Metadata) { for (const meta of subscriber.Metadata) {
placeholderData[`subscriber.metadata.${meta.key}`] = placeholderData[`subscriber.metadata.${meta.key}`] =
meta.value meta.value;
} }
} }
emailContent = replacePlaceholders(emailContent, placeholderData) emailContent = replacePlaceholders(emailContent, placeholderData);
if (!generalSettings.baseURL) { if (!generalSettings.baseURL) {
console.error( console.error(
`Cron job: Campaign ${campaign.id} has no baseURL. Skipping.` `Cron job: Campaign ${campaign.id} has no baseURL. Skipping.`,
) );
continue continue;
} }
const { content: finalContent } = const { content: finalContent } =
await linkTracker.replaceMessageContentWithTrackedLinks( await linkTracker.replaceMessageContentWithTrackedLinks(
emailContent, emailContent,
campaign.id, campaign.id,
generalSettings.baseURL generalSettings.baseURL,
) );
messagesToCreate.push({ messagesToCreate.push({
id: messageId, id: messageId,
campaignId: campaign.id, campaignId: campaign.id,
subscriberId: subscriber.id, subscriberId: subscriber.id,
content: finalContent, content: finalContent,
status: "QUEUED", status: "QUEUED",
}) });
} }
if (messagesToCreate.length > 0) { if (messagesToCreate.length > 0) {
await tx.message.createMany({ await tx.message.createMany({
data: messagesToCreate, data: messagesToCreate,
}) });
const subscribersLeft = await tx.subscriber.count({ const subscribersLeft = await tx.subscriber.count({
where: { where: {
Messages: { none: { campaignId: campaign.id } }, Messages: { none: { campaignId: campaign.id } },
ListSubscribers: { ListSubscribers: {
some: { some: {
listId: { in: selectedListIds }, listId: { in: selectedListIds },
unsubscribedAt: null, unsubscribedAt: null,
}, },
}, },
}, },
}) });
if (subscribersLeft === 0) { if (subscribersLeft === 0) {
await tx.campaign.update({ await tx.campaign.update({
where: { id: campaign.id }, where: { id: campaign.id },
data: { status: "SENDING" }, data: { status: "SENDING" },
}) });
} }
console.log( console.log(
`Cron job: Created ${messagesToCreate.length} messages for campaign ${campaign.id}.` `Cron job: Created ${messagesToCreate.length} messages for campaign ${campaign.id}.`,
) );
} }
}, },
{ timeout: 60_000 } { timeout: 60_000 },
) // End transaction ); // End transaction
turnOnLogger("errorProcessingCampaign") turnOnLogger("errorProcessingCampaign");
} catch (error) { } catch (error) {
oneTimeLogger( oneTimeLogger(
"errorProcessingCampaign", "errorProcessingCampaign",
`Cron job: Error processing campaign ${campaign.id}:`, `Cron job: Error processing campaign ${campaign.id}:`,
error error,
) );
// Optionally, mark campaign as FAILED // Optionally, mark campaign as FAILED
// await prisma.campaign.update({ where: { id: basicCampaignInfo.id }, data: { status: 'FAILED', statusReason: error.message }}); // await prisma.campaign.update({ where: { id: basicCampaignInfo.id }, data: { status: 'FAILED', statusReason: error.message }});
} }
} }
} },
) );

Some files were not shown because too many files have changed in this diff Show More