chore: format
This commit is contained in:
287
.claude.json
287
.claude.json
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"mcpServers": {},
|
||||
"isUsingBuiltInNodeForMcp": false
|
||||
}
|
||||
"mcpServers": {},
|
||||
"isUsingBuiltInNodeForMcp": false
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,6 +34,7 @@
|
||||
!.zshenv
|
||||
!.hushlogin
|
||||
!.last_pwd
|
||||
!biome.json
|
||||
|
||||
!/.github/
|
||||
!/.github/**
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Ant-Dark",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Ant-Dark",
|
||||
"ServiceTypes": [
|
||||
"Plasma/LookAndFeel"
|
||||
],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Ant/tree/master/kde/Dark"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1464285",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1464321",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1464309",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2139782",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1473492",
|
||||
"kns://icons.knsrc/api.kde-look.org/1640981",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Ant-Dark",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Ant-Dark",
|
||||
"ServiceTypes": ["Plasma/LookAndFeel"],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Ant/tree/master/kde/Dark"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1464285",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1464321",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1464309",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2139782",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1473492",
|
||||
"kns://icons.knsrc/api.kde-look.org/1640981",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic-bluish",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic-bluish",
|
||||
"ServiceTypes": [
|
||||
"Plasma/LookAndFeel"
|
||||
],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1801631",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1801641",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2144235",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1810707",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic-bluish",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic-bluish",
|
||||
"ServiceTypes": ["Plasma/LookAndFeel"],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1801631",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1801641",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2144235",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1810707",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic-darker",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic-darker",
|
||||
"ServiceTypes": [
|
||||
"Plasma/LookAndFeel"
|
||||
],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1629062",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1633673",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2146572",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1704303",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://lookandfeel.knsrc/api.kde-look.org/2145004",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic-darker",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic-darker",
|
||||
"ServiceTypes": ["Plasma/LookAndFeel"],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1629062",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1633673",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2146572",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1704303",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://lookandfeel.knsrc/api.kde-look.org/2145004",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic",
|
||||
"ServiceTypes": [
|
||||
"Plasma/LookAndFeel"
|
||||
],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1326271",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1326896",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1416702",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2144235",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105",
|
||||
"kns://lookandfeel.knsrc/api.kde-look.org/2146574"
|
||||
]
|
||||
"KPackageStructure": "Plasma/LookAndFeel",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Plasma Look And Feel",
|
||||
"EnabledByDefault": true,
|
||||
"Id": "Nordic",
|
||||
"License": "GPL 3+",
|
||||
"Name": "Nordic",
|
||||
"ServiceTypes": ["Plasma/LookAndFeel"],
|
||||
"Version": "0.1",
|
||||
"Website": "https://github.com/EliverLara/Nordic"
|
||||
},
|
||||
"X-KPackage-Dependencies": [
|
||||
"kns://colorschemes.knsrc/api.kde-look.org/1326271",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1326896",
|
||||
"kns://plasma-themes.knsrc/api.kde-look.org/1416702",
|
||||
"kns://aurorae.knsrc/api.kde-look.org/1326274",
|
||||
"kns://sddmtheme.knsrc/api.kde-look.org/2144235",
|
||||
"kns://xcursor.knsrc/api.kde-look.org/1662218",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/1683121",
|
||||
"kns://icons.knsrc/api.kde-look.org/1733012",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2144212",
|
||||
"kns://wallpaper.knsrc/api.kde-look.org/2086140",
|
||||
"kns://plasmoids.knsrc/api.kde-look.org/2196105",
|
||||
"kns://lookandfeel.knsrc/api.kde-look.org/2146574"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
function isDark(color) {
|
||||
var r = color.r;
|
||||
var g = color.g;
|
||||
var b = color.b;
|
||||
|
||||
var r = color.r;
|
||||
var g = color.g;
|
||||
var b = color.b;
|
||||
// Using the HSP value, determine whether the color is light or dark
|
||||
var colorArray = [r, g, b].map((v) => {
|
||||
if (v <= 0.03928) {
|
||||
return v / 12.92;
|
||||
}
|
||||
|
||||
// Using the HSP value, determine whether the color is light or dark
|
||||
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);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"KPackageStructure": "Plasma/Applet",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Application Launchers",
|
||||
"Description": "A modern Launcher for plasma!",
|
||||
"EnabledByDefault": true,
|
||||
"Icon": "start-here-kde",
|
||||
"Id": "AndromedaLauncher",
|
||||
"Name": "Andromeda Launcher",
|
||||
"KPackageStructure": "Plasma/Applet",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "eliverlara@gmail.com",
|
||||
"Name": "EliverLara"
|
||||
}
|
||||
],
|
||||
"Category": "Application Launchers",
|
||||
"Description": "A modern Launcher for plasma!",
|
||||
"EnabledByDefault": true,
|
||||
"Icon": "start-here-kde",
|
||||
"Id": "AndromedaLauncher",
|
||||
"Name": "Andromeda Launcher",
|
||||
|
||||
"Version": "0.6",
|
||||
"Website": "https://github.com/EliverLara/AndromedaLauncher"
|
||||
},
|
||||
"X-Plasma-Provides": [ "org.kde.plasma.launchermenu" ],
|
||||
"X-Plasma-API-Minimum-Version": "6.0"
|
||||
"Version": "0.6",
|
||||
"Website": "https://github.com/EliverLara/AndromedaLauncher"
|
||||
},
|
||||
"X-Plasma-Provides": ["org.kde.plasma.launchermenu"],
|
||||
"X-Plasma-API-Minimum-Version": "6.0"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
function updateBrightness(rootItem, source) {
|
||||
if (rootItem.updateScreenBrightnessJob)
|
||||
return;
|
||||
if (rootItem.updateScreenBrightnessJob) return;
|
||||
|
||||
if (!source.data["PowerDevil"]) {
|
||||
return;
|
||||
}
|
||||
if (!source.data["PowerDevil"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// we don't want passive brightness change send setBrightness call
|
||||
rootItem.disableBrightnessUpdate = true;
|
||||
// we don't want passive brightness change send setBrightness call
|
||||
rootItem.disableBrightnessUpdate = true;
|
||||
|
||||
if (typeof source.data["PowerDevil"]["Screen Brightness"] === 'number') {
|
||||
rootItem.screenBrightness = source.data["PowerDevil"]["Screen Brightness"];
|
||||
}
|
||||
rootItem.disableBrightnessUpdate = false;
|
||||
}
|
||||
if (typeof source.data["PowerDevil"]["Screen Brightness"] === "number") {
|
||||
rootItem.screenBrightness = source.data["PowerDevil"]["Screen Brightness"];
|
||||
}
|
||||
rootItem.disableBrightnessUpdate = false;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
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 g = color.g;
|
||||
var b = color.b;
|
||||
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 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]
|
||||
|
||||
return luminance <= 0.179
|
||||
}
|
||||
var luminance =
|
||||
0.2126 * colorArray[0] + 0.7152 * colorArray[1] + 0.0722 * colorArray[2];
|
||||
|
||||
return luminance <= 0.179;
|
||||
}
|
||||
|
||||
@@ -1,163 +1,168 @@
|
||||
function getBtDevice() {
|
||||
var connectedDevices = [];
|
||||
var connectedDevices = [];
|
||||
|
||||
var status = {
|
||||
active: false,
|
||||
message: ""
|
||||
}
|
||||
var status = {
|
||||
active: false,
|
||||
message: "",
|
||||
};
|
||||
|
||||
for (var i = 0; i < btManager.devices.length; ++i) {
|
||||
var device = btManager.devices[i];
|
||||
if (device.connected) {
|
||||
connectedDevices.push(device);
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < btManager.devices.length; ++i) {
|
||||
var device = btManager.devices[i];
|
||||
if (device.connected) {
|
||||
connectedDevices.push(device);
|
||||
}
|
||||
}
|
||||
|
||||
if (btManager.bluetoothBlocked) {
|
||||
status.active = false;
|
||||
status.message = "Disabled";
|
||||
} else if (!btManager.bluetoothOperational) {
|
||||
if (!btManager.adapters.length) {
|
||||
status.active = false;
|
||||
status.message = "Unavailable";
|
||||
} else {
|
||||
status.active = false;
|
||||
status.message = "Offline";
|
||||
}
|
||||
} else if (connectedDevices.length >= 1) {
|
||||
status.active = true;
|
||||
status.message = connectedDevices[0].name;
|
||||
} else {
|
||||
status.active = true;
|
||||
status.message = "Not Connected";
|
||||
}
|
||||
return status;
|
||||
if (btManager.bluetoothBlocked) {
|
||||
status.active = false;
|
||||
status.message = "Disabled";
|
||||
} else if (!btManager.bluetoothOperational) {
|
||||
if (!btManager.adapters.length) {
|
||||
status.active = false;
|
||||
status.message = "Unavailable";
|
||||
} else {
|
||||
status.active = false;
|
||||
status.message = "Offline";
|
||||
}
|
||||
} else if (connectedDevices.length >= 1) {
|
||||
status.active = true;
|
||||
status.message = connectedDevices[0].name;
|
||||
} else {
|
||||
status.active = true;
|
||||
status.message = "Not Connected";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function toggleBluetooth()
|
||||
{
|
||||
var enable = !btManager.bluetoothOperational;
|
||||
btManager.bluetoothBlocked = !enable;
|
||||
function toggleBluetooth() {
|
||||
var enable = !btManager.bluetoothOperational;
|
||||
btManager.bluetoothBlocked = !enable;
|
||||
|
||||
for (var i = 0; i < btManager.adapters.length; ++i) {
|
||||
var adapter = btManager.adapters[i];
|
||||
adapter.powered = enable;
|
||||
}
|
||||
for (var i = 0; i < btManager.adapters.length; ++i) {
|
||||
var adapter = btManager.adapters[i];
|
||||
adapter.powered = enable;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkInhibition() {
|
||||
var inhibited = false;
|
||||
var inhibited = false;
|
||||
|
||||
if (!NotificationManager.Server.valid) {
|
||||
return false;
|
||||
}
|
||||
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
|
||||
if (!isNaN(inhibitedUntil.getTime())) {
|
||||
inhibited |= (Date.now() < inhibitedUntil.getTime());
|
||||
}
|
||||
if (!NotificationManager.Server.valid) {
|
||||
return false;
|
||||
}
|
||||
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
|
||||
if (!isNaN(inhibitedUntil.getTime())) {
|
||||
inhibited |= Date.now() < inhibitedUntil.getTime();
|
||||
}
|
||||
|
||||
if (notificationSettings.notificationsInhibitedByApplication) {
|
||||
inhibited |= true;
|
||||
}
|
||||
if (notificationSettings.notificationsInhibitedByApplication) {
|
||||
inhibited |= true;
|
||||
}
|
||||
|
||||
if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
|
||||
inhibited |= notificationSettings.screensMirrored;
|
||||
}
|
||||
return inhibited;
|
||||
if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
|
||||
inhibited |= notificationSettings.screensMirrored;
|
||||
}
|
||||
return inhibited;
|
||||
}
|
||||
|
||||
function toggleDnd() {
|
||||
if (Funcs.checkInhibition()) {
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
if (Funcs.checkInhibition()) {
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
|
||||
// overrules current mirrored screen setup, updates again when screen configuration
|
||||
notificationSettings.screensMirrored = false;
|
||||
notificationSettings.save();
|
||||
// overrules current mirrored screen setup, updates again when screen configuration
|
||||
notificationSettings.screensMirrored = false;
|
||||
notificationSettings.save();
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var d = new Date();
|
||||
d.setYear(d.getFullYear()+1)
|
||||
var d = new Date();
|
||||
d.setYear(d.getFullYear() + 1);
|
||||
|
||||
notificationSettings.notificationsInhibitedUntil = d
|
||||
notificationSettings.save()
|
||||
notificationSettings.notificationsInhibitedUntil = d;
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
function revokeInhibitions() {
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
// overrules current mirrored screen setup, updates again when screen configuration changes
|
||||
notificationSettings.screensMirrored = false;
|
||||
notificationSettings.notificationsInhibitedUntil = undefined;
|
||||
notificationSettings.revokeApplicationInhibitions();
|
||||
// overrules current mirrored screen setup, updates again when screen configuration changes
|
||||
notificationSettings.screensMirrored = false;
|
||||
|
||||
notificationSettings.save();
|
||||
notificationSettings.save();
|
||||
}
|
||||
|
||||
function toggleRedshiftInhibition() {
|
||||
if (!monitor.available) {
|
||||
return;
|
||||
}
|
||||
switch (inhibitor.state) {
|
||||
case Redshift.Inhibitor.Inhibiting:
|
||||
case Redshift.Inhibitor.Inhibited:
|
||||
inhibitor.uninhibit();
|
||||
break;
|
||||
case Redshift.Inhibitor.Uninhibiting:
|
||||
case Redshift.Inhibitor.Uninhibited:
|
||||
inhibitor.inhibit();
|
||||
break;
|
||||
}
|
||||
if (!monitor.available) {
|
||||
return;
|
||||
}
|
||||
switch (inhibitor.state) {
|
||||
case Redshift.Inhibitor.Inhibiting:
|
||||
case Redshift.Inhibitor.Inhibited:
|
||||
inhibitor.uninhibit();
|
||||
break;
|
||||
case Redshift.Inhibitor.Uninhibiting:
|
||||
case Redshift.Inhibitor.Uninhibited:
|
||||
inhibitor.inhibit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function volumePercent(volume) {
|
||||
return volume / Vol.PulseAudio.NormalVolume * 100
|
||||
return (volume / Vol.PulseAudio.NormalVolume) * 100;
|
||||
}
|
||||
|
||||
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) {
|
||||
const oldVolume = volumeObject.volume;
|
||||
const oldPercent = volumePercent(oldVolume);
|
||||
const targetPercent = oldPercent + deltaPercent;
|
||||
const newVolume = boundVolume(Math.round(Vol.PulseAudio.NormalVolume * (targetPercent/100)));
|
||||
const newPercent = volumePercent(newVolume);
|
||||
volumeObject.muted = newPercent == 0;
|
||||
volumeObject.volume = newVolume;
|
||||
return newPercent;
|
||||
const oldVolume = volumeObject.volume;
|
||||
const oldPercent = volumePercent(oldVolume);
|
||||
const targetPercent = oldPercent + deltaPercent;
|
||||
const newVolume = boundVolume(
|
||||
Math.round(Vol.PulseAudio.NormalVolume * (targetPercent / 100)),
|
||||
);
|
||||
const newPercent = volumePercent(newVolume);
|
||||
volumeObject.muted = newPercent == 0;
|
||||
volumeObject.volume = newVolume;
|
||||
return newPercent;
|
||||
}
|
||||
function volIconName(volume, muted, prefix) {
|
||||
if (!prefix) {
|
||||
prefix = "audio-volume";
|
||||
}
|
||||
var icon = null;
|
||||
var percent = volume / Vol.PulseAudio.NormalVolume
|
||||
if (percent <= 0.0 || muted) {
|
||||
icon = prefix + "-muted";
|
||||
} else if (percent <= 0.25) {
|
||||
icon = prefix + "-low";
|
||||
} else if (percent <= 0.75) {
|
||||
icon = prefix + "-medium";
|
||||
} else {
|
||||
icon = prefix + "-high";
|
||||
}
|
||||
return icon;
|
||||
if (!prefix) {
|
||||
prefix = "audio-volume";
|
||||
}
|
||||
var icon = null;
|
||||
var percent = volume / Vol.PulseAudio.NormalVolume;
|
||||
if (percent <= 0.0 || muted) {
|
||||
icon = prefix + "-muted";
|
||||
} else if (percent <= 0.25) {
|
||||
icon = prefix + "-low";
|
||||
} else if (percent <= 0.75) {
|
||||
icon = prefix + "-medium";
|
||||
} else {
|
||||
icon = prefix + "-high";
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
function getNetworkConnectionName() {
|
||||
var status = network.networkStatus.activeConnections;
|
||||
var statusParts;
|
||||
var status = network.networkStatus.activeConnections;
|
||||
var statusParts;
|
||||
|
||||
if(isAirplane){ return "On"; }
|
||||
if (isAirplane) {
|
||||
return "On";
|
||||
}
|
||||
|
||||
if(status && status !== "Disconnected") {
|
||||
statusParts = status.split(":");
|
||||
var connectionName = statusParts[1]?.trim().split(" ").slice(2).join(" ");
|
||||
return connectionName || "Connected";
|
||||
}
|
||||
if (status && status !== "Disconnected") {
|
||||
statusParts = status.split(":");
|
||||
var connectionName = statusParts[1]?.trim().split(" ").slice(2).join(" ");
|
||||
return connectionName || "Connected";
|
||||
}
|
||||
|
||||
return "Disconnected";
|
||||
}
|
||||
return "Disconnected";
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
function listProperty(item) {
|
||||
for (var p in item)
|
||||
{
|
||||
if( typeof item[p] != "function" )
|
||||
if(p != "objectName")
|
||||
console.log(p + ":" + item[p]);
|
||||
}
|
||||
|
||||
}
|
||||
for (var p in item) {
|
||||
if (typeof item[p] != "function")
|
||||
if (p != "objectName") console.log(p + ":" + item[p]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
"Name[de]": "KDE Kontrollzentrum",
|
||||
"Name[ko]": "KDE 제어 센터",
|
||||
"Name[pt_BR]": "Estação de controle KDE",
|
||||
"ServiceTypes": [
|
||||
"Plasma/Applet"
|
||||
],
|
||||
"ServiceTypes": ["Plasma/Applet"],
|
||||
"Version": "0.1.0",
|
||||
"Website": "https://github.com/EliverLara/kde-control-station/tree/plasma6"
|
||||
},
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
{
|
||||
"name": "nvm",
|
||||
"version": "0.40.1",
|
||||
"description": "Node Version Manager - Simple bash script to manage multiple active node.js versions",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"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/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/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/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: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}'",
|
||||
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
|
||||
"predoctoc:check": "cp README.md v-README.md.orig && npm run doctoc",
|
||||
"doctoc:check": "diff -q README.md v-README.md.orig",
|
||||
"postdoctoc:check": "mv v-README.md.orig README.md",
|
||||
"eclint": "eclint check $(git ls-tree --name-only HEAD | xargs)",
|
||||
"dockerfile_lint": "dockerfile_lint",
|
||||
"markdown-link-check": "git ls-files | command grep -E '\\.md$' | xargs -n 1 markdown-link-check -p"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nvm-sh/nvm.git"
|
||||
},
|
||||
"keywords": [
|
||||
"nvm",
|
||||
"node",
|
||||
"iojs",
|
||||
"version",
|
||||
"manager"
|
||||
],
|
||||
"author": "Tim Caswell <tim@creationix.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nvm-sh/nvm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nvm-sh/nvm",
|
||||
"devDependencies": {
|
||||
"dockerfile_lint": "^0.3.4",
|
||||
"doctoc": "^2.2.1",
|
||||
"eclint": "^2.8.1",
|
||||
"markdown-link-check": "^3.12.2",
|
||||
"replace": "^1.2.2",
|
||||
"semver": "^7.6.3",
|
||||
"urchin": "^0.0.5"
|
||||
}
|
||||
"name": "nvm",
|
||||
"version": "0.40.1",
|
||||
"description": "Node Version Manager - Simple bash script to manage multiple active node.js versions",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"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/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/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/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: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}'",
|
||||
"doctoc": "doctoc --title='## Table of Contents' --github README.md",
|
||||
"predoctoc:check": "cp README.md v-README.md.orig && npm run doctoc",
|
||||
"doctoc:check": "diff -q README.md v-README.md.orig",
|
||||
"postdoctoc:check": "mv v-README.md.orig README.md",
|
||||
"eclint": "eclint check $(git ls-tree --name-only HEAD | xargs)",
|
||||
"dockerfile_lint": "dockerfile_lint",
|
||||
"markdown-link-check": "git ls-files | command grep -E '\\.md$' | xargs -n 1 markdown-link-check -p"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nvm-sh/nvm.git"
|
||||
},
|
||||
"keywords": [
|
||||
"nvm",
|
||||
"node",
|
||||
"iojs",
|
||||
"version",
|
||||
"manager"
|
||||
],
|
||||
"author": "Tim Caswell <tim@creationix.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nvm-sh/nvm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/nvm-sh/nvm",
|
||||
"devDependencies": {
|
||||
"dockerfile_lint": "^0.3.4",
|
||||
"doctoc": "^2.2.1",
|
||||
"eclint": "^2.8.1",
|
||||
"markdown-link-check": "^3.12.2",
|
||||
"replace": "^1.2.2",
|
||||
"semver": "^7.6.3",
|
||||
"urchin": "^0.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"openFiles": [
|
||||
"README.md"
|
||||
]
|
||||
"openFiles": ["README.md"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(|install.sh)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "text/plain"
|
||||
},
|
||||
{
|
||||
"key": "Content-Disposition",
|
||||
"value": "inline; filename=\"install.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/",
|
||||
"destination": "/install.sh"
|
||||
}
|
||||
]
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(|install.sh)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "text/plain"
|
||||
},
|
||||
{
|
||||
"key": "Content-Disposition",
|
||||
"value": "inline; filename=\"install.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/",
|
||||
"destination": "/install.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
94
.vscode/settings.json
vendored
94
.vscode/settings.json
vendored
@@ -1,49 +1,49 @@
|
||||
{
|
||||
"explorer.excludeGitIgnore": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[ignore]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "ms-azuretools.vscode-containers"
|
||||
},
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
}
|
||||
"explorer.excludeGitIgnore": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[ignore]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[shellscript]": {
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "trunk.io"
|
||||
},
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "ms-azuretools.vscode-containers"
|
||||
},
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "Vue.volar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@electron/asar": "^3.2.1"
|
||||
}
|
||||
"devDependencies": {
|
||||
"@electron/asar": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Custom animations */
|
||||
--animate-pulse: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
|
||||
/* Custom keyframes for pulse */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
/* Custom animations */
|
||||
--animate-pulse: pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
|
||||
/* Custom keyframes for pulse */
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -30,18 +31,18 @@
|
||||
|
||||
/* Custom scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgb(17 24 39);
|
||||
background: rgb(17 24 39);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(139 92 246);
|
||||
border-radius: 5px;
|
||||
background: rgb(139 92 246);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgb(167 139 250);
|
||||
background: rgb(167 139 250);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,65 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Pivoine Docs - Documentation Hub',
|
||||
description: 'Comprehensive documentation hub for all Pivoine projects by Valknar. Explore technical guides, API references, and tutorials.',
|
||||
keywords: ['documentation', 'pivoine', 'valknar', 'developer', 'guides', 'api'],
|
||||
authors: [{ name: 'Valknar', url: 'https://pivoine.art' }],
|
||||
creator: 'Valknar',
|
||||
manifest: '/manifest.json',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' },
|
||||
],
|
||||
apple: [
|
||||
{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
|
||||
],
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: 'black-translucent',
|
||||
title: 'Pivoine Docs',
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://docs.pivoine.art',
|
||||
title: 'Pivoine Docs - Documentation Hub',
|
||||
description: 'Comprehensive documentation hub for all Pivoine projects',
|
||||
siteName: 'Pivoine Docs',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Pivoine Docs - Documentation Hub',
|
||||
description: 'Comprehensive documentation hub for all Pivoine projects',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
},
|
||||
}
|
||||
title: "Pivoine Docs - Documentation Hub",
|
||||
description:
|
||||
"Comprehensive documentation hub for all Pivoine projects by Valknar. Explore technical guides, API references, and tutorials.",
|
||||
keywords: [
|
||||
"documentation",
|
||||
"pivoine",
|
||||
"valknar",
|
||||
"developer",
|
||||
"guides",
|
||||
"api",
|
||||
],
|
||||
authors: [{ name: "Valknar", url: "https://pivoine.art" }],
|
||||
creator: "Valknar",
|
||||
manifest: "/manifest.json",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: "/favicon.svg", type: "image/svg+xml" },
|
||||
{ url: "/icon.svg", type: "image/svg+xml", sizes: "any" },
|
||||
],
|
||||
apple: [
|
||||
{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
|
||||
],
|
||||
},
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "black-translucent",
|
||||
title: "Pivoine Docs",
|
||||
},
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: "https://docs.pivoine.art",
|
||||
title: "Pivoine Docs - Documentation Hub",
|
||||
description: "Comprehensive documentation hub for all Pivoine projects",
|
||||
siteName: "Pivoine Docs",
|
||||
},
|
||||
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({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="scroll-smooth">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
)
|
||||
return (
|
||||
<html lang="en" className="scroll-smooth">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,204 +1,241 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { BookOpen, Code2, Globe, ChevronRight, Sparkles, Terminal } from 'lucide-react'
|
||||
import KomposeIcon from '@/components/icons/KomposeIcon'
|
||||
import { PivoineDocsIcon } from '@/components/icons'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
BookOpen,
|
||||
Code2,
|
||||
Globe,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import KomposeIcon from "@/components/icons/KomposeIcon";
|
||||
import { PivoineDocsIcon } from "@/components/icons";
|
||||
|
||||
export default function DocsHub() {
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 })
|
||||
const [isHovering, setIsHovering] = useState<string | null>(null)
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
const [isHovering, setIsHovering] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setMousePosition({
|
||||
x: (e.clientX / window.innerWidth) * 20 - 10,
|
||||
y: (e.clientY / window.innerHeight) * 20 - 10,
|
||||
})
|
||||
}
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setMousePosition({
|
||||
x: (e.clientX / window.innerWidth) * 20 - 10,
|
||||
y: (e.clientY / window.innerHeight) * 20 - 10,
|
||||
});
|
||||
};
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
return () => window.removeEventListener("mousemove", handleMouseMove);
|
||||
}, []);
|
||||
|
||||
const projects = [
|
||||
{
|
||||
name: 'Kompose',
|
||||
status: 'Active',
|
||||
description: 'Comprehensive documentation for Kompose project',
|
||||
url: '/kompose',
|
||||
gradient: 'from-violet-500 to-purple-600'
|
||||
}
|
||||
]
|
||||
const projects = [
|
||||
{
|
||||
name: "Kompose",
|
||||
status: "Active",
|
||||
description: "Comprehensive documentation for Kompose project",
|
||||
url: "/kompose",
|
||||
gradient: "from-violet-500 to-purple-600",
|
||||
},
|
||||
];
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: "Valknar's Blog",
|
||||
icon: Globe,
|
||||
url: 'http://pivoine.art',
|
||||
gradient: 'from-pink-500 to-rose-600'
|
||||
},
|
||||
{
|
||||
title: 'Source Code',
|
||||
icon: Code2,
|
||||
url: 'https://code.pivoine.art',
|
||||
gradient: 'from-cyan-500 to-blue-600'
|
||||
}
|
||||
]
|
||||
const links = [
|
||||
{
|
||||
title: "Valknar's Blog",
|
||||
icon: Globe,
|
||||
url: "http://pivoine.art",
|
||||
gradient: "from-pink-500 to-rose-600",
|
||||
},
|
||||
{
|
||||
title: "Source Code",
|
||||
icon: Code2,
|
||||
url: "https://code.pivoine.art",
|
||||
gradient: "from-cyan-500 to-blue-600",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="absolute w-96 h-96 bg-purple-500/20 rounded-full blur-3xl top-0 -left-48 animate-pulse"
|
||||
style={{
|
||||
transform: `translate(${mousePosition.x}px, ${mousePosition.y}px)`,
|
||||
transition: 'transform 0.3s ease-out'
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute w-96 h-96 bg-pink-500/20 rounded-full blur-3xl bottom-0 -right-48 animate-pulse"
|
||||
style={{
|
||||
transform: `translate(${-mousePosition.x}px, ${-mousePosition.y}px)`,
|
||||
transition: 'transform 0.3s ease-out',
|
||||
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>
|
||||
return (
|
||||
<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 */}
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="absolute w-96 h-96 bg-purple-500/20 rounded-full blur-3xl top-0 -left-48 animate-pulse"
|
||||
style={{
|
||||
transform: `translate(${mousePosition.x}px, ${mousePosition.y}px)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute w-96 h-96 bg-pink-500/20 rounded-full blur-3xl bottom-0 -right-48 animate-pulse"
|
||||
style={{
|
||||
transform: `translate(${-mousePosition.x}px, ${-mousePosition.y}px)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
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>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 container mx-auto px-6 py-12 max-w-6xl">
|
||||
{/* Header */}
|
||||
<header className="text-center mb-20 pt-12">
|
||||
{/* Hero Icon */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<PivoineDocsIcon size="200px" showLabel={false} interactive={true} />
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Sparkles className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm text-purple-300">Documentation Hub</span>
|
||||
</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>
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 container mx-auto px-6 py-12 max-w-6xl">
|
||||
{/* Header */}
|
||||
<header className="text-center mb-20 pt-12">
|
||||
{/* Hero Icon */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<PivoineDocsIcon
|
||||
size="200px"
|
||||
showLabel={false}
|
||||
interactive={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<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>
|
||||
<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">
|
||||
<Sparkles className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-sm text-purple-300">Documentation Hub</span>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
<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 */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,302 +1,407 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import PivoineDocsIcon from './PivoineDocsIcon'
|
||||
import PivoineDocsIcon from "./PivoineDocsIcon";
|
||||
|
||||
export default function PivoineIconDemo() {
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)',
|
||||
padding: '4rem 2rem',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<div style={{
|
||||
maxWidth: '1400px',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '4rem' }}>
|
||||
<h1 style={{
|
||||
fontSize: '3rem',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #ec4899, #a855f7, #c084fc)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
marginBottom: '1rem'
|
||||
}}>
|
||||
Pivoine Docs Icon
|
||||
</h1>
|
||||
<p style={{
|
||||
fontSize: '1.25rem',
|
||||
color: '#94a3b8',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
A beautiful animated peony blossom icon with interactive states
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: "100vh",
|
||||
background: "linear-gradient(135deg, #1e293b 0%, #0f172a 100%)",
|
||||
padding: "4rem 2rem",
|
||||
color: "#fff",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "1400px",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: "center", marginBottom: "4rem" }}>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "3rem",
|
||||
fontWeight: "bold",
|
||||
background: "linear-gradient(135deg, #ec4899, #a855f7, #c084fc)",
|
||||
backgroundClip: "text",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
marginBottom: "1rem",
|
||||
}}
|
||||
>
|
||||
Pivoine Docs Icon
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "1.25rem",
|
||||
color: "#94a3b8",
|
||||
maxWidth: "600px",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
A beautiful animated peony blossom icon with interactive states
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Main Showcase */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '3rem',
|
||||
marginBottom: '4rem'
|
||||
}}>
|
||||
{/* Large Interactive */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<h3 style={{ marginBottom: '1.5rem', color: '#f472b6' }}>
|
||||
Interactive (Hover & Click)
|
||||
</h3>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '320px'
|
||||
}}>
|
||||
<PivoineDocsIcon size="280px" />
|
||||
</div>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}>
|
||||
Hover to bloom • Click to close
|
||||
</p>
|
||||
</div>
|
||||
{/* Main Showcase */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
||||
gap: "3rem",
|
||||
marginBottom: "4rem",
|
||||
}}
|
||||
>
|
||||
{/* Large Interactive */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "2rem",
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: "1.5rem", color: "#f472b6" }}>
|
||||
Interactive (Hover & Click)
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "320px",
|
||||
}}
|
||||
>
|
||||
<PivoineDocsIcon size="280px" />
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
fontSize: "0.875rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
Hover to bloom • Click to close
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* With Label */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<h3 style={{ marginBottom: '1.5rem', color: '#c084fc' }}>
|
||||
With Label
|
||||
</h3>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '320px'
|
||||
}}>
|
||||
<PivoineDocsIcon size="240px" showLabel />
|
||||
</div>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}>
|
||||
Perfect for hero sections
|
||||
</p>
|
||||
</div>
|
||||
{/* With Label */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "2rem",
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: "1.5rem", color: "#c084fc" }}>
|
||||
With Label
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "320px",
|
||||
}}
|
||||
>
|
||||
<PivoineDocsIcon size="240px" showLabel />
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
fontSize: "0.875rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
Perfect for hero sections
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Non-Interactive */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<h3 style={{ marginBottom: '1.5rem', color: '#fb7185' }}>
|
||||
Static (Non-Interactive)
|
||||
</h3>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '320px'
|
||||
}}>
|
||||
<PivoineDocsIcon size="240px" interactive={false} />
|
||||
</div>
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.875rem', marginTop: '1rem' }}>
|
||||
Ideal for favicons & PWA icons
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Non-Interactive */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "2rem",
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ marginBottom: "1.5rem", color: "#fb7185" }}>
|
||||
Static (Non-Interactive)
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
minHeight: "320px",
|
||||
}}
|
||||
>
|
||||
<PivoineDocsIcon size="240px" interactive={false} />
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
fontSize: "0.875rem",
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
>
|
||||
Ideal for favicons & PWA icons
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Size Variations */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '3rem',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
marginBottom: '4rem'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2rem',
|
||||
textAlign: 'center',
|
||||
color: '#f0abfc'
|
||||
}}>
|
||||
Size Variations
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'flex-end',
|
||||
flexWrap: 'wrap',
|
||||
gap: '2rem',
|
||||
padding: '2rem'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<PivoineDocsIcon size="64px" />
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}>
|
||||
64px<br />Favicon
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<PivoineDocsIcon size="96px" />
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.75rem', marginTop: '0.5rem' }}>
|
||||
96px<br />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>
|
||||
{/* Size Variations */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "3rem",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
marginBottom: "4rem",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "2rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "2rem",
|
||||
textAlign: "center",
|
||||
color: "#f0abfc",
|
||||
}}
|
||||
>
|
||||
Size Variations
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
alignItems: "flex-end",
|
||||
flexWrap: "wrap",
|
||||
gap: "2rem",
|
||||
padding: "2rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<PivoineDocsIcon size="64px" />
|
||||
<p
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
fontSize: "0.75rem",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
64px
|
||||
<br />
|
||||
Favicon
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<PivoineDocsIcon size="96px" />
|
||||
<p
|
||||
style={{
|
||||
color: "#94a3b8",
|
||||
fontSize: "0.75rem",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
96px
|
||||
<br />
|
||||
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 */}
|
||||
<div style={{
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '3rem',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2rem',
|
||||
textAlign: 'center',
|
||||
color: '#f0abfc'
|
||||
}}>
|
||||
Features
|
||||
</h2>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
|
||||
gap: '2rem'
|
||||
}}>
|
||||
{[
|
||||
{
|
||||
icon: '🌸',
|
||||
title: 'Realistic Design',
|
||||
description: 'Multi-layered peony with natural gradients'
|
||||
},
|
||||
{
|
||||
icon: '✨',
|
||||
title: 'Smooth Animations',
|
||||
description: 'Gentle breathing in normal state'
|
||||
},
|
||||
{
|
||||
icon: '🎭',
|
||||
title: 'Interactive States',
|
||||
description: 'Bloom on hover, close on click'
|
||||
},
|
||||
{
|
||||
icon: '💫',
|
||||
title: 'Particle Effects',
|
||||
description: '12 bloom particles flying around'
|
||||
},
|
||||
{
|
||||
icon: '🎨',
|
||||
title: 'Beautiful Colors',
|
||||
description: 'Pink to purple gradient palette'
|
||||
},
|
||||
{
|
||||
icon: '♿',
|
||||
title: 'Accessible',
|
||||
description: 'Reduced motion & touch support'
|
||||
},
|
||||
{
|
||||
icon: '📱',
|
||||
title: 'Responsive',
|
||||
description: 'Works perfectly on all devices'
|
||||
},
|
||||
{
|
||||
icon: '⚡',
|
||||
title: 'High Performance',
|
||||
description: 'GPU-accelerated CSS animations'
|
||||
}
|
||||
].map((feature, i) => (
|
||||
<div key={i} style={{
|
||||
padding: '1.5rem',
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: '0.75rem',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)'
|
||||
}}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: '0.75rem' }}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h4 style={{
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: '600',
|
||||
marginBottom: '0.5rem',
|
||||
color: '#fda4af'
|
||||
}}>
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p style={{
|
||||
fontSize: '0.875rem',
|
||||
color: '#94a3b8'
|
||||
}}>
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Feature List */}
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "3rem",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "2rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "2rem",
|
||||
textAlign: "center",
|
||||
color: "#f0abfc",
|
||||
}}
|
||||
>
|
||||
Features
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(250px, 1fr))",
|
||||
gap: "2rem",
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{
|
||||
icon: "🌸",
|
||||
title: "Realistic Design",
|
||||
description: "Multi-layered peony with natural gradients",
|
||||
},
|
||||
{
|
||||
icon: "✨",
|
||||
title: "Smooth Animations",
|
||||
description: "Gentle breathing in normal state",
|
||||
},
|
||||
{
|
||||
icon: "🎭",
|
||||
title: "Interactive States",
|
||||
description: "Bloom on hover, close on click",
|
||||
},
|
||||
{
|
||||
icon: "💫",
|
||||
title: "Particle Effects",
|
||||
description: "12 bloom particles flying around",
|
||||
},
|
||||
{
|
||||
icon: "🎨",
|
||||
title: "Beautiful Colors",
|
||||
description: "Pink to purple gradient palette",
|
||||
},
|
||||
{
|
||||
icon: "♿",
|
||||
title: "Accessible",
|
||||
description: "Reduced motion & touch support",
|
||||
},
|
||||
{
|
||||
icon: "📱",
|
||||
title: "Responsive",
|
||||
description: "Works perfectly on all devices",
|
||||
},
|
||||
{
|
||||
icon: "⚡",
|
||||
title: "High Performance",
|
||||
description: "GPU-accelerated CSS animations",
|
||||
},
|
||||
].map((feature, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
background: "rgba(255, 255, 255, 0.03)",
|
||||
borderRadius: "0.75rem",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: "2rem", marginBottom: "0.75rem" }}>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h4
|
||||
style={{
|
||||
fontSize: "1.125rem",
|
||||
fontWeight: "600",
|
||||
marginBottom: "0.5rem",
|
||||
color: "#fda4af",
|
||||
}}
|
||||
>
|
||||
{feature.title}
|
||||
</h4>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.875rem",
|
||||
color: "#94a3b8",
|
||||
}}
|
||||
>
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Example */}
|
||||
<div style={{
|
||||
marginTop: '4rem',
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)'
|
||||
}}>
|
||||
<h2 style={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '1rem',
|
||||
color: '#f0abfc'
|
||||
}}>
|
||||
Quick Start
|
||||
</h2>
|
||||
<pre style={{
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
padding: '1.5rem',
|
||||
borderRadius: '0.5rem',
|
||||
overflow: 'auto',
|
||||
fontSize: '0.875rem',
|
||||
color: '#e2e8f0'
|
||||
}}>
|
||||
{`import PivoineDocsIcon from '@/components/icons/PivoineDocsIcon'
|
||||
{/* Usage Example */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "4rem",
|
||||
background: "rgba(255, 255, 255, 0.05)",
|
||||
borderRadius: "1rem",
|
||||
padding: "2rem",
|
||||
backdropFilter: "blur(10px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{
|
||||
fontSize: "1.5rem",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "1rem",
|
||||
color: "#f0abfc",
|
||||
}}
|
||||
>
|
||||
Quick Start
|
||||
</h2>
|
||||
<pre
|
||||
style={{
|
||||
background: "rgba(0, 0, 0, 0.3)",
|
||||
padding: "1.5rem",
|
||||
borderRadius: "0.5rem",
|
||||
overflow: "auto",
|
||||
fontSize: "0.875rem",
|
||||
color: "#e2e8f0",
|
||||
}}
|
||||
>
|
||||
{`import PivoineDocsIcon from '@/components/icons/PivoineDocsIcon'
|
||||
|
||||
// Basic usage
|
||||
<PivoineDocsIcon size="256px" />
|
||||
@@ -306,19 +411,21 @@ export default function PivoineIconDemo() {
|
||||
|
||||
// Static for favicon
|
||||
<PivoineDocsIcon size="128px" interactive={false} />`}
|
||||
</pre>
|
||||
</div>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
marginTop: '4rem',
|
||||
textAlign: 'center',
|
||||
color: '#64748b',
|
||||
fontSize: '0.875rem'
|
||||
}}>
|
||||
<p>Made with 🌸 for beautiful documentation experiences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "4rem",
|
||||
textAlign: "center",
|
||||
color: "#64748b",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
<p>Made with 🌸 for beautiful documentation experiences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,333 +1,334 @@
|
||||
/* Kompose Icon Styles */
|
||||
|
||||
.kompose-icon-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.kompose-icon-wrapper:not(.is-interactive) {
|
||||
cursor: default;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.kompose-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
filter: drop-shadow(0 4px 20px rgba(0, 220, 130, 0.2));
|
||||
transition: filter 0.4s ease;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
filter: drop-shadow(0 4px 20px rgba(0, 220, 130, 0.2));
|
||||
transition: filter 0.4s ease;
|
||||
}
|
||||
|
||||
/* Hover Effects */
|
||||
.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 {
|
||||
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
|
||||
animation: subtle-pulse 2s ease-in-out infinite;
|
||||
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
|
||||
animation: subtle-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
animation: pulse-expand 1s ease-in-out infinite;
|
||||
animation: pulse-expand 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.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 {
|
||||
opacity: 1 !important;
|
||||
animation: corner-extend 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
opacity: 1 !important;
|
||||
animation: corner-extend 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* Click/Active Effects */
|
||||
.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 {
|
||||
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));
|
||||
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));
|
||||
}
|
||||
|
||||
.kompose-icon-wrapper.is-clicked .k-letter {
|
||||
animation: letter-flash 0.6s ease-out;
|
||||
filter: url(#intenseglow192);
|
||||
animation: letter-flash 0.6s ease-out;
|
||||
filter: url(#intenseglow192);
|
||||
}
|
||||
|
||||
.kompose-icon-wrapper.is-clicked .status-dot {
|
||||
animation: dot-burst 0.6s ease-out;
|
||||
animation: dot-burst 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Ripple Effect */
|
||||
.ripple {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 220, 130, 0.6) 0%,
|
||||
rgba(0, 220, 130, 0) 70%
|
||||
);
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
animation: ripple-expand 0.8s ease-out;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 220, 130, 0.6) 0%,
|
||||
rgba(0, 220, 130, 0) 70%
|
||||
);
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
animation: ripple-expand 0.8s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Default animations for status dot */
|
||||
.status-dot {
|
||||
animation: default-pulse 2s ease-in-out infinite;
|
||||
animation: default-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-ring {
|
||||
animation: default-ring-pulse 2s ease-in-out infinite;
|
||||
animation: default-ring-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Keyframe Animations */
|
||||
@keyframes subtle-pulse {
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 8px 35px rgba(0, 220, 130, 0.6));
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
filter: drop-shadow(0 8px 30px rgba(0, 220, 130, 0.4));
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 8px 35px rgba(0, 220, 130, 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bg-glow {
|
||||
0%,
|
||||
100% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes letter-glow {
|
||||
0%,
|
||||
100% {
|
||||
filter: url(#glow192);
|
||||
}
|
||||
50% {
|
||||
filter: url(#intenseglow192);
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
filter: url(#glow192);
|
||||
}
|
||||
50% {
|
||||
filter: url(#intenseglow192);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line-slide-vertical {
|
||||
0% {
|
||||
stroke-dasharray: 96;
|
||||
stroke-dashoffset: 96;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 96;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
0% {
|
||||
stroke-dasharray: 96;
|
||||
stroke-dashoffset: 96;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 96;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line-slide-diagonal-top {
|
||||
0% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 68;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
0% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 68;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line-slide-diagonal-bottom {
|
||||
0% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 68;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
0% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 68;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 68;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-expand {
|
||||
0%,
|
||||
100% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
r: 14;
|
||||
opacity: 1;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
r: 14;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ring-pulse {
|
||||
0%,
|
||||
100% {
|
||||
r: 17.28;
|
||||
opacity: 0.3;
|
||||
stroke-width: 3;
|
||||
}
|
||||
50% {
|
||||
r: 20;
|
||||
opacity: 0.6;
|
||||
stroke-width: 2;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
r: 17.28;
|
||||
opacity: 0.3;
|
||||
stroke-width: 3;
|
||||
}
|
||||
50% {
|
||||
r: 20;
|
||||
opacity: 0.6;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes corner-extend {
|
||||
0% {
|
||||
stroke-dasharray: 13.44;
|
||||
stroke-dashoffset: 13.44;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 13.44;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
0% {
|
||||
stroke-dasharray: 13.44;
|
||||
stroke-dashoffset: 13.44;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 13.44;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes click-bounce {
|
||||
0% {
|
||||
transform: scale(1) translateY(0) rotateY(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: scale(0.92) translateY(0) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08) translateY(-4px) rotateY(180deg);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.98) translateY(0) rotateY(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) translateY(0) rotateY(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: scale(1) translateY(0) rotateY(0deg);
|
||||
}
|
||||
30% {
|
||||
transform: scale(0.92) translateY(0) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08) translateY(-4px) rotateY(180deg);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.98) translateY(0) rotateY(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) translateY(0) rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-3d {
|
||||
0% {
|
||||
transform: perspective(800px) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: perspective(800px) rotateY(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: perspective(800px) rotateY(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: perspective(800px) rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: perspective(800px) rotateY(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: perspective(800px) rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes letter-flash {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-burst {
|
||||
0% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
r: 20;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
0% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
r: 20;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
r: 11.52;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple-expand {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2.5);
|
||||
opacity: 0;
|
||||
}
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes default-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
r: 11.52;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
r: 13.44;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
r: 11.52;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
r: 13.44;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes default-ring-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.kompose-icon-wrapper.is-interactive:hover {
|
||||
transform: scale(1.03) translateY(-1px);
|
||||
}
|
||||
.kompose-icon-wrapper.is-interactive:hover {
|
||||
transform: scale(1.03) translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kompose-icon-wrapper,
|
||||
.kompose-icon,
|
||||
.kompose-icon *,
|
||||
.ripple {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.kompose-icon-wrapper,
|
||||
.kompose-icon,
|
||||
.kompose-icon *,
|
||||
.ripple {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.kompose-icon-wrapper.is-interactive:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.kompose-icon-wrapper.is-interactive:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch device optimizations */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.kompose-icon-wrapper.is-interactive:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.kompose-icon-wrapper.is-interactive:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +1,254 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import './KomposeIcon.css'
|
||||
import React, { useState } from "react";
|
||||
import "./KomposeIcon.css";
|
||||
|
||||
interface KomposeIconProps {
|
||||
size?: string
|
||||
interactive?: boolean
|
||||
className?: string
|
||||
size?: string;
|
||||
interactive?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function KomposeIcon({
|
||||
size = '192px',
|
||||
interactive = true,
|
||||
className = ''
|
||||
export default function KomposeIcon({
|
||||
size = "192px",
|
||||
interactive = true,
|
||||
className = "",
|
||||
}: KomposeIconProps) {
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
const [showRipple, setShowRipple] = useState(false)
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
const [showRipple, setShowRipple] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!interactive) return
|
||||
const handleClick = () => {
|
||||
if (!interactive) return;
|
||||
|
||||
setIsClicked(true)
|
||||
setShowRipple(true)
|
||||
setIsClicked(true);
|
||||
setShowRipple(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsClicked(false)
|
||||
}, 600)
|
||||
setTimeout(() => {
|
||||
setIsClicked(false);
|
||||
}, 600);
|
||||
|
||||
setTimeout(() => {
|
||||
setShowRipple(false)
|
||||
}, 800)
|
||||
}
|
||||
setTimeout(() => {
|
||||
setShowRipple(false);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleTouch = (e: React.TouchEvent) => {
|
||||
if (!interactive) return
|
||||
handleClick()
|
||||
}
|
||||
const handleTouch = (e: React.TouchEvent) => {
|
||||
if (!interactive) return;
|
||||
handleClick();
|
||||
};
|
||||
|
||||
const wrapperClasses = [
|
||||
'kompose-icon-wrapper',
|
||||
isClicked && 'is-clicked',
|
||||
interactive && 'is-interactive',
|
||||
className
|
||||
].filter(Boolean).join(' ')
|
||||
const wrapperClasses = [
|
||||
"kompose-icon-wrapper",
|
||||
isClicked && "is-clicked",
|
||||
interactive && "is-interactive",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={wrapperClasses}
|
||||
onClick={handleClick}
|
||||
onTouchStart={handleTouch}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg
|
||||
className="kompose-icon"
|
||||
viewBox="0 0 192 192"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="carbon192" x="0" 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>
|
||||
return (
|
||||
<div
|
||||
className={wrapperClasses}
|
||||
onClick={handleClick}
|
||||
onTouchStart={handleTouch}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg
|
||||
className="kompose-icon"
|
||||
viewBox="0 0 192 192"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="carbon192"
|
||||
x="0"
|
||||
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%">
|
||||
<stop offset="0%" style={{ stopColor: '#1a1d2e', stopOpacity: 1 }}></stop>
|
||||
<stop offset="100%" style={{ stopColor: '#0a0e27', stopOpacity: 1 }}></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="bgGrad192" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop
|
||||
offset="0%"
|
||||
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%">
|
||||
<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>
|
||||
<linearGradient
|
||||
id="primaryGrad192"
|
||||
x1="0%"
|
||||
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">
|
||||
<feGaussianBlur stdDeviation="6" result="coloredBlur"></feGaussianBlur>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow192">
|
||||
<feGaussianBlur
|
||||
stdDeviation="6"
|
||||
result="coloredBlur"
|
||||
></feGaussianBlur>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="intenseglow192">
|
||||
<feGaussianBlur stdDeviation="12" result="coloredBlur"></feGaussianBlur>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<filter id="intenseglow192">
|
||||
<feGaussianBlur
|
||||
stdDeviation="12"
|
||||
result="coloredBlur"
|
||||
></feGaussianBlur>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"></feMergeNode>
|
||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Background */}
|
||||
<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>
|
||||
{/* Background */}
|
||||
<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 */}
|
||||
<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 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>
|
||||
{/* Stylized K */}
|
||||
<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
|
||||
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 */}
|
||||
<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>
|
||||
{/* Animated status dot */}
|
||||
<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 */}
|
||||
<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 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>
|
||||
{/* 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
|
||||
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 */}
|
||||
{showRipple && <div className="ripple"></div>}
|
||||
</div>
|
||||
)
|
||||
{/* Ripple effect container */}
|
||||
{showRipple && <div className="ripple"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,338 +1,494 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import './PivoineDocsIcon.css'
|
||||
import React, { useState } from "react";
|
||||
import "./PivoineDocsIcon.css";
|
||||
|
||||
interface PivoineDocsIconProps {
|
||||
size?: string
|
||||
interactive?: boolean
|
||||
className?: string
|
||||
showLabel?: boolean
|
||||
size?: string;
|
||||
interactive?: boolean;
|
||||
className?: string;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export default function PivoineDocsIcon({
|
||||
size = '256px',
|
||||
interactive = true,
|
||||
className = '',
|
||||
showLabel = false
|
||||
export default function PivoineDocsIcon({
|
||||
size = "256px",
|
||||
interactive = true,
|
||||
className = "",
|
||||
showLabel = false,
|
||||
}: PivoineDocsIconProps) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!interactive) return
|
||||
setIsHovered(true)
|
||||
}
|
||||
const handleMouseEnter = () => {
|
||||
if (!interactive) return;
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!interactive) return
|
||||
setIsHovered(false)
|
||||
}
|
||||
const handleMouseLeave = () => {
|
||||
if (!interactive) return;
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (!interactive) return
|
||||
const handleClick = () => {
|
||||
if (!interactive) return;
|
||||
|
||||
setIsClicked(true)
|
||||
setTimeout(() => {
|
||||
setIsClicked(false)
|
||||
}, 1200)
|
||||
}
|
||||
setIsClicked(true);
|
||||
setTimeout(() => {
|
||||
setIsClicked(false);
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handleTouch = (e: React.TouchEvent) => {
|
||||
if (!interactive) return
|
||||
e.preventDefault()
|
||||
setIsHovered(true)
|
||||
|
||||
setTimeout(() => {
|
||||
handleClick()
|
||||
}, 50)
|
||||
const handleTouch = (e: React.TouchEvent) => {
|
||||
if (!interactive) return;
|
||||
e.preventDefault();
|
||||
setIsHovered(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsHovered(false)
|
||||
}, 1500)
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleClick();
|
||||
}, 50);
|
||||
|
||||
const wrapperClasses = [
|
||||
'pivoine-docs-icon-wrapper',
|
||||
isHovered && 'is-hovered',
|
||||
isClicked && 'is-clicked',
|
||||
interactive && 'is-interactive',
|
||||
className
|
||||
].filter(Boolean).join(' ')
|
||||
setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Generate bloom particles with varied properties
|
||||
const bloomParticles = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
angle: (360 / 12) * i,
|
||||
distance: 80 + Math.random() * 20,
|
||||
size: 2 + Math.random() * 2,
|
||||
delay: i * 0.08,
|
||||
}))
|
||||
const wrapperClasses = [
|
||||
"pivoine-docs-icon-wrapper",
|
||||
isHovered && "is-hovered",
|
||||
isClicked && "is-clicked",
|
||||
interactive && "is-interactive",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={wrapperClasses}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
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>
|
||||
// Generate bloom particles with varied properties
|
||||
const bloomParticles = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
angle: (360 / 12) * i,
|
||||
distance: 80 + Math.random() * 20,
|
||||
size: 2 + Math.random() * 2,
|
||||
delay: i * 0.08,
|
||||
}));
|
||||
|
||||
<radialGradient id="petal-gradient-2" cx="30%" cy="30%">
|
||||
<stop offset="0%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} />
|
||||
<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>
|
||||
return (
|
||||
<div
|
||||
className={wrapperClasses}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
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%">
|
||||
<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%">
|
||||
<stop
|
||||
offset="0%"
|
||||
style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
|
||||
/>
|
||||
<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%">
|
||||
<stop offset="0%" style={{ stopColor: '#fae8ff', stopOpacity: 1 }} />
|
||||
<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-3" 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="center-gradient" cx="50%" cy="50%">
|
||||
<stop offset="0%" style={{ stopColor: '#fef3c7', stopOpacity: 1 }} />
|
||||
<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>
|
||||
<radialGradient id="petal-gradient-4" cx="30%" cy="30%">
|
||||
<stop
|
||||
offset="0%"
|
||||
style={{ stopColor: "#fae8ff", stopOpacity: 1 }}
|
||||
/>
|
||||
<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%">
|
||||
<stop offset="0%" style={{ stopColor: '#fffbeb', stopOpacity: 1 }} />
|
||||
<stop offset="50%" style={{ stopColor: '#fef3c7', stopOpacity: 1 }} />
|
||||
<stop offset="100%" style={{ stopColor: '#fde68a', stopOpacity: 1 }} />
|
||||
</radialGradient>
|
||||
<radialGradient id="center-gradient" cx="50%" cy="50%">
|
||||
<stop
|
||||
offset="0%"
|
||||
style={{ stopColor: "#fef3c7", stopOpacity: 1 }}
|
||||
/>
|
||||
<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%">
|
||||
<stop offset="0%" style={{ stopColor: '#ffffff', stopOpacity: 0.98 }} />
|
||||
<stop offset="100%" style={{ stopColor: '#f3f4f6', stopOpacity: 0.98 }} />
|
||||
</linearGradient>
|
||||
<radialGradient id="center-inner-gradient" cx="50%" cy="50%">
|
||||
<stop
|
||||
offset="0%"
|
||||
style={{ stopColor: "#fffbeb", stopOpacity: 1 }}
|
||||
/>
|
||||
<stop
|
||||
offset="50%"
|
||||
style={{ stopColor: "#fef3c7", stopOpacity: 1 }}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
style={{ stopColor: "#fde68a", stopOpacity: 1 }}
|
||||
/>
|
||||
</radialGradient>
|
||||
|
||||
{/* Enhanced Filters */}
|
||||
<filter id="petal-glow">
|
||||
<feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="page-gradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="100%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
style={{ stopColor: "#ffffff", stopOpacity: 0.98 }}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
style={{ stopColor: "#f3f4f6", stopOpacity: 0.98 }}
|
||||
/>
|
||||
</linearGradient>
|
||||
|
||||
<filter id="intense-glow">
|
||||
<feGaussianBlur stdDeviation="8" result="coloredBlur" />
|
||||
<feComponentTransfer in="coloredBlur" result="brightBlur">
|
||||
<feFuncA type="linear" slope="1.5" />
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode in="brightBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
{/* Enhanced Filters */}
|
||||
<filter id="petal-glow">
|
||||
<feGaussianBlur stdDeviation="2.5" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="center-glow">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="intense-glow">
|
||||
<feGaussianBlur stdDeviation="8" result="coloredBlur" />
|
||||
<feComponentTransfer in="coloredBlur" result="brightBlur">
|
||||
<feFuncA type="linear" slope="1.5" />
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode in="brightBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="sparkle-glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="center-glow">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="page-shadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="4" floodOpacity="0.15" />
|
||||
</filter>
|
||||
</defs>
|
||||
<filter id="sparkle-glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
{/* Subtle background glow */}
|
||||
<circle className="bg-glow" cx="128" cy="128" r="120" fill="url(#petal-gradient-3)" opacity="0.08" />
|
||||
<filter id="page-shadow">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="4" floodOpacity="0.15" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{/* Outer layer - Large petals (8 petals) */}
|
||||
<g className="outer-petals">
|
||||
{[
|
||||
{ angle: 0, scaleX: 1.1, scaleY: 1, gradient: 1 },
|
||||
{ angle: 45, scaleX: 1, scaleY: 1.05, gradient: 2 },
|
||||
{ angle: 90, scaleX: 1.05, scaleY: 1, gradient: 3 },
|
||||
{ angle: 135, scaleX: 1, scaleY: 1.1, gradient: 4 },
|
||||
{ angle: 180, scaleX: 1.08, scaleY: 1, gradient: 1 },
|
||||
{ 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>
|
||||
{/* Subtle background glow */}
|
||||
<circle
|
||||
className="bg-glow"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="120"
|
||||
fill="url(#petal-gradient-3)"
|
||||
opacity="0.08"
|
||||
/>
|
||||
|
||||
{/* Middle layer - Medium petals (8 petals, offset) */}
|
||||
<g className="middle-petals">
|
||||
{[
|
||||
{ angle: 22.5, scaleX: 1, scaleY: 1, gradient: 2 },
|
||||
{ angle: 67.5, scaleX: 1.05, scaleY: 1, gradient: 3 },
|
||||
{ angle: 112.5, scaleX: 1, scaleY: 1.02, gradient: 4 },
|
||||
{ angle: 157.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
|
||||
{ angle: 202.5, scaleX: 1, scaleY: 1.05, gradient: 2 },
|
||||
{ angle: 247.5, scaleX: 1.03, scaleY: 1, gradient: 3 },
|
||||
{ angle: 292.5, scaleX: 1, scaleY: 1, gradient: 4 },
|
||||
{ angle: 337.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
|
||||
].map((petal, i) => (
|
||||
<ellipse
|
||||
key={`middle-${i}`}
|
||||
className={`petal middle-petal petal-m-${i}`}
|
||||
cx="128"
|
||||
cy="78"
|
||||
rx="34"
|
||||
ry="56"
|
||||
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>
|
||||
{/* Outer layer - Large petals (8 petals) */}
|
||||
<g className="outer-petals">
|
||||
{[
|
||||
{ angle: 0, scaleX: 1.1, scaleY: 1, gradient: 1 },
|
||||
{ angle: 45, scaleX: 1, scaleY: 1.05, gradient: 2 },
|
||||
{ angle: 90, scaleX: 1.05, scaleY: 1, gradient: 3 },
|
||||
{ angle: 135, scaleX: 1, scaleY: 1.1, gradient: 4 },
|
||||
{ angle: 180, scaleX: 1.08, scaleY: 1, gradient: 1 },
|
||||
{ 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>
|
||||
|
||||
{/* Inner layer - Small petals (10 petals) */}
|
||||
<g className="inner-petals">
|
||||
{[
|
||||
{ angle: 0, gradient: 3 },
|
||||
{ angle: 45, gradient: 4 },
|
||||
{ angle: 90, gradient: 1 },
|
||||
{ angle: 135, gradient: 2 },
|
||||
{ angle: 180, gradient: 3 },
|
||||
{ angle: 225, gradient: 4 },
|
||||
{ angle: 270, gradient: 1 },
|
||||
{ angle: 315, gradient: 2 },
|
||||
].map((petal, i) => (
|
||||
<ellipse
|
||||
key={`inner-${i}`}
|
||||
className={`petal inner-petal petal-i-${i}`}
|
||||
cx="128"
|
||||
cy="88"
|
||||
rx="28"
|
||||
ry="44"
|
||||
fill={`url(#petal-gradient-${petal.gradient})`}
|
||||
filter="url(#petal-glow)"
|
||||
style={{rotate: `${petal.angle}deg`}}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
{/* Middle layer - Medium petals (8 petals, offset) */}
|
||||
<g className="middle-petals">
|
||||
{[
|
||||
{ angle: 22.5, scaleX: 1, scaleY: 1, gradient: 2 },
|
||||
{ angle: 67.5, scaleX: 1.05, scaleY: 1, gradient: 3 },
|
||||
{ angle: 112.5, scaleX: 1, scaleY: 1.02, gradient: 4 },
|
||||
{ angle: 157.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
|
||||
{ angle: 202.5, scaleX: 1, scaleY: 1.05, gradient: 2 },
|
||||
{ angle: 247.5, scaleX: 1.03, scaleY: 1, gradient: 3 },
|
||||
{ angle: 292.5, scaleX: 1, scaleY: 1, gradient: 4 },
|
||||
{ angle: 337.5, scaleX: 1.02, scaleY: 1, gradient: 1 },
|
||||
].map((petal, i) => (
|
||||
<ellipse
|
||||
key={`middle-${i}`}
|
||||
className={`petal middle-petal petal-m-${i}`}
|
||||
cx="128"
|
||||
cy="78"
|
||||
rx="34"
|
||||
ry="56"
|
||||
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>
|
||||
|
||||
{/* Center circles - Flower stamen */}
|
||||
<circle
|
||||
className="center-circle-outer"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="12"
|
||||
fill="url(#center-gradient)"
|
||||
filter="url(#center-glow)"
|
||||
/>
|
||||
<circle
|
||||
className="center-circle-inner"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="2"
|
||||
fill="url(#center-inner-gradient)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
{/* Inner layer - Small petals (10 petals) */}
|
||||
<g className="inner-petals">
|
||||
{[
|
||||
{ angle: 0, gradient: 3 },
|
||||
{ angle: 45, gradient: 4 },
|
||||
{ angle: 90, gradient: 1 },
|
||||
{ angle: 135, gradient: 2 },
|
||||
{ angle: 180, gradient: 3 },
|
||||
{ angle: 225, gradient: 4 },
|
||||
{ angle: 270, gradient: 1 },
|
||||
{ angle: 315, gradient: 2 },
|
||||
].map((petal, i) => (
|
||||
<ellipse
|
||||
key={`inner-${i}`}
|
||||
className={`petal inner-petal petal-i-${i}`}
|
||||
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 */}
|
||||
<g className="center-stamens">
|
||||
{Array.from({ length: 8 }).map((_, i) => {
|
||||
const angle = (360 / 8) * i
|
||||
const x = 128 + Math.cos((angle * Math.PI) / 180) * 10
|
||||
const y = 128 + Math.sin((angle * Math.PI) / 180) * 10
|
||||
return (
|
||||
<circle
|
||||
key={`stamen-${i}`}
|
||||
className={`stamen stamen-${i}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r="2"
|
||||
fill="#d97706"
|
||||
opacity="0.8"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
{/* Center circles - Flower stamen */}
|
||||
<circle
|
||||
className="center-circle-outer"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="12"
|
||||
fill="url(#center-gradient)"
|
||||
filter="url(#center-glow)"
|
||||
/>
|
||||
<circle
|
||||
className="center-circle-inner"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="2"
|
||||
fill="url(#center-inner-gradient)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
|
||||
{/* Sparkles - ambient magical effect */}
|
||||
<g className="sparkles">
|
||||
<circle className="sparkle sparkle-1" cx="180" cy="75" r="3" fill="#fbbf24" filter="url(#sparkle-glow)" />
|
||||
<circle className="sparkle sparkle-2" cx="76" cy="76" r="2.5" fill="#a855f7" filter="url(#sparkle-glow)" />
|
||||
<circle className="sparkle sparkle-3" 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>
|
||||
{/* Center details - tiny stamens */}
|
||||
<g className="center-stamens">
|
||||
{Array.from({ length: 8 }).map((_, i) => {
|
||||
const angle = (360 / 8) * i;
|
||||
const x = 128 + Math.cos((angle * Math.PI) / 180) * 10;
|
||||
const y = 128 + Math.sin((angle * Math.PI) / 180) * 10;
|
||||
return (
|
||||
<circle
|
||||
key={`stamen-${i}`}
|
||||
className={`stamen stamen-${i}`}
|
||||
cx={x}
|
||||
cy={y}
|
||||
r="2"
|
||||
fill="#d97706"
|
||||
opacity="0.8"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Flying bloom particles (visible on hover) */}
|
||||
<g className="bloom-particles">
|
||||
{bloomParticles.map((particle) => (
|
||||
<circle
|
||||
key={`bloom-particle-${particle.id}`}
|
||||
className={`bloom-particle bloom-particle-${particle.id}`}
|
||||
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>
|
||||
{/* Sparkles - ambient magical effect */}
|
||||
<g className="sparkles">
|
||||
<circle
|
||||
className="sparkle sparkle-1"
|
||||
cx="180"
|
||||
cy="75"
|
||||
r="3"
|
||||
fill="#fbbf24"
|
||||
filter="url(#sparkle-glow)"
|
||||
/>
|
||||
<circle
|
||||
className="sparkle sparkle-2"
|
||||
cx="76"
|
||||
cy="76"
|
||||
r="2.5"
|
||||
fill="#a855f7"
|
||||
filter="url(#sparkle-glow)"
|
||||
/>
|
||||
<circle
|
||||
className="sparkle sparkle-3"
|
||||
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 */}
|
||||
{showLabel && (
|
||||
<div className="icon-label">
|
||||
<span className="label-text">Pivoine Docs</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
{/* Flying bloom particles (visible on hover) */}
|
||||
<g className="bloom-particles">
|
||||
{bloomParticles.map((particle) => (
|
||||
<circle
|
||||
key={`bloom-particle-${particle.id}`}
|
||||
className={`bloom-particle bloom-particle-${particle.id}`}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as KomposeIcon } from './KomposeIcon'
|
||||
export { default as PivoineDocsIcon } from './PivoineDocsIcon'
|
||||
export { default as KomposeIcon } from "./KomposeIcon";
|
||||
export { default as PivoineDocsIcon } from "./PivoineDocsIcon";
|
||||
|
||||
@@ -6,11 +6,11 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
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',
|
||||
},
|
||||
output: "export",
|
||||
reactStrictMode: true,
|
||||
|
||||
// Image optimization
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
},
|
||||
// Next.js 15 uses turbopack by default for dev
|
||||
// No need to explicitly enable swcMinify anymore
|
||||
|
||||
// Headers for security
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
// Optimize production build
|
||||
compiler: {
|
||||
removeConsole: process.env.NODE_ENV === "production",
|
||||
},
|
||||
|
||||
// Enable experimental features if needed
|
||||
experimental: {
|
||||
// turbo is now stable in Next.js 15
|
||||
// Add other experimental features here if needed
|
||||
},
|
||||
// Image optimization
|
||||
images: {
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
|
||||
turbopack: {
|
||||
root: '.'
|
||||
}
|
||||
}
|
||||
// Headers for security
|
||||
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;
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
{
|
||||
"name": "pivoine-docs-hub",
|
||||
"version": "1.0.0",
|
||||
"description": "Documentation hub for Pivoine projects by Valknar",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"lucide-react": "^0.263.1",
|
||||
"next": "^15.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.0"
|
||||
"name": "pivoine-docs-hub",
|
||||
"version": "1.0.0",
|
||||
"description": "Documentation hub for Pivoine projects by Valknar",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"lucide-react": "^0.263.1",
|
||||
"next": "^15.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0",
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.0.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"name": "Pivoine Docs Hub",
|
||||
"short_name": "Pivoine Docs",
|
||||
"description": "Documentation hub for all Pivoine projects by Valknar",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#a855f7",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["documentation", "developer", "tools"],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshot-wide.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "/screenshot-narrow.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
]
|
||||
"name": "Pivoine Docs Hub",
|
||||
"short_name": "Pivoine Docs",
|
||||
"description": "Documentation hub for all Pivoine projects by Valknar",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#a855f7",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"categories": ["documentation", "developer", "tools"],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshot-wide.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide"
|
||||
},
|
||||
{
|
||||
"src": "/screenshot-narrow.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
}
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"buildCommand": "pnpm build",
|
||||
"devCommand": "pnpm dev",
|
||||
"installCommand": "pnpm install",
|
||||
"framework": "nextjs",
|
||||
"regions": ["iad1"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "SAMEORIGIN"
|
||||
},
|
||||
{
|
||||
"key": "X-XSS-Protection",
|
||||
"value": "1; mode=block"
|
||||
},
|
||||
{
|
||||
"key": "Referrer-Policy",
|
||||
"value": "origin-when-cross-origin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"buildCommand": "pnpm build",
|
||||
"devCommand": "pnpm dev",
|
||||
"installCommand": "pnpm install",
|
||||
"framework": "nextjs",
|
||||
"regions": ["iad1"],
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "SAMEORIGIN"
|
||||
},
|
||||
{
|
||||
"key": "X-XSS-Protection",
|
||||
"value": "1; mode=block"
|
||||
},
|
||||
{
|
||||
"key": "Referrer-Policy",
|
||||
"value": "origin-when-cross-origin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,60 +4,55 @@
|
||||
// gem install scss-lint
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
// Project configuration
|
||||
grunt.initConfig({
|
||||
// Metadata
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
banner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
|
||||
'<%= grunt.template.today("yyyy-mm-dd") %>\n' +
|
||||
'<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
|
||||
'* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
|
||||
' Licensed <%= props.license %> */\n',
|
||||
"use strict";
|
||||
// Project configuration
|
||||
grunt.initConfig({
|
||||
// Metadata
|
||||
pkg: grunt.file.readJSON("package.json"),
|
||||
banner:
|
||||
"/*! <%= pkg.name %> - v<%= pkg.version %> - " +
|
||||
'<%= grunt.template.today("yyyy-mm-dd") %>\n' +
|
||||
'<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
|
||||
'* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
|
||||
" Licensed <%= props.license %> */\n",
|
||||
|
||||
webfont: {
|
||||
icons: {
|
||||
src: [
|
||||
'icons/sbed/*.svg',
|
||||
'icons/lorc/*.svg'
|
||||
],
|
||||
dest: 'fonts',
|
||||
options: {
|
||||
styles: 'font,icon,extra',
|
||||
fontFilename: 'game-icons',
|
||||
types: ['eot', 'woff2', 'woff', 'ttf', 'svg'],
|
||||
syntax: 'bootstrap',
|
||||
destCss: 'css',
|
||||
destScss: 'scss',
|
||||
templateOptions: {
|
||||
baseClass: 'gi',
|
||||
classPrefix: 'gi-'
|
||||
},
|
||||
fontFamilyName: 'GameIcons',
|
||||
font: 'game-icons',
|
||||
stylesheets: ['css', 'scss'],
|
||||
fontPathVariables: true,
|
||||
htmlDemo: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
// CSS Min
|
||||
// =======
|
||||
cssmin: {
|
||||
target: {
|
||||
files: {
|
||||
'css/game-icons.min.css': 'css/game-icons.css'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
webfont: {
|
||||
icons: {
|
||||
src: ["icons/sbed/*.svg", "icons/lorc/*.svg"],
|
||||
dest: "fonts",
|
||||
options: {
|
||||
styles: "font,icon,extra",
|
||||
fontFilename: "game-icons",
|
||||
types: ["eot", "woff2", "woff", "ttf", "svg"],
|
||||
syntax: "bootstrap",
|
||||
destCss: "css",
|
||||
destScss: "scss",
|
||||
templateOptions: {
|
||||
baseClass: "gi",
|
||||
classPrefix: "gi-",
|
||||
},
|
||||
fontFamilyName: "GameIcons",
|
||||
font: "game-icons",
|
||||
stylesheets: ["css", "scss"],
|
||||
fontPathVariables: true,
|
||||
htmlDemo: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// CSS Min
|
||||
// =======
|
||||
cssmin: {
|
||||
target: {
|
||||
files: {
|
||||
"css/game-icons.min.css": "css/game-icons.css",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// These plugins provide necessary tasks
|
||||
grunt.loadNpmTasks('grunt-webfont');
|
||||
grunt.loadNpmTasks('grunt-contrib-cssmin');
|
||||
// These plugins provide necessary tasks
|
||||
grunt.loadNpmTasks("grunt-webfont");
|
||||
grunt.loadNpmTasks("grunt-contrib-cssmin");
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'webfont',
|
||||
'cssmin'
|
||||
]);
|
||||
grunt.registerTask("default", ["webfont", "cssmin"]);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "game-icons",
|
||||
"style": "css/game-icons.css",
|
||||
"sass": "scss/game-icons.scss",
|
||||
"version": "0.1.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nagoshiashumari/game-icons.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.6.1",
|
||||
"grunt-contrib-cssmin": "^5.0.0",
|
||||
"grunt-webfont": "^1.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "grunt",
|
||||
"test": "grunt scsslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"grunt-cli": "^1.5.0"
|
||||
}
|
||||
"name": "game-icons",
|
||||
"style": "css/game-icons.css",
|
||||
"sass": "scss/game-icons.scss",
|
||||
"version": "0.1.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/nagoshiashumari/game-icons.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"grunt": "^1.6.1",
|
||||
"grunt-contrib-cssmin": "^5.0.0",
|
||||
"grunt-webfont": "^1.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "grunt",
|
||||
"test": "grunt scsslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"grunt-cli": "^1.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
1
Projects/kompose/.gitignore
vendored
1
Projects/kompose/.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
|
||||
.DS_Store
|
||||
*.log*
|
||||
|
||||
|
||||
@@ -1,58 +1,63 @@
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'emerald',
|
||||
secondary: 'fuchsia',
|
||||
neutral: 'zinc'
|
||||
},
|
||||
footer: {
|
||||
slots: {
|
||||
root: 'border-t border-default',
|
||||
left: 'text-sm text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
seo: {
|
||||
siteName: 'Kompose'
|
||||
},
|
||||
header: {
|
||||
title: '',
|
||||
to: '/',
|
||||
logo: {
|
||||
alt: '',
|
||||
light: '',
|
||||
dark: ''
|
||||
},
|
||||
search: true,
|
||||
colorMode: true,
|
||||
links: [{
|
||||
'icon': 'i-simple-icons-github',
|
||||
'to': 'https://github.com/nuxt-ui-templates/docs',
|
||||
'target': '_blank',
|
||||
'aria-label': 'GitHub'
|
||||
}]
|
||||
},
|
||||
footer: {
|
||||
credits: `kompose © Valknar ${new Date().getFullYear()}`,
|
||||
colorMode: false,
|
||||
links: [{
|
||||
'icon': 'i-simple-icons-x',
|
||||
'to': 'https://x.com/bordeaux1981',
|
||||
'target': '_blank',
|
||||
'aria-label': 'Nuxt on X'
|
||||
}, {
|
||||
'icon': 'i-simple-icons-github',
|
||||
'to': 'https://github.com/valknarogg',
|
||||
'target': '_blank',
|
||||
'aria-label': 'Valknar on GitHub'
|
||||
}]
|
||||
},
|
||||
toc: {
|
||||
title: 'Table of Contents',
|
||||
bottom: {
|
||||
title: 'Community',
|
||||
edit: 'https://code.pivoine.art/valknar/kompose/src/branch/main/docs/content',
|
||||
links: []
|
||||
}
|
||||
}
|
||||
})
|
||||
ui: {
|
||||
colors: {
|
||||
primary: "emerald",
|
||||
secondary: "fuchsia",
|
||||
neutral: "zinc",
|
||||
},
|
||||
footer: {
|
||||
slots: {
|
||||
root: "border-t border-default",
|
||||
left: "text-sm text-muted",
|
||||
},
|
||||
},
|
||||
},
|
||||
seo: {
|
||||
siteName: "Kompose",
|
||||
},
|
||||
header: {
|
||||
title: "",
|
||||
to: "/",
|
||||
logo: {
|
||||
alt: "",
|
||||
light: "",
|
||||
dark: "",
|
||||
},
|
||||
search: true,
|
||||
colorMode: true,
|
||||
links: [
|
||||
{
|
||||
icon: "i-simple-icons-github",
|
||||
to: "https://github.com/nuxt-ui-templates/docs",
|
||||
target: "_blank",
|
||||
"aria-label": "GitHub",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
credits: `kompose © Valknar ${new Date().getFullYear()}`,
|
||||
colorMode: false,
|
||||
links: [
|
||||
{
|
||||
icon: "i-simple-icons-x",
|
||||
to: "https://x.com/bordeaux1981",
|
||||
target: "_blank",
|
||||
"aria-label": "Nuxt on X",
|
||||
},
|
||||
{
|
||||
icon: "i-simple-icons-github",
|
||||
to: "https://github.com/valknarogg",
|
||||
target: "_blank",
|
||||
"aria-label": "Valknar on GitHub",
|
||||
},
|
||||
],
|
||||
},
|
||||
toc: {
|
||||
title: "Table of Contents",
|
||||
bottom: {
|
||||
title: "Community",
|
||||
edit: "https://code.pivoine.art/valknar/kompose/src/branch/main/docs/content",
|
||||
links: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
const { seo } = useAppConfig()
|
||||
const { seo } = useAppConfig();
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
||||
server: false
|
||||
})
|
||||
const { data: navigation } = await useAsyncData("navigation", () =>
|
||||
queryCollectionNavigation("docs"),
|
||||
);
|
||||
const { data: files } = useLazyAsyncData(
|
||||
"search",
|
||||
() => queryCollectionSearchSections("docs"),
|
||||
{
|
||||
server: false,
|
||||
},
|
||||
);
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
],
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
}
|
||||
})
|
||||
meta: [{ name: "viewport", content: "width=device-width, initial-scale=1" }],
|
||||
htmlAttrs: {
|
||||
lang: "en",
|
||||
},
|
||||
});
|
||||
|
||||
useSeoMeta({
|
||||
titleTemplate: `%s - ${seo?.siteName}`,
|
||||
ogSiteName: seo?.siteName,
|
||||
twitterCard: 'summary_large_image'
|
||||
})
|
||||
titleTemplate: `%s - ${seo?.siteName}`,
|
||||
ogSiteName: seo?.siteName,
|
||||
twitterCard: "summary_large_image",
|
||||
});
|
||||
|
||||
provide('navigation', navigation)
|
||||
provide("navigation", navigation);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,30 +4,30 @@
|
||||
@source "../../../content/**/*";
|
||||
|
||||
@theme static {
|
||||
--container-8xl: 90rem;
|
||||
--font-sans: 'Public Sans', sans-serif;
|
||||
--container-8xl: 90rem;
|
||||
--font-sans: "Public Sans", sans-serif;
|
||||
|
||||
--color-green-50: #EFFDF5;
|
||||
--color-green-100: #D9FBE8;
|
||||
--color-green-200: #B3F5D1;
|
||||
--color-green-300: #75EDAE;
|
||||
--color-green-400: #00DC82;
|
||||
--color-green-500: #00C16A;
|
||||
--color-green-600: #00A155;
|
||||
--color-green-700: #007F45;
|
||||
--color-green-800: #016538;
|
||||
--color-green-900: #0A5331;
|
||||
--color-green-950: #052E16;
|
||||
--color-green-50: #effdf5;
|
||||
--color-green-100: #d9fbe8;
|
||||
--color-green-200: #b3f5d1;
|
||||
--color-green-300: #75edae;
|
||||
--color-green-400: #00dc82;
|
||||
--color-green-500: #00c16a;
|
||||
--color-green-600: #00a155;
|
||||
--color-green-700: #007f45;
|
||||
--color-green-800: #016538;
|
||||
--color-green-900: #0a5331;
|
||||
--color-green-950: #052e16;
|
||||
}
|
||||
|
||||
:root {
|
||||
--ui-container: var(--container-8xl);
|
||||
--ui-container: var(--container-8xl);
|
||||
}
|
||||
|
||||
h2 > a > span + span {
|
||||
@apply size-6 align-text-top;
|
||||
@apply size-6 align-text-top;
|
||||
}
|
||||
|
||||
h3 > a > span + span {
|
||||
@apply size-5 align-text-top;
|
||||
@apply size-5 align-text-top;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const { footer } = useAppConfig()
|
||||
const { footer } = useAppConfig();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -75,50 +75,50 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
size?: string
|
||||
interactive?: boolean
|
||||
size?: string;
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: '192px',
|
||||
interactive: true
|
||||
})
|
||||
size: "192px",
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
const isClicked = ref(false)
|
||||
const showRipple = ref(false)
|
||||
const isClicked = ref(false);
|
||||
const showRipple = ref(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.interactive) return
|
||||
if (!props.interactive) return;
|
||||
|
||||
isClicked.value = true
|
||||
showRipple.value = true
|
||||
isClicked.value = true;
|
||||
showRipple.value = true;
|
||||
|
||||
setTimeout(() => {
|
||||
isClicked.value = false
|
||||
}, 600)
|
||||
setTimeout(() => {
|
||||
isClicked.value = false;
|
||||
}, 600);
|
||||
|
||||
setTimeout(() => {
|
||||
showRipple.value = false
|
||||
}, 800)
|
||||
}
|
||||
setTimeout(() => {
|
||||
showRipple.value = false;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const handleHover = () => {
|
||||
if (!props.interactive) return
|
||||
// Hover animations are handled by CSS
|
||||
}
|
||||
if (!props.interactive) return;
|
||||
// Hover animations are handled by CSS
|
||||
};
|
||||
|
||||
const handleLeave = () => {
|
||||
if (!props.interactive) return
|
||||
// Leave animations are handled by CSS
|
||||
}
|
||||
if (!props.interactive) return;
|
||||
// Leave animations are handled by CSS
|
||||
};
|
||||
|
||||
const handleTouch = (e: TouchEvent) => {
|
||||
if (!props.interactive) return
|
||||
handleClick()
|
||||
}
|
||||
if (!props.interactive) return;
|
||||
handleClick();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: '42px' // Can be: '24px', '32px', '42px', '56px', etc.
|
||||
}
|
||||
})
|
||||
size: {
|
||||
type: String,
|
||||
default: "42px", // Can be: '24px', '32px', '42px', '56px', etc.
|
||||
},
|
||||
});
|
||||
|
||||
const isHovered = ref(false)
|
||||
const isHovered = ref(false);
|
||||
|
||||
// Load Google Font
|
||||
if (typeof document !== 'undefined') {
|
||||
const link = document.createElement('link')
|
||||
link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@800;900&display=swap'
|
||||
link.rel = 'stylesheet'
|
||||
document.head.appendChild(link)
|
||||
if (typeof document !== "undefined") {
|
||||
const link = document.createElement("link");
|
||||
link.href =
|
||||
"https://fonts.googleapis.com/css2?family=Inter:wght@800;900&display=swap";
|
||||
link.rel = "stylesheet";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(defineProps<{ title?: string, description?: string, headline?: string }>(), {
|
||||
title: 'title',
|
||||
description: 'description'
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{ title?: string; description?: string; headline?: string }>(),
|
||||
{
|
||||
title: "title",
|
||||
description: "description",
|
||||
},
|
||||
);
|
||||
|
||||
const title = computed(() => (props.title || '').slice(0, 60))
|
||||
const description = computed(() => (props.description || '').slice(0, 200))
|
||||
const title = computed(() => (props.title || "").slice(0, 60));
|
||||
const description = computed(() => (props.description || "").slice(0, 200));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const { copy, copied } = useClipboard()
|
||||
const site = useSiteConfig()
|
||||
const isCopying = ref(false)
|
||||
console.log(site)
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const { copy, copied } = useClipboard();
|
||||
const site = useSiteConfig();
|
||||
const isCopying = ref(false);
|
||||
console.log(site);
|
||||
|
||||
const mdPath = computed(() => `${site.url}/raw${route.path}.md`)
|
||||
const mdPath = computed(() => `${site.url}/raw${route.path}.md`);
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: 'Copy Markdown link',
|
||||
icon: 'i-lucide-link',
|
||||
onSelect() {
|
||||
copy(mdPath.value)
|
||||
toast.add({
|
||||
title: 'Copied to clipboard',
|
||||
icon: 'i-lucide-check-circle'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'View as Markdown',
|
||||
icon: 'i-simple-icons:markdown',
|
||||
target: '_blank',
|
||||
to: `/raw${route.path}.md`
|
||||
},
|
||||
{
|
||||
label: 'Open in ChatGPT',
|
||||
icon: 'i-simple-icons:openai',
|
||||
target: '_blank',
|
||||
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
||||
},
|
||||
{
|
||||
label: 'Open in Claude',
|
||||
icon: 'i-simple-icons:anthropic',
|
||||
target: '_blank',
|
||||
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`
|
||||
}
|
||||
]
|
||||
{
|
||||
label: "Copy Markdown link",
|
||||
icon: "i-lucide-link",
|
||||
onSelect() {
|
||||
copy(mdPath.value);
|
||||
toast.add({
|
||||
title: "Copied to clipboard",
|
||||
icon: "i-lucide-check-circle",
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "View as Markdown",
|
||||
icon: "i-simple-icons:markdown",
|
||||
target: "_blank",
|
||||
to: `/raw${route.path}.md`,
|
||||
},
|
||||
{
|
||||
label: "Open in ChatGPT",
|
||||
icon: "i-simple-icons:openai",
|
||||
target: "_blank",
|
||||
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
|
||||
},
|
||||
{
|
||||
label: "Open in Claude",
|
||||
icon: "i-simple-icons:anthropic",
|
||||
target: "_blank",
|
||||
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${mdPath.value} so I can ask questions about it.`)}`,
|
||||
},
|
||||
];
|
||||
|
||||
async function copyPage() {
|
||||
isCopying.value = true
|
||||
copy(await $fetch<string>(`/raw${route.path}.md`))
|
||||
isCopying.value = false
|
||||
isCopying.value = true;
|
||||
copy(await $fetch<string>(`/raw${route.path}.md`));
|
||||
isCopying.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const { isLoading } = useLoadingIndicator()
|
||||
const { isLoading } = useLoadingIndicator();
|
||||
|
||||
const appear = ref(false)
|
||||
const appeared = ref(false)
|
||||
const appear = ref(false);
|
||||
const appeared = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
appear.value = true
|
||||
setTimeout(() => {
|
||||
appeared.value = true
|
||||
}, 1000)
|
||||
}, 0)
|
||||
})
|
||||
setTimeout(() => {
|
||||
appear.value = true;
|
||||
setTimeout(() => {
|
||||
appeared.value = true;
|
||||
}, 1000);
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,58 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
interface Star {
|
||||
x: number
|
||||
y: number
|
||||
size: number
|
||||
x: number;
|
||||
y: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
starCount?: number
|
||||
color?: string
|
||||
speed?: 'slow' | 'normal' | 'fast'
|
||||
size?: { min: number, max: number }
|
||||
}>(), {
|
||||
starCount: 300,
|
||||
color: 'var(--ui-primary)',
|
||||
speed: 'normal',
|
||||
size: () => ({
|
||||
min: 1,
|
||||
max: 2
|
||||
})
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
starCount?: number;
|
||||
color?: string;
|
||||
speed?: "slow" | "normal" | "fast";
|
||||
size?: { min: number; max: number };
|
||||
}>(),
|
||||
{
|
||||
starCount: 300,
|
||||
color: "var(--ui-primary)",
|
||||
speed: "normal",
|
||||
size: () => ({
|
||||
min: 1,
|
||||
max: 2,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
// Generate random star positions and sizes
|
||||
const generateStars = (count: number): Star[] => {
|
||||
return Array.from({ length: count }, () => ({
|
||||
x: Math.floor(Math.random() * 2000),
|
||||
y: Math.floor(Math.random() * 2000),
|
||||
size: typeof props.size === 'number'
|
||||
? props.size
|
||||
: Math.random() * (props.size.max - props.size.min) + props.size.min
|
||||
}))
|
||||
}
|
||||
return Array.from({ length: count }, () => ({
|
||||
x: Math.floor(Math.random() * 2000),
|
||||
y: Math.floor(Math.random() * 2000),
|
||||
size:
|
||||
typeof props.size === "number"
|
||||
? props.size
|
||||
: Math.random() * (props.size.max - props.size.min) + props.size.min,
|
||||
}));
|
||||
};
|
||||
|
||||
// Define speed configurations once
|
||||
const speedMap = {
|
||||
slow: { duration: 200, opacity: 0.5, ratio: 0.3 },
|
||||
normal: { duration: 150, opacity: 0.75, ratio: 0.3 },
|
||||
fast: { duration: 100, opacity: 1, ratio: 0.4 }
|
||||
}
|
||||
slow: { duration: 200, opacity: 0.5, ratio: 0.3 },
|
||||
normal: { duration: 150, opacity: 0.75, ratio: 0.3 },
|
||||
fast: { duration: 100, opacity: 1, ratio: 0.4 },
|
||||
};
|
||||
|
||||
// Use a more efficient approach to generate and store stars
|
||||
const stars = useState<{ slow: Star[], normal: Star[], fast: Star[] }>('stars', () => {
|
||||
return {
|
||||
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))
|
||||
}
|
||||
})
|
||||
const stars = useState<{ slow: Star[]; normal: Star[]; fast: Star[] }>(
|
||||
"stars",
|
||||
() => {
|
||||
return {
|
||||
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
|
||||
const starLayers = computed(() => [
|
||||
{ stars: stars.value.fast, ...speedMap.fast },
|
||||
{ stars: stars.value.normal, ...speedMap.normal },
|
||||
{ stars: stars.value.slow, ...speedMap.slow }
|
||||
])
|
||||
{ stars: stars.value.fast, ...speedMap.fast },
|
||||
{ stars: stars.value.normal, ...speedMap.normal },
|
||||
{ stars: stars.value.slow, ...speedMap.slow },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
import type { NuxtError } from "#app";
|
||||
|
||||
defineProps<{
|
||||
error: NuxtError
|
||||
}>()
|
||||
error: NuxtError;
|
||||
}>();
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
}
|
||||
})
|
||||
htmlAttrs: {
|
||||
lang: "en",
|
||||
},
|
||||
});
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Page not found',
|
||||
description: 'We are sorry but this page could not be found.'
|
||||
})
|
||||
title: "Page not found",
|
||||
description: "We are sorry but this page could not be found.",
|
||||
});
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
||||
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
|
||||
server: false
|
||||
})
|
||||
const { data: navigation } = await useAsyncData("navigation", () =>
|
||||
queryCollectionNavigation("docs"),
|
||||
);
|
||||
const { data: files } = useLazyAsyncData(
|
||||
"search",
|
||||
() => queryCollectionSearchSections("docs"),
|
||||
{
|
||||
server: false,
|
||||
},
|
||||
);
|
||||
|
||||
provide('navigation', navigation)
|
||||
provide("navigation", navigation);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
import { findPageHeadline } from '@nuxt/content/utils'
|
||||
import type { ContentNavigationItem } from "@nuxt/content";
|
||||
import { findPageHeadline } from "@nuxt/content/utils";
|
||||
|
||||
definePageMeta({
|
||||
layout: 'docs'
|
||||
})
|
||||
layout: "docs",
|
||||
});
|
||||
|
||||
const route = useRoute()
|
||||
const { toc } = useAppConfig()
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
const route = useRoute();
|
||||
const { toc } = useAppConfig();
|
||||
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) {
|
||||
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`, () => {
|
||||
return queryCollectionItemSurroundings('docs', route.path, {
|
||||
fields: ['description']
|
||||
})
|
||||
})
|
||||
return queryCollectionItemSurroundings("docs", route.path, {
|
||||
fields: ["description"],
|
||||
});
|
||||
});
|
||||
|
||||
const title = page.value.seo?.title || page.value.title
|
||||
const description = page.value.seo?.description || page.value.description
|
||||
const title = page.value.seo?.title || page.value.title;
|
||||
const description = page.value.seo?.description || page.value.description;
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
ogTitle: title,
|
||||
description,
|
||||
ogDescription: description
|
||||
})
|
||||
title,
|
||||
ogTitle: title,
|
||||
description,
|
||||
ogDescription: description,
|
||||
});
|
||||
|
||||
const headline = computed(() => findPageHeadline(navigation?.value, page.value?.path))
|
||||
const headline = computed(() =>
|
||||
findPageHeadline(navigation?.value, page.value?.path),
|
||||
);
|
||||
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: headline.value
|
||||
})
|
||||
defineOgImageComponent("Docs", {
|
||||
headline: headline.value,
|
||||
});
|
||||
|
||||
const links = computed(() => {
|
||||
const links = []
|
||||
if (toc?.bottom?.edit) {
|
||||
links.push({
|
||||
icon: 'i-lucide-external-link',
|
||||
label: 'Edit this page',
|
||||
to: `${toc.bottom.edit}/${page?.value?.stem}.${page?.value?.extension}`,
|
||||
target: '_blank'
|
||||
})
|
||||
}
|
||||
const links = [];
|
||||
if (toc?.bottom?.edit) {
|
||||
links.push({
|
||||
icon: "i-lucide-external-link",
|
||||
label: "Edit this page",
|
||||
to: `${toc.bottom.edit}/${page?.value?.stem}.${page?.value?.extension}`,
|
||||
target: "_blank",
|
||||
});
|
||||
}
|
||||
|
||||
return [...links, ...(toc?.bottom?.links || [])].filter(Boolean)
|
||||
})
|
||||
return [...links, ...(toc?.bottom?.links || [])].filter(Boolean);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
|
||||
import { defineContentConfig, defineCollection, z } from "@nuxt/content";
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
landing: defineCollection({
|
||||
type: 'page',
|
||||
source: 'index.md'
|
||||
}),
|
||||
docs: defineCollection({
|
||||
type: 'page',
|
||||
source: {
|
||||
include: '**',
|
||||
},
|
||||
schema: z.object({
|
||||
links: z.array(z.object({
|
||||
label: z.string(),
|
||||
icon: z.string(),
|
||||
to: z.string(),
|
||||
target: z.string().optional()
|
||||
})).optional()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
collections: {
|
||||
landing: defineCollection({
|
||||
type: "page",
|
||||
source: "index.md",
|
||||
}),
|
||||
docs: defineCollection({
|
||||
type: "page",
|
||||
source: {
|
||||
include: "**",
|
||||
},
|
||||
schema: z.object({
|
||||
links: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
icon: z.string(),
|
||||
to: z.string(),
|
||||
target: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
import withNuxt from "./.nuxt/eslint.config.mjs";
|
||||
|
||||
export default withNuxt(
|
||||
// Your custom configs here
|
||||
)
|
||||
// Your custom configs here
|
||||
);
|
||||
|
||||
@@ -1,103 +1,102 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
app: {
|
||||
baseURL: '/kompose/',
|
||||
},
|
||||
modules: [
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/image',
|
||||
'@nuxt/ui',
|
||||
'@nuxt/content',
|
||||
'nuxt-og-image',
|
||||
'nuxt-llms'
|
||||
],
|
||||
app: {
|
||||
baseURL: "/kompose/",
|
||||
},
|
||||
modules: [
|
||||
"@nuxt/eslint",
|
||||
"@nuxt/image",
|
||||
"@nuxt/ui",
|
||||
"@nuxt/content",
|
||||
"nuxt-og-image",
|
||||
"nuxt-llms",
|
||||
],
|
||||
|
||||
// content: {
|
||||
// build: {
|
||||
// markdown: {
|
||||
// // Object syntax can be used to override default options
|
||||
// remarkPlugins: {
|
||||
// // Override remark-emoji options
|
||||
// 'remark-emoji': {
|
||||
// options: {
|
||||
// emoticon: true
|
||||
// }
|
||||
// },
|
||||
// // Disable remark-gfm
|
||||
// 'remark-gfm': false,
|
||||
// // Add remark-oembed
|
||||
// 'remark-oembed': {
|
||||
// // Options
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// content: {
|
||||
// build: {
|
||||
// markdown: {
|
||||
// // Object syntax can be used to override default options
|
||||
// remarkPlugins: {
|
||||
// // Override remark-emoji options
|
||||
// 'remark-emoji': {
|
||||
// options: {
|
||||
// emoticon: true
|
||||
// }
|
||||
// },
|
||||
// // Disable remark-gfm
|
||||
// 'remark-gfm': false,
|
||||
// // Add remark-oembed
|
||||
// 'remark-oembed': {
|
||||
// // Options
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
|
||||
devtools: {
|
||||
enabled: false
|
||||
},
|
||||
devtools: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
css: ["~/assets/css/main.css"],
|
||||
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
toc: {
|
||||
searchDepth: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
content: {
|
||||
build: {
|
||||
markdown: {
|
||||
toc: {
|
||||
searchDepth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2024-07-11',
|
||||
compatibilityDate: "2024-07-11",
|
||||
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: [
|
||||
'/'
|
||||
],
|
||||
crawlLinks: true,
|
||||
autoSubfolderIndex: false
|
||||
}
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ["/"],
|
||||
crawlLinks: true,
|
||||
autoSubfolderIndex: false,
|
||||
},
|
||||
},
|
||||
|
||||
eslint: {
|
||||
config: {
|
||||
stylistic: {
|
||||
commaDangle: 'never',
|
||||
braceStyle: '1tbs'
|
||||
}
|
||||
}
|
||||
},
|
||||
eslint: {
|
||||
config: {
|
||||
stylistic: {
|
||||
commaDangle: "never",
|
||||
braceStyle: "1tbs",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
icon: {
|
||||
provider: 'iconify'
|
||||
},
|
||||
icon: {
|
||||
provider: "iconify",
|
||||
},
|
||||
|
||||
llms: {
|
||||
domain: 'https://docs-template.nuxt.dev/',
|
||||
title: 'Nuxt Docs Template',
|
||||
description: 'A template for building documentation with Nuxt UI and Nuxt Content.',
|
||||
full: {
|
||||
title: 'Nuxt Docs Template - Full Documentation',
|
||||
description: 'This is the full documentation for the Nuxt Docs Template.'
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: 'Getting Started',
|
||||
contentCollection: 'docs',
|
||||
contentFilters: [
|
||||
{ field: 'path', operator: 'LIKE', value: '/getting-started%' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Essentials',
|
||||
contentCollection: 'docs',
|
||||
contentFilters: [
|
||||
{ field: 'path', operator: 'LIKE', value: '/essentials%' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
llms: {
|
||||
domain: "https://docs-template.nuxt.dev/",
|
||||
title: "Nuxt Docs Template",
|
||||
description:
|
||||
"A template for building documentation with Nuxt UI and Nuxt Content.",
|
||||
full: {
|
||||
title: "Nuxt Docs Template - Full Documentation",
|
||||
description: "This is the full documentation for the Nuxt Docs Template.",
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: "Getting Started",
|
||||
contentCollection: "docs",
|
||||
contentFilters: [
|
||||
{ field: "path", operator: "LIKE", value: "/getting-started%" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Essentials",
|
||||
contentCollection: "docs",
|
||||
contentFilters: [
|
||||
{ field: "path", operator: "LIKE", value: "/essentials%" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
{
|
||||
"name": "nuxt-ui-template-docs",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate": "nuxi generate",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.68",
|
||||
"@iconify-json/simple-icons": "^1.2.54",
|
||||
"@iconify-json/vscode-icons": "^1.2.30",
|
||||
"@nuxt/content": "^3.7.1",
|
||||
"@nuxt/image": "^1.11.0",
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"@nuxtjs/mdc": "^0.17.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@vite-pwa/nuxt": "^1.0.4",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"nuxt": "^4.1.2",
|
||||
"nuxt-llms": "0.1.3",
|
||||
"nuxt-og-image": "^5.1.11",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.9.0",
|
||||
"eslint": "^9.37.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vue-tsc": "^3.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"unimport": "4.1.1"
|
||||
}
|
||||
"name": "nuxt-ui-template-docs",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate": "nuxi generate",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.68",
|
||||
"@iconify-json/simple-icons": "^1.2.54",
|
||||
"@iconify-json/vscode-icons": "^1.2.30",
|
||||
"@nuxt/content": "^3.7.1",
|
||||
"@nuxt/image": "^1.11.0",
|
||||
"@nuxt/ui": "^4.0.1",
|
||||
"@nuxtjs/mdc": "^0.17.4",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@vite-pwa/nuxt": "^1.0.4",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"nuxt": "^4.1.2",
|
||||
"nuxt-llms": "0.1.3",
|
||||
"nuxt-og-image": "^5.1.11",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.9.0",
|
||||
"eslint": "^9.37.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vue-tsc": "^3.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"unimport": "4.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"extends": [
|
||||
"github>nuxt/renovate-config-nuxt"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [{
|
||||
"matchDepTypes": ["resolutions"],
|
||||
"enabled": false
|
||||
}],
|
||||
"postUpdateOptions": ["pnpmDedupe"]
|
||||
"extends": ["github>nuxt/renovate-config-nuxt"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": ["resolutions"],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"postUpdateOptions": ["pnpmDedupe"]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { withLeadingSlash } from 'ufo'
|
||||
import { stringify } from 'minimark/stringify'
|
||||
import { queryCollection } from '@nuxt/content/nitro'
|
||||
import type { Collections } from '@nuxt/content'
|
||||
import { withLeadingSlash } from "ufo";
|
||||
import { stringify } from "minimark/stringify";
|
||||
import { queryCollection } from "@nuxt/content/nitro";
|
||||
import type { Collections } from "@nuxt/content";
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const slug = getRouterParams(event)['slug.md']
|
||||
if (!slug?.endsWith('.md')) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
const slug = getRouterParams(event)["slug.md"];
|
||||
if (!slug?.endsWith(".md")) {
|
||||
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()
|
||||
if (!page) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
|
||||
}
|
||||
const page = await queryCollection(event, "docs" as keyof Collections)
|
||||
.path(path)
|
||||
.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
|
||||
if (page.body.value[0]?.[0] !== 'h1') {
|
||||
page.body.value.unshift(['blockquote', {}, page.description])
|
||||
page.body.value.unshift(['h1', {}, page.title])
|
||||
}
|
||||
// Add title and description to the top of the page if missing
|
||||
if (page.body.value[0]?.[0] !== "h1") {
|
||||
page.body.value.unshift(["blockquote", {}, page.description]);
|
||||
page.body.value.unshift(["h1", {}, page.title]);
|
||||
}
|
||||
|
||||
setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
|
||||
return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
|
||||
})
|
||||
setHeader(event, "Content-Type", "text/markdown; charset=utf-8");
|
||||
return stringify(
|
||||
{ ...page.body, type: "minimark" },
|
||||
{ format: "markdown/html" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
||||
|
||||
26
Projects/kompose/news/.vscode/settings.json
vendored
26
Projects/kompose/news/.vscode/settings.json
vendored
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"mode": "auto"
|
||||
}
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import globals from "globals"
|
||||
import pluginJs from "@eslint/js"
|
||||
import * as tseslint from "typescript-eslint"
|
||||
import globals from "globals";
|
||||
import pluginJs from "@eslint/js";
|
||||
import * as tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config({
|
||||
files: ["src/**/*.{js,mjs,cjs,ts}"],
|
||||
extends: [pluginJs.configs.recommended],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }],
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
})
|
||||
files: ["src/**/*.{js,mjs,cjs,ts}"],
|
||||
extends: [pluginJs.configs.recommended],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { caughtErrors: "none" }],
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
"dev": "watchexec -r -e ts bun run src/index.ts",
|
||||
"build": "rm -rf dist && tsc -b tsconfig.build.json",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"generate": "prisma generate",
|
||||
"generate:sql": "prisma generate --sql && pnpm exec prettier --write prisma/client",
|
||||
"test": "dotenv -e .env.test -- vitest"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./shared": "./src/shared.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "bun prisma/seed.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.7.0",
|
||||
"@trpc/server": "11.0.0-rc.730",
|
||||
"bcryptjs": "^3.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parse": "^5.6.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.10.0",
|
||||
"p-map": "^7.0.3",
|
||||
"superjson": "^2.2.2",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dotenv": "^8.2.3",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.8",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitest/ui": "3.0.5",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.19.0",
|
||||
"globals": "^15.14.0",
|
||||
"prisma": "^6.7.0",
|
||||
"supertest": "^7.0.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
"name": "backend",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
"dev": "watchexec -r -e ts bun run src/index.ts",
|
||||
"build": "rm -rf dist && tsc -b tsconfig.build.json",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"generate": "prisma generate",
|
||||
"generate:sql": "prisma generate --sql && pnpm exec prettier --write prisma/client",
|
||||
"test": "dotenv -e .env.test -- vitest"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./shared": "./src/shared.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "bun prisma/seed.ts"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.7.0",
|
||||
"@trpc/server": "11.0.0-rc.730",
|
||||
"bcryptjs": "^3.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"csv-parse": "^5.6.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.10.0",
|
||||
"p-map": "^7.0.3",
|
||||
"superjson": "^2.2.2",
|
||||
"uuid": "^11.0.5",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dotenv": "^8.2.3",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.8",
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"@types/swagger-ui-express": "^4.1.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitest/ui": "3.0.5",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.19.0",
|
||||
"globals": "^15.14.0",
|
||||
"prisma": "^6.7.0",
|
||||
"supertest": "^7.0.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.22.0",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
export interface $DbEnums {}
|
||||
|
||||
export namespace $DbEnums {
|
||||
type CampaignStatus =
|
||||
| "DRAFT"
|
||||
| "SCHEDULED"
|
||||
| "SENDING"
|
||||
| "COMPLETED"
|
||||
| "CANCELLED";
|
||||
type MessageStatus =
|
||||
| "QUEUED"
|
||||
| "PENDING"
|
||||
| "SENT"
|
||||
| "OPENED"
|
||||
| "CLICKED"
|
||||
| "FAILED"
|
||||
| "RETRYING";
|
||||
type SmtpEncryption = "STARTTLS" | "SSL_TLS" | "NONE";
|
||||
type CampaignStatus =
|
||||
| "DRAFT"
|
||||
| "SCHEDULED"
|
||||
| "SENDING"
|
||||
| "COMPLETED"
|
||||
| "CANCELLED";
|
||||
type MessageStatus =
|
||||
| "QUEUED"
|
||||
| "PENDING"
|
||||
| "SENT"
|
||||
| "OPENED"
|
||||
| "CLICKED"
|
||||
| "FAILED"
|
||||
| "RETRYING";
|
||||
type SmtpEncryption = "STARTTLS" | "SSL_TLS" | "NONE";
|
||||
}
|
||||
|
||||
@@ -4,19 +4,19 @@ import * as $runtime from "../runtime/library";
|
||||
* @param text
|
||||
*/
|
||||
export const countDbSize: (
|
||||
text: string,
|
||||
text: string,
|
||||
) => $runtime.TypedSql<countDbSize.Parameters, countDbSize.Result>;
|
||||
|
||||
export namespace countDbSize {
|
||||
export type Parameters = [text: string];
|
||||
export type Result = {
|
||||
organization_id: string;
|
||||
organization_name: string;
|
||||
campaign_count: bigint | null;
|
||||
template_count: bigint | null;
|
||||
message_count: bigint | null;
|
||||
subscriber_count: bigint | null;
|
||||
list_count: bigint | null;
|
||||
total_size_mb: $runtime.Decimal | null;
|
||||
};
|
||||
export type Parameters = [text: string];
|
||||
export type Result = {
|
||||
organization_id: string;
|
||||
organization_name: string;
|
||||
campaign_count: bigint | null;
|
||||
template_count: bigint | null;
|
||||
message_count: bigint | null;
|
||||
subscriber_count: bigint | null;
|
||||
list_count: bigint | null;
|
||||
total_size_mb: $runtime.Decimal | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -4,15 +4,15 @@ import * as $runtime from "../runtime/library";
|
||||
* @param text
|
||||
*/
|
||||
export const countDistinctRecipients: (
|
||||
text: string,
|
||||
text: string,
|
||||
) => $runtime.TypedSql<
|
||||
countDistinctRecipients.Parameters,
|
||||
countDistinctRecipients.Result
|
||||
countDistinctRecipients.Parameters,
|
||||
countDistinctRecipients.Result
|
||||
>;
|
||||
|
||||
export namespace countDistinctRecipients {
|
||||
export type Parameters = [text: string];
|
||||
export type Result = {
|
||||
count: bigint | null;
|
||||
};
|
||||
export type Parameters = [text: string];
|
||||
export type Result = {
|
||||
count: bigint | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -6,17 +6,17 @@ import * as $runtime from "../runtime/library";
|
||||
* @param timestamp
|
||||
*/
|
||||
export const countDistinctRecipientsInTimeRange: (
|
||||
text: string,
|
||||
timestamp: Date,
|
||||
timestamp: Date,
|
||||
text: string,
|
||||
timestamp: Date,
|
||||
timestamp: Date,
|
||||
) => $runtime.TypedSql<
|
||||
countDistinctRecipientsInTimeRange.Parameters,
|
||||
countDistinctRecipientsInTimeRange.Result
|
||||
countDistinctRecipientsInTimeRange.Parameters,
|
||||
countDistinctRecipientsInTimeRange.Result
|
||||
>;
|
||||
|
||||
export namespace countDistinctRecipientsInTimeRange {
|
||||
export type Parameters = [text: string, timestamp: Date, timestamp: Date];
|
||||
export type Result = {
|
||||
count: bigint | null;
|
||||
};
|
||||
export type Parameters = [text: string, timestamp: Date, timestamp: Date];
|
||||
export type Result = {
|
||||
count: bigint | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
|
||||
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;',
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"use strict";
|
||||
exports.countDbSize = require("./countDbSize.edge.js").countDbSize;
|
||||
exports.countDistinctRecipients =
|
||||
require("./countDistinctRecipients.edge.js").countDistinctRecipients;
|
||||
require("./countDistinctRecipients.edge.js").countDistinctRecipients;
|
||||
exports.countDistinctRecipientsInTimeRange =
|
||||
require("./countDistinctRecipientsInTimeRange.edge.js").countDistinctRecipientsInTimeRange;
|
||||
require("./countDistinctRecipientsInTimeRange.edge.js").countDistinctRecipientsInTimeRange;
|
||||
exports.subscriberGrowthQuery =
|
||||
require("./subscriberGrowthQuery.edge.js").subscriberGrowthQuery;
|
||||
require("./subscriberGrowthQuery.edge.js").subscriberGrowthQuery;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"use strict";
|
||||
exports.countDbSize = require("./countDbSize.js").countDbSize;
|
||||
exports.countDistinctRecipients =
|
||||
require("./countDistinctRecipients.js").countDistinctRecipients;
|
||||
require("./countDistinctRecipients.js").countDistinctRecipients;
|
||||
exports.countDistinctRecipientsInTimeRange =
|
||||
require("./countDistinctRecipientsInTimeRange.js").countDistinctRecipientsInTimeRange;
|
||||
require("./countDistinctRecipientsInTimeRange.js").countDistinctRecipientsInTimeRange;
|
||||
exports.subscriberGrowthQuery =
|
||||
require("./subscriberGrowthQuery.js").subscriberGrowthQuery;
|
||||
require("./subscriberGrowthQuery.js").subscriberGrowthQuery;
|
||||
|
||||
@@ -6,18 +6,18 @@ import * as $runtime from "../runtime/library";
|
||||
* @param timestamp
|
||||
*/
|
||||
export const subscriberGrowthQuery: (
|
||||
text: string,
|
||||
timestamp: Date,
|
||||
timestamp: Date,
|
||||
text: string,
|
||||
timestamp: Date,
|
||||
timestamp: Date,
|
||||
) => $runtime.TypedSql<
|
||||
subscriberGrowthQuery.Parameters,
|
||||
subscriberGrowthQuery.Result
|
||||
subscriberGrowthQuery.Parameters,
|
||||
subscriberGrowthQuery.Result
|
||||
>;
|
||||
|
||||
export namespace subscriberGrowthQuery {
|
||||
export type Parameters = [text: string, timestamp: Date, timestamp: Date];
|
||||
export type Result = {
|
||||
date: Date | null;
|
||||
count: bigint | null;
|
||||
};
|
||||
export type Parameters = [text: string, timestamp: Date, timestamp: Date];
|
||||
export type Result = {
|
||||
date: Date | null;
|
||||
count: bigint | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/edge.js");
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/edge.js";
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"use strict";
|
||||
const { makeTypedQueryFactory: $mkFactory } = require("../runtime/library");
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
/* eslint-disable */
|
||||
import { makeTypedQueryFactory as $mkFactory } from "../runtime/library";
|
||||
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',
|
||||
);
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
import { hashPassword } from "../src/utils/auth"
|
||||
import { prisma } from "../src/utils/prisma"
|
||||
import { SmtpEncryption } from "./client"
|
||||
import dayjs from "dayjs"
|
||||
import { hashPassword } from "../src/utils/auth";
|
||||
import { prisma } from "../src/utils/prisma";
|
||||
import { SmtpEncryption } from "./client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
async function seed() {
|
||||
if (!(await prisma.organization.findFirst())) {
|
||||
await prisma.organization.create({
|
||||
data: {
|
||||
name: "Test Organization",
|
||||
description: "Test Description",
|
||||
GeneralSettings: {
|
||||
create: {},
|
||||
},
|
||||
EmailDeliverySettings: {
|
||||
create: {
|
||||
rateLimit: 100,
|
||||
},
|
||||
},
|
||||
SmtpSettings: {
|
||||
create: {
|
||||
host: "smtp.test.com",
|
||||
port: 587,
|
||||
username: "test",
|
||||
password: "test",
|
||||
encryption: SmtpEncryption.STARTTLS,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!(await prisma.organization.findFirst())) {
|
||||
await prisma.organization.create({
|
||||
data: {
|
||||
name: "Test Organization",
|
||||
description: "Test Description",
|
||||
GeneralSettings: {
|
||||
create: {},
|
||||
},
|
||||
EmailDeliverySettings: {
|
||||
create: {
|
||||
rateLimit: 100,
|
||||
},
|
||||
},
|
||||
SmtpSettings: {
|
||||
create: {
|
||||
host: "smtp.test.com",
|
||||
port: 587,
|
||||
username: "test",
|
||||
password: "test",
|
||||
encryption: SmtpEncryption.STARTTLS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const orgId = (
|
||||
await prisma.organization.findFirst({
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
})
|
||||
)?.id
|
||||
const orgId = (
|
||||
await prisma.organization.findFirst({
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
})
|
||||
)?.id;
|
||||
|
||||
if (!orgId) {
|
||||
throw new Error("not reachable")
|
||||
}
|
||||
if (!orgId) {
|
||||
throw new Error("not reachable");
|
||||
}
|
||||
|
||||
if (!(await prisma.user.findFirst())) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
name: "Admin",
|
||||
email: "admin@example.com",
|
||||
password: await hashPassword("password123"),
|
||||
UserOrganizations: {
|
||||
create: {
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!(await prisma.user.findFirst())) {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
name: "Admin",
|
||||
email: "admin@example.com",
|
||||
password: await hashPassword("password123"),
|
||||
UserOrganizations: {
|
||||
create: {
|
||||
organizationId: orgId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Create 5000 subscribers
|
||||
const subscribers = Array.from({ length: 5000 }, (_, i) => ({
|
||||
name: `Subscriber ${i + 1}`,
|
||||
email: `subscriber${i + 1}@example.com`,
|
||||
organizationId: orgId,
|
||||
createdAt: dayjs().subtract(12, "days").toDate(),
|
||||
}))
|
||||
await prisma.subscriber.createMany({
|
||||
data: subscribers,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
// Then 10 more for each day for 10 days
|
||||
const now = new Date()
|
||||
for (let d = 0; d < 10; d++) {
|
||||
const day = dayjs(now)
|
||||
.subtract(d + 1, "day")
|
||||
.toDate()
|
||||
// Create 5000 subscribers
|
||||
const subscribers = Array.from({ length: 5000 }, (_, i) => ({
|
||||
name: `Subscriber ${i + 1}`,
|
||||
email: `subscriber${i + 1}@example.com`,
|
||||
organizationId: orgId,
|
||||
createdAt: dayjs().subtract(12, "days").toDate(),
|
||||
}));
|
||||
await prisma.subscriber.createMany({
|
||||
data: subscribers,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
// Then 10 more for each day for 10 days
|
||||
const now = new Date();
|
||||
for (let d = 0; d < 10; d++) {
|
||||
const day = dayjs(now)
|
||||
.subtract(d + 1, "day")
|
||||
.toDate();
|
||||
|
||||
const dailySubs = Array.from({ length: 10 }, (_, i) => ({
|
||||
name: `DailySub ${d + 1}-${i + 1}`,
|
||||
email: `dailysub${d + 1}-${i + 1}@example.com`,
|
||||
organizationId: orgId,
|
||||
createdAt: day,
|
||||
updatedAt: day,
|
||||
}))
|
||||
await prisma.subscriber.createMany({
|
||||
data: dailySubs,
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
const dailySubs = Array.from({ length: 10 }, (_, i) => ({
|
||||
name: `DailySub ${d + 1}-${i + 1}`,
|
||||
email: `dailysub${d + 1}-${i + 1}@example.com`,
|
||||
organizationId: orgId,
|
||||
createdAt: day,
|
||||
updatedAt: day,
|
||||
}));
|
||||
await prisma.subscriber.createMany({
|
||||
data: dailySubs,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
seed()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e)
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
import { prisma } from "../utils/prisma"
|
||||
import express, { NextFunction } from "express"
|
||||
import { prisma } from "../utils/prisma";
|
||||
import express, { NextFunction } from "express";
|
||||
|
||||
export const authenticateApiKey = async (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: NextFunction
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const apiKey = req.header("x-api-key")
|
||||
if (!apiKey) {
|
||||
res.status(401).json({ error: "Missing API Key" })
|
||||
return
|
||||
}
|
||||
const apiKey = req.header("x-api-key");
|
||||
if (!apiKey) {
|
||||
res.status(401).json({ error: "Missing API Key" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keyRecord = await prisma.apiKey.findUnique({
|
||||
where: { key: apiKey },
|
||||
select: { id: true, Organization: true },
|
||||
})
|
||||
try {
|
||||
const keyRecord = await prisma.apiKey.findUnique({
|
||||
where: { key: apiKey },
|
||||
select: { id: true, Organization: true },
|
||||
});
|
||||
|
||||
if (!keyRecord) {
|
||||
res.status(401).json({ error: "Invalid API Key" })
|
||||
return
|
||||
}
|
||||
if (!keyRecord) {
|
||||
res.status(401).json({ error: "Invalid API Key" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update lastUsed timestamp asynchronously, don't await
|
||||
prisma.apiKey
|
||||
.update({
|
||||
where: { id: keyRecord.id },
|
||||
data: { lastUsed: new Date() },
|
||||
})
|
||||
.catch((updateError) => {
|
||||
// Log the error but don't block the request
|
||||
console.error(
|
||||
"Failed to update API key lastUsed timestamp",
|
||||
updateError
|
||||
)
|
||||
})
|
||||
// Update lastUsed timestamp asynchronously, don't await
|
||||
prisma.apiKey
|
||||
.update({
|
||||
where: { id: keyRecord.id },
|
||||
data: { lastUsed: new Date() },
|
||||
})
|
||||
.catch((updateError) => {
|
||||
// Log the error but don't block the request
|
||||
console.error(
|
||||
"Failed to update API key lastUsed timestamp",
|
||||
updateError,
|
||||
);
|
||||
});
|
||||
|
||||
req.organization = keyRecord.Organization
|
||||
next()
|
||||
} catch (error) {
|
||||
console.error("Error validating API key", error)
|
||||
res.status(500).json({ error: "Server error" })
|
||||
}
|
||||
}
|
||||
req.organization = keyRecord.Organization;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error("Error validating API key", error);
|
||||
res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +1,165 @@
|
||||
import * as trpcExpress from "@trpc/server/adapters/express"
|
||||
import path from "path"
|
||||
import express from "express"
|
||||
import cors from "cors"
|
||||
import { prisma } from "./utils/prisma"
|
||||
import swaggerUi from "swagger-ui-express"
|
||||
import * as trpcExpress from "@trpc/server/adapters/express";
|
||||
import path from "path";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import { prisma } from "./utils/prisma";
|
||||
import swaggerUi from "swagger-ui-express";
|
||||
|
||||
import { createContext, router } from "./trpc"
|
||||
import { userRouter } from "./user/router"
|
||||
import { listRouter } from "./list/router"
|
||||
import { organizationRouter } from "./organization/router"
|
||||
import { subscriberRouter } from "./subscriber/router"
|
||||
import { templateRouter } from "./template/router"
|
||||
import { campaignRouter } from "./campaign/router"
|
||||
import { messageRouter } from "./message/router"
|
||||
import { settingsRouter } from "./settings/router"
|
||||
import swaggerSpec from "./swagger"
|
||||
import { apiRouter } from "./api/server"
|
||||
import { dashboardRouter } from "./dashboard/router"
|
||||
import { statsRouter } from "./stats/router"
|
||||
import { ONE_PX_PNG } from "./constants"
|
||||
import { createContext, router } from "./trpc";
|
||||
import { userRouter } from "./user/router";
|
||||
import { listRouter } from "./list/router";
|
||||
import { organizationRouter } from "./organization/router";
|
||||
import { subscriberRouter } from "./subscriber/router";
|
||||
import { templateRouter } from "./template/router";
|
||||
import { campaignRouter } from "./campaign/router";
|
||||
import { messageRouter } from "./message/router";
|
||||
import { settingsRouter } from "./settings/router";
|
||||
import swaggerSpec from "./swagger";
|
||||
import { apiRouter } from "./api/server";
|
||||
import { dashboardRouter } from "./dashboard/router";
|
||||
import { statsRouter } from "./stats/router";
|
||||
import { ONE_PX_PNG } from "./constants";
|
||||
|
||||
const appRouter = router({
|
||||
user: userRouter,
|
||||
list: listRouter,
|
||||
organization: organizationRouter,
|
||||
subscriber: subscriberRouter,
|
||||
template: templateRouter,
|
||||
campaign: campaignRouter,
|
||||
message: messageRouter,
|
||||
settings: settingsRouter,
|
||||
dashboard: dashboardRouter,
|
||||
stats: statsRouter,
|
||||
})
|
||||
user: userRouter,
|
||||
list: listRouter,
|
||||
organization: organizationRouter,
|
||||
subscriber: subscriberRouter,
|
||||
template: templateRouter,
|
||||
campaign: campaignRouter,
|
||||
message: messageRouter,
|
||||
settings: settingsRouter,
|
||||
dashboard: dashboardRouter,
|
||||
stats: statsRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
export const app = express()
|
||||
export const app = express();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: ["http://localhost:3000", "http://localhost:4173"],
|
||||
})
|
||||
)
|
||||
app.use(express.json())
|
||||
cors({
|
||||
origin: ["http://localhost:3000", "http://localhost:4173"],
|
||||
}),
|
||||
);
|
||||
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) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const subscriberId = req.query.sid
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const subscriberId = req.query.sid;
|
||||
|
||||
const trackedLink = await prisma.trackedLink.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
const trackedLink = await prisma.trackedLink.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!trackedLink) {
|
||||
res.status(404).send("Link not found")
|
||||
return
|
||||
}
|
||||
if (!trackedLink) {
|
||||
res.status(404).send("Link not found");
|
||||
return;
|
||||
}
|
||||
|
||||
res.redirect(trackedLink.url)
|
||||
res.redirect(trackedLink.url);
|
||||
|
||||
if (subscriberId && typeof subscriberId === "string") {
|
||||
await prisma
|
||||
.$transaction(async (tx) => {
|
||||
// add a new click
|
||||
await tx.click.create({
|
||||
data: {
|
||||
subscriberId,
|
||||
trackedLinkId: trackedLink.id,
|
||||
},
|
||||
})
|
||||
if (subscriberId && typeof subscriberId === "string") {
|
||||
await prisma
|
||||
.$transaction(async (tx) => {
|
||||
// add a new click
|
||||
await tx.click.create({
|
||||
data: {
|
||||
subscriberId,
|
||||
trackedLinkId: trackedLink.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!trackedLink.campaignId) return
|
||||
if (!trackedLink.campaignId) return;
|
||||
|
||||
const message = await tx.message.findFirst({
|
||||
where: {
|
||||
campaignId: trackedLink.campaignId,
|
||||
subscriberId,
|
||||
status: {
|
||||
not: "CLICKED",
|
||||
},
|
||||
},
|
||||
})
|
||||
const message = await tx.message.findFirst({
|
||||
where: {
|
||||
campaignId: trackedLink.campaignId,
|
||||
subscriberId,
|
||||
status: {
|
||||
not: "CLICKED",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!message) return
|
||||
if (!message) return;
|
||||
|
||||
await tx.message.update({
|
||||
where: {
|
||||
id: message.id,
|
||||
},
|
||||
data: {
|
||||
status: "CLICKED",
|
||||
},
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating message status", error)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(404).send("Link not found")
|
||||
}
|
||||
})
|
||||
await tx.message.update({
|
||||
where: {
|
||||
id: message.id,
|
||||
},
|
||||
data: {
|
||||
status: "CLICKED",
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error updating message status", error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(404).send("Link not found");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/img/:id/img.png", async (req, res) => {
|
||||
// Send pixel immediately
|
||||
const pixel = Buffer.from(ONE_PX_PNG, "base64")
|
||||
res.setHeader("Content-Type", "image/png")
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
res.setHeader("Pragma", "no-cache")
|
||||
res.setHeader("Expires", "0")
|
||||
res.end(pixel)
|
||||
// Send pixel immediately
|
||||
const pixel = Buffer.from(ONE_PX_PNG, "base64");
|
||||
res.setHeader("Content-Type", "image/png");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
res.end(pixel);
|
||||
|
||||
const id = req.params.id
|
||||
const id = req.params.id;
|
||||
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const message = await tx.message.findUnique({
|
||||
where: {
|
||||
id,
|
||||
Campaign: {
|
||||
openTracking: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const message = await tx.message.findUnique({
|
||||
where: {
|
||||
id,
|
||||
Campaign: {
|
||||
openTracking: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.status !== "SENT") return
|
||||
if (message.status !== "SENT") return;
|
||||
|
||||
await tx.message.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: "OPENED",
|
||||
},
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error updating message status", error)
|
||||
}
|
||||
})
|
||||
await tx.message.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: "OPENED",
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating message status", error);
|
||||
}
|
||||
});
|
||||
|
||||
app.use("/api", apiRouter)
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
app.use(
|
||||
"/trpc",
|
||||
trpcExpress.createExpressMiddleware({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
})
|
||||
)
|
||||
"/trpc",
|
||||
trpcExpress.createExpressMiddleware({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
}),
|
||||
);
|
||||
|
||||
const staticPath = path.join(__dirname, "..", "..", "web", "dist")
|
||||
const staticPath = path.join(__dirname, "..", "..", "web", "dist");
|
||||
|
||||
// serve SPA content
|
||||
app.use(express.static(staticPath))
|
||||
app.use(express.static(staticPath));
|
||||
|
||||
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
@@ -1,249 +1,249 @@
|
||||
import { z } from "zod"
|
||||
import { authProcedure } from "../trpc"
|
||||
import { prisma } from "../utils/prisma"
|
||||
import { TRPCError } from "@trpc/server"
|
||||
import { paginationSchema } from "../utils/schemas"
|
||||
import { Prisma } from "../../prisma/client"
|
||||
import { resolveProps } from "../utils/pProps"
|
||||
import { z } from "zod";
|
||||
import { authProcedure } from "../trpc";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { paginationSchema } from "../utils/schemas";
|
||||
import { Prisma } from "../../prisma/client";
|
||||
import { resolveProps } from "../utils/pProps";
|
||||
|
||||
export const listCampaigns = authProcedure
|
||||
.input(z.object({ organizationId: z.string() }).merge(paginationSchema))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userOrganization = await prisma.userOrganization.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
})
|
||||
.input(z.object({ organizationId: z.string() }).merge(paginationSchema))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userOrganization = await prisma.userOrganization.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userOrganization) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Organization not found",
|
||||
})
|
||||
}
|
||||
if (!userOrganization) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Organization not found",
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.CampaignWhereInput = {
|
||||
organizationId: input.organizationId,
|
||||
...(input.search
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: input.search, mode: "insensitive" } },
|
||||
{ description: { contains: input.search, mode: "insensitive" } },
|
||||
{ subject: { contains: input.search, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
const where: Prisma.CampaignWhereInput = {
|
||||
organizationId: input.organizationId,
|
||||
...(input.search
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: input.search, mode: "insensitive" } },
|
||||
{ description: { contains: input.search, mode: "insensitive" } },
|
||||
{ subject: { contains: input.search, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const [total, campaigns] = await prisma.$transaction([
|
||||
prisma.campaign.count({ where }),
|
||||
prisma.campaign.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
||||
skip: (input.page - 1) * input.perPage,
|
||||
take: input.perPage,
|
||||
include: {
|
||||
Template: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
CampaignLists: {
|
||||
include: {
|
||||
List: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
Messages: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
const [total, campaigns] = await prisma.$transaction([
|
||||
prisma.campaign.count({ where }),
|
||||
prisma.campaign.findMany({
|
||||
where,
|
||||
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
||||
skip: (input.page - 1) * input.perPage,
|
||||
take: input.perPage,
|
||||
include: {
|
||||
Template: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
CampaignLists: {
|
||||
include: {
|
||||
List: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
Messages: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / input.perPage)
|
||||
const totalPages = Math.ceil(total / input.perPage);
|
||||
|
||||
return {
|
||||
campaigns,
|
||||
pagination: {
|
||||
total,
|
||||
totalPages,
|
||||
page: input.page,
|
||||
perPage: input.perPage,
|
||||
hasMore: input.page < totalPages,
|
||||
},
|
||||
}
|
||||
})
|
||||
return {
|
||||
campaigns,
|
||||
pagination: {
|
||||
total,
|
||||
totalPages,
|
||||
page: input.page,
|
||||
perPage: input.perPage,
|
||||
hasMore: input.page < totalPages,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const getCampaign = authProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userOrganization = await prisma.userOrganization.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
organizationId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const userOrganization = await prisma.userOrganization.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userOrganization) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Organization not found",
|
||||
})
|
||||
}
|
||||
if (!userOrganization) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Organization not found",
|
||||
});
|
||||
}
|
||||
|
||||
const campaign = await prisma.campaign.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
include: {
|
||||
Template: true,
|
||||
CampaignLists: {
|
||||
include: {
|
||||
List: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const campaign = await prisma.campaign.findFirst({
|
||||
where: {
|
||||
id: input.id,
|
||||
organizationId: input.organizationId,
|
||||
},
|
||||
include: {
|
||||
Template: true,
|
||||
CampaignLists: {
|
||||
include: {
|
||||
List: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!campaign) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Campaign not found",
|
||||
})
|
||||
}
|
||||
if (!campaign) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Campaign not found",
|
||||
});
|
||||
}
|
||||
|
||||
const listSubscribers = await prisma.listSubscriber.findMany({
|
||||
where: {
|
||||
listId: {
|
||||
in: campaign.CampaignLists.map((cl) => cl.listId),
|
||||
},
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
distinct: ["subscriberId"],
|
||||
})
|
||||
const listSubscribers = await prisma.listSubscriber.findMany({
|
||||
where: {
|
||||
listId: {
|
||||
in: campaign.CampaignLists.map((cl) => cl.listId),
|
||||
},
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
distinct: ["subscriberId"],
|
||||
});
|
||||
|
||||
// Add the count to each list for backward compatibility
|
||||
const campaignWithCounts = {
|
||||
...campaign,
|
||||
CampaignLists: await Promise.all(
|
||||
campaign.CampaignLists.map(async (cl) => {
|
||||
const count = await prisma.listSubscriber.count({
|
||||
where: {
|
||||
listId: cl.listId,
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
})
|
||||
// Add the count to each list for backward compatibility
|
||||
const campaignWithCounts = {
|
||||
...campaign,
|
||||
CampaignLists: await Promise.all(
|
||||
campaign.CampaignLists.map(async (cl) => {
|
||||
const count = await prisma.listSubscriber.count({
|
||||
where: {
|
||||
listId: cl.listId,
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...cl,
|
||||
List: {
|
||||
...cl.List,
|
||||
_count: {
|
||||
ListSubscribers: count,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
),
|
||||
// Add the unique subscriber count directly to the campaign object
|
||||
uniqueRecipientCount: listSubscribers.length,
|
||||
}
|
||||
return {
|
||||
...cl,
|
||||
List: {
|
||||
...cl.List,
|
||||
_count: {
|
||||
ListSubscribers: count,
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
),
|
||||
// Add the unique subscriber count directly to the campaign object
|
||||
uniqueRecipientCount: listSubscribers.length,
|
||||
};
|
||||
|
||||
const promises = {
|
||||
totalMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
},
|
||||
}),
|
||||
queuedMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "QUEUED",
|
||||
},
|
||||
}),
|
||||
pendingMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
}),
|
||||
sentMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
in: ["SENT", "OPENED", "CLICKED"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
failedMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "FAILED",
|
||||
},
|
||||
}),
|
||||
processed: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
not: "QUEUED",
|
||||
},
|
||||
},
|
||||
}),
|
||||
clicked: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "CLICKED",
|
||||
},
|
||||
}),
|
||||
opened: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
in: ["OPENED", "CLICKED"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
const promises = {
|
||||
totalMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
},
|
||||
}),
|
||||
queuedMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "QUEUED",
|
||||
},
|
||||
}),
|
||||
pendingMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "PENDING",
|
||||
},
|
||||
}),
|
||||
sentMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
in: ["SENT", "OPENED", "CLICKED"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
failedMessages: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "FAILED",
|
||||
},
|
||||
}),
|
||||
processed: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
not: "QUEUED",
|
||||
},
|
||||
},
|
||||
}),
|
||||
clicked: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: "CLICKED",
|
||||
},
|
||||
}),
|
||||
opened: prisma.message.count({
|
||||
where: {
|
||||
campaignId: campaign.id,
|
||||
status: {
|
||||
in: ["OPENED", "CLICKED"],
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const result = await resolveProps(promises)
|
||||
const result = await resolveProps(promises);
|
||||
|
||||
return {
|
||||
campaign: campaignWithCounts,
|
||||
stats: {
|
||||
totalMessages: result.totalMessages,
|
||||
queuedMessages: result.queuedMessages,
|
||||
pendingMessages: result.pendingMessages,
|
||||
sentMessages: result.sentMessages,
|
||||
failedMessages: result.failedMessages,
|
||||
processed: result.processed,
|
||||
clicked: result.clicked,
|
||||
opened: result.opened,
|
||||
clickRate:
|
||||
result.sentMessages > 0
|
||||
? (result.clicked / result.sentMessages) * 100
|
||||
: 0,
|
||||
openRate:
|
||||
result.sentMessages > 0
|
||||
? (result.opened / result.sentMessages) * 100
|
||||
: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
return {
|
||||
campaign: campaignWithCounts,
|
||||
stats: {
|
||||
totalMessages: result.totalMessages,
|
||||
queuedMessages: result.queuedMessages,
|
||||
pendingMessages: result.pendingMessages,
|
||||
sentMessages: result.sentMessages,
|
||||
failedMessages: result.failedMessages,
|
||||
processed: result.processed,
|
||||
clicked: result.clicked,
|
||||
opened: result.opened,
|
||||
clickRate:
|
||||
result.sentMessages > 0
|
||||
? (result.clicked / result.sentMessages) * 100
|
||||
: 0,
|
||||
openRate:
|
||||
result.sentMessages > 0
|
||||
? (result.opened / result.sentMessages) * 100
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { router } from "../trpc"
|
||||
import { router } from "../trpc";
|
||||
import {
|
||||
createCampaign,
|
||||
updateCampaign,
|
||||
deleteCampaign,
|
||||
startCampaign,
|
||||
cancelCampaign,
|
||||
sendTestEmail,
|
||||
duplicateCampaign,
|
||||
} from "./mutation"
|
||||
import { getCampaign, listCampaigns } from "./query"
|
||||
createCampaign,
|
||||
updateCampaign,
|
||||
deleteCampaign,
|
||||
startCampaign,
|
||||
cancelCampaign,
|
||||
sendTestEmail,
|
||||
duplicateCampaign,
|
||||
} from "./mutation";
|
||||
import { getCampaign, listCampaigns } from "./query";
|
||||
|
||||
export const campaignRouter = router({
|
||||
create: createCampaign,
|
||||
update: updateCampaign,
|
||||
delete: deleteCampaign,
|
||||
get: getCampaign,
|
||||
list: listCampaigns,
|
||||
start: startCampaign,
|
||||
cancel: cancelCampaign,
|
||||
sendTestEmail,
|
||||
duplicate: duplicateCampaign,
|
||||
})
|
||||
create: createCampaign,
|
||||
update: updateCampaign,
|
||||
delete: deleteCampaign,
|
||||
get: getCampaign,
|
||||
list: listCampaigns,
|
||||
start: startCampaign,
|
||||
cancel: cancelCampaign,
|
||||
sendTestEmail,
|
||||
duplicate: duplicateCampaign,
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { z } from "zod"
|
||||
import { z } from "zod";
|
||||
|
||||
export const env = z
|
||||
.object({
|
||||
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
|
||||
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
|
||||
})
|
||||
.parse(process.env)
|
||||
.object({
|
||||
JWT_SECRET: z.string().min(1, "JWT_SECRET is required"),
|
||||
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),
|
||||
})
|
||||
.parse(process.env);
|
||||
|
||||
export const ONE_PX_PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
import cron from "node-cron"
|
||||
import { sendMessagesCron } from "./sendMessages"
|
||||
import { dailyMaintenanceCron } from "./dailyMaintenance"
|
||||
import { processQueuedCampaigns } from "./processQueuedCampaigns"
|
||||
import cron from "node-cron";
|
||||
import { sendMessagesCron } from "./sendMessages";
|
||||
import { dailyMaintenanceCron } from "./dailyMaintenance";
|
||||
import { processQueuedCampaigns } from "./processQueuedCampaigns";
|
||||
|
||||
type CronJob = {
|
||||
name: string
|
||||
schedule: string
|
||||
job: () => Promise<void>
|
||||
enabled: boolean
|
||||
}
|
||||
name: string;
|
||||
schedule: string;
|
||||
job: () => Promise<void>;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const sendMessagesJob: CronJob = {
|
||||
name: "send-queued-messages",
|
||||
schedule: "*/5 * * * * *", // Runs every 5 seconds
|
||||
job: sendMessagesCron,
|
||||
enabled: true,
|
||||
}
|
||||
name: "send-queued-messages",
|
||||
schedule: "*/5 * * * * *", // Runs every 5 seconds
|
||||
job: sendMessagesCron,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const dailyMaintenanceJob: CronJob = {
|
||||
name: "daily-maintenance",
|
||||
schedule: "0 0 * * *", // Runs daily at midnight
|
||||
job: dailyMaintenanceCron,
|
||||
enabled: true,
|
||||
}
|
||||
name: "daily-maintenance",
|
||||
schedule: "0 0 * * *", // Runs daily at midnight
|
||||
job: dailyMaintenanceCron,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const processQueuedCampaignsJob: CronJob = {
|
||||
name: "process-queued-campaigns",
|
||||
schedule: "* * * * * *", // Runs every second
|
||||
job: processQueuedCampaigns,
|
||||
enabled: true,
|
||||
}
|
||||
name: "process-queued-campaigns",
|
||||
schedule: "* * * * * *", // Runs every second
|
||||
job: processQueuedCampaigns,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const cronJobs: CronJob[] = [
|
||||
sendMessagesJob,
|
||||
dailyMaintenanceJob,
|
||||
processQueuedCampaignsJob,
|
||||
]
|
||||
sendMessagesJob,
|
||||
dailyMaintenanceJob,
|
||||
processQueuedCampaignsJob,
|
||||
];
|
||||
|
||||
export const initializeCronJobs = () => {
|
||||
const scheduledJobs = cronJobs
|
||||
.filter((job) => job.enabled)
|
||||
.map((job) => {
|
||||
const task = cron.schedule(job.schedule, job.job)
|
||||
console.log(
|
||||
`Cron job '${job.name}' scheduled with cron expression: ${job.schedule}`
|
||||
)
|
||||
return { name: job.name, task }
|
||||
})
|
||||
const scheduledJobs = cronJobs
|
||||
.filter((job) => job.enabled)
|
||||
.map((job) => {
|
||||
const task = cron.schedule(job.schedule, job.job);
|
||||
console.log(
|
||||
`Cron job '${job.name}' scheduled with cron expression: ${job.schedule}`,
|
||||
);
|
||||
return { name: job.name, task };
|
||||
});
|
||||
|
||||
console.log(`${scheduledJobs.length} cron jobs initialized`)
|
||||
console.log(`${scheduledJobs.length} cron jobs initialized`);
|
||||
|
||||
return {
|
||||
jobs: scheduledJobs,
|
||||
stop: () => scheduledJobs.forEach(({ task }) => task.stop()),
|
||||
}
|
||||
}
|
||||
return {
|
||||
jobs: scheduledJobs,
|
||||
stop: () => scheduledJobs.forEach(({ task }) => task.stop()),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
const runningJobs = new Map<string, boolean>()
|
||||
const runningJobs = new Map<string, boolean>();
|
||||
|
||||
/**
|
||||
* A wrapper for cron jobs
|
||||
*/
|
||||
export function cronJob(name: string, cronFn: () => Promise<void>) {
|
||||
return async () => {
|
||||
if (runningJobs.get(name)) {
|
||||
return
|
||||
}
|
||||
return async () => {
|
||||
if (runningJobs.get(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
runningJobs.set(name, true)
|
||||
runningJobs.set(name, true);
|
||||
|
||||
try {
|
||||
await cronFn()
|
||||
} catch (error) {
|
||||
console.error("Cron Error:", `[${name}]`, error)
|
||||
} finally {
|
||||
runningJobs.set(name, false)
|
||||
}
|
||||
}
|
||||
try {
|
||||
await cronFn();
|
||||
} catch (error) {
|
||||
console.error("Cron Error:", `[${name}]`, error);
|
||||
} finally {
|
||||
runningJobs.set(name, false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
import { cronJob } from "./cron.utils"
|
||||
import { prisma } from "../utils/prisma"
|
||||
import dayjs from "dayjs"
|
||||
import { cronJob } from "./cron.utils";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const dailyMaintenanceCron = cronJob("daily-maintenance", async () => {
|
||||
const organizations = await prisma.organization.findMany({
|
||||
include: {
|
||||
GeneralSettings: true,
|
||||
},
|
||||
})
|
||||
const organizations = await prisma.organization.findMany({
|
||||
include: {
|
||||
GeneralSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
let totalDeletedMessages = 0
|
||||
let totalDeletedMessages = 0;
|
||||
|
||||
for (const org of organizations) {
|
||||
const cleanupIntervalDays = org.GeneralSettings?.cleanupInterval ?? 30
|
||||
const cleanupOlderThanDate = dayjs()
|
||||
.subtract(cleanupIntervalDays, "days")
|
||||
.toDate()
|
||||
for (const org of organizations) {
|
||||
const cleanupIntervalDays = org.GeneralSettings?.cleanupInterval ?? 30;
|
||||
const cleanupOlderThanDate = dayjs()
|
||||
.subtract(cleanupIntervalDays, "days")
|
||||
.toDate();
|
||||
|
||||
try {
|
||||
const messagesToClean = await prisma.message.findMany({
|
||||
where: {
|
||||
Campaign: {
|
||||
organizationId: org.id,
|
||||
},
|
||||
status: {
|
||||
in: ["SENT", "OPENED", "CLICKED", "FAILED"],
|
||||
},
|
||||
createdAt: {
|
||||
lt: cleanupOlderThanDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
try {
|
||||
const messagesToClean = await prisma.message.findMany({
|
||||
where: {
|
||||
Campaign: {
|
||||
organizationId: org.id,
|
||||
},
|
||||
status: {
|
||||
in: ["SENT", "OPENED", "CLICKED", "FAILED"],
|
||||
},
|
||||
createdAt: {
|
||||
lt: cleanupOlderThanDate,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.message.updateMany({
|
||||
data: {
|
||||
content: null,
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
in: messagesToClean.map((msg) => msg.id),
|
||||
},
|
||||
},
|
||||
})
|
||||
await prisma.message.updateMany({
|
||||
data: {
|
||||
content: null,
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
in: messagesToClean.map((msg) => msg.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (messagesToClean.length > 0) {
|
||||
console.log(
|
||||
`Daily maintenance for org ${org.id}: Deleted ${messagesToClean.length} messages older than ${cleanupIntervalDays} days.`
|
||||
)
|
||||
totalDeletedMessages += messagesToClean.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting messages for org ${org.id}: ${error}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (messagesToClean.length > 0) {
|
||||
console.log(
|
||||
`Daily maintenance for org ${org.id}: Deleted ${messagesToClean.length} messages older than ${cleanupIntervalDays} days.`,
|
||||
);
|
||||
totalDeletedMessages += messagesToClean.length;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting messages for org ${org.id}: ${error}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalDeletedMessages > 0) {
|
||||
console.log(
|
||||
`Daily maintenance job finished. Total deleted messages: ${totalDeletedMessages}.`
|
||||
)
|
||||
} else {
|
||||
console.log("Daily maintenance job finished. No messages to delete.")
|
||||
}
|
||||
})
|
||||
if (totalDeletedMessages > 0) {
|
||||
console.log(
|
||||
`Daily maintenance job finished. Total deleted messages: ${totalDeletedMessages}.`,
|
||||
);
|
||||
} else {
|
||||
console.log("Daily maintenance job finished. No messages to delete.");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,285 +1,285 @@
|
||||
import { prisma } from "../utils/prisma"
|
||||
import { LinkTracker } from "../lib/LinkTracker"
|
||||
import { v4 as uuidV4 } from "uuid"
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { LinkTracker } from "../lib/LinkTracker";
|
||||
import { v4 as uuidV4 } from "uuid";
|
||||
import {
|
||||
replacePlaceholders,
|
||||
PlaceholderDataKey,
|
||||
} from "../utils/placeholder-parser"
|
||||
import pMap from "p-map"
|
||||
import { Subscriber, Prisma, SubscriberMetadata } from "../../prisma/client"
|
||||
import { cronJob } from "./cron.utils"
|
||||
replacePlaceholders,
|
||||
PlaceholderDataKey,
|
||||
} from "../utils/placeholder-parser";
|
||||
import pMap from "p-map";
|
||||
import { Subscriber, Prisma, SubscriberMetadata } from "../../prisma/client";
|
||||
import { cronJob } from "./cron.utils";
|
||||
|
||||
// TODO: Make this a config
|
||||
const BATCH_SIZE = 100
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
async function getSubscribersForCampaign(
|
||||
campaignId: string,
|
||||
selectedListIds: string[]
|
||||
campaignId: string,
|
||||
selectedListIds: string[],
|
||||
): Promise<Map<string, Subscriber & { Metadata: SubscriberMetadata[] }>> {
|
||||
if (selectedListIds.length === 0) {
|
||||
return new Map()
|
||||
}
|
||||
if (selectedListIds.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const subscribers = await prisma.subscriber.findMany({
|
||||
where: {
|
||||
Messages: { none: { campaignId } },
|
||||
ListSubscribers: {
|
||||
some: {
|
||||
listId: { in: selectedListIds },
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: BATCH_SIZE,
|
||||
include: {
|
||||
Metadata: true,
|
||||
},
|
||||
})
|
||||
const subscribers = await prisma.subscriber.findMany({
|
||||
where: {
|
||||
Messages: { none: { campaignId } },
|
||||
ListSubscribers: {
|
||||
some: {
|
||||
listId: { in: selectedListIds },
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: BATCH_SIZE,
|
||||
include: {
|
||||
Metadata: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!subscribers.length) return new Map()
|
||||
if (!subscribers.length) return new Map();
|
||||
|
||||
const subscribersMap = new Map<
|
||||
string,
|
||||
Subscriber & { Metadata: SubscriberMetadata[] }
|
||||
>()
|
||||
await pMap(subscribers, async (subscriber) => {
|
||||
subscribersMap.set(subscriber.id, subscriber)
|
||||
})
|
||||
const subscribersMap = new Map<
|
||||
string,
|
||||
Subscriber & { Metadata: SubscriberMetadata[] }
|
||||
>();
|
||||
await pMap(subscribers, async (subscriber) => {
|
||||
subscribersMap.set(subscriber.id, subscriber);
|
||||
});
|
||||
|
||||
return subscribersMap
|
||||
return subscribersMap;
|
||||
}
|
||||
|
||||
const logged = {
|
||||
noQueuedCampaigns: false,
|
||||
missingCampaignData: false,
|
||||
noSubscribers: false,
|
||||
missingCampaignContent: false,
|
||||
missingCampaignSubject: false,
|
||||
errorProcessingCampaign: false,
|
||||
}
|
||||
noQueuedCampaigns: false,
|
||||
missingCampaignData: false,
|
||||
noSubscribers: false,
|
||||
missingCampaignContent: false,
|
||||
missingCampaignSubject: false,
|
||||
errorProcessingCampaign: false,
|
||||
};
|
||||
|
||||
const oneTimeLogger = (key: keyof typeof logged, ...messages: unknown[]) => {
|
||||
if (!logged[key]) {
|
||||
console.log(...messages)
|
||||
logged[key] = true
|
||||
}
|
||||
}
|
||||
if (!logged[key]) {
|
||||
console.log(...messages);
|
||||
logged[key] = true;
|
||||
}
|
||||
};
|
||||
|
||||
const turnOnLogger = (key: keyof typeof logged) => {
|
||||
logged[key] = false
|
||||
}
|
||||
logged[key] = false;
|
||||
};
|
||||
|
||||
export const processQueuedCampaigns = cronJob(
|
||||
"process-queued-campaigns",
|
||||
async () => {
|
||||
const queuedCampaigns = await prisma.campaign.findMany({
|
||||
where: {
|
||||
status: "CREATING",
|
||||
},
|
||||
include: {
|
||||
CampaignLists: {
|
||||
select: { listId: true },
|
||||
},
|
||||
Organization: {
|
||||
include: {
|
||||
GeneralSettings: true,
|
||||
SmtpSettings: true,
|
||||
},
|
||||
},
|
||||
Template: true,
|
||||
},
|
||||
})
|
||||
"process-queued-campaigns",
|
||||
async () => {
|
||||
const queuedCampaigns = await prisma.campaign.findMany({
|
||||
where: {
|
||||
status: "CREATING",
|
||||
},
|
||||
include: {
|
||||
CampaignLists: {
|
||||
select: { listId: true },
|
||||
},
|
||||
Organization: {
|
||||
include: {
|
||||
GeneralSettings: true,
|
||||
SmtpSettings: true,
|
||||
},
|
||||
},
|
||||
Template: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (queuedCampaigns.length === 0) {
|
||||
oneTimeLogger(
|
||||
"noQueuedCampaigns",
|
||||
"Cron job: No queued campaigns to process."
|
||||
)
|
||||
return
|
||||
}
|
||||
if (queuedCampaigns.length === 0) {
|
||||
oneTimeLogger(
|
||||
"noQueuedCampaigns",
|
||||
"Cron job: No queued campaigns to process.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
turnOnLogger("noQueuedCampaigns")
|
||||
turnOnLogger("noQueuedCampaigns");
|
||||
|
||||
for (const campaign of queuedCampaigns) {
|
||||
try {
|
||||
if (
|
||||
!campaign ||
|
||||
!campaign.content ||
|
||||
!campaign.subject ||
|
||||
!campaign.Organization ||
|
||||
!campaign.Organization.GeneralSettings?.baseURL
|
||||
) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignData",
|
||||
`Cron job: Campaign ${campaign.id} is missing required data (content, subject, organization, or baseURL). Skipping.`
|
||||
)
|
||||
// Optionally, update status to FAILED or similar
|
||||
// await prisma.campaign.update({ where: { id: campaign.id }, data: { status: 'FAILED', statusReason: 'Missing critical data for processing' } });
|
||||
continue
|
||||
}
|
||||
for (const campaign of queuedCampaigns) {
|
||||
try {
|
||||
if (
|
||||
!campaign ||
|
||||
!campaign.content ||
|
||||
!campaign.subject ||
|
||||
!campaign.Organization ||
|
||||
!campaign.Organization.GeneralSettings?.baseURL
|
||||
) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignData",
|
||||
`Cron job: Campaign ${campaign.id} is missing required data (content, subject, organization, or baseURL). Skipping.`,
|
||||
);
|
||||
// Optionally, update status to FAILED or similar
|
||||
// await prisma.campaign.update({ where: { id: campaign.id }, data: { status: 'FAILED', statusReason: 'Missing critical data for processing' } });
|
||||
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(
|
||||
campaign.id,
|
||||
selectedListIds
|
||||
)
|
||||
if (allSubscribersMap.size === 0) {
|
||||
oneTimeLogger(
|
||||
"noSubscribers",
|
||||
`Cron job: Campaign ${campaign.id} has no subscribers. Skipping.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
const allSubscribersMap = await getSubscribersForCampaign(
|
||||
campaign.id,
|
||||
selectedListIds,
|
||||
);
|
||||
if (allSubscribersMap.size === 0) {
|
||||
oneTimeLogger(
|
||||
"noSubscribers",
|
||||
`Cron job: Campaign ${campaign.id} has no subscribers. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
turnOnLogger("noSubscribers")
|
||||
turnOnLogger("noSubscribers");
|
||||
|
||||
const messageSubscriberIds = (
|
||||
await prisma.message.findMany({
|
||||
where: { campaignId: campaign.id },
|
||||
select: { subscriberId: true },
|
||||
})
|
||||
).map((m) => m.subscriberId)
|
||||
const subscribersWithMessage = new Set(messageSubscriberIds)
|
||||
const messageSubscriberIds = (
|
||||
await prisma.message.findMany({
|
||||
where: { campaignId: campaign.id },
|
||||
select: { subscriberId: true },
|
||||
})
|
||||
).map((m) => m.subscriberId);
|
||||
const subscribersWithMessage = new Set(messageSubscriberIds);
|
||||
|
||||
const subscribersToProcess = Array.from(
|
||||
allSubscribersMap.values()
|
||||
).filter((sub) => !subscribersWithMessage.has(sub.id))
|
||||
const subscribersToProcess = Array.from(
|
||||
allSubscribersMap.values(),
|
||||
).filter((sub) => !subscribersWithMessage.has(sub.id));
|
||||
|
||||
if (subscribersToProcess.length === 0) {
|
||||
continue
|
||||
}
|
||||
if (subscribersToProcess.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const linkTracker = new LinkTracker(tx)
|
||||
const messagesToCreate: Prisma.MessageCreateManyInput[] = []
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const linkTracker = new LinkTracker(tx);
|
||||
const messagesToCreate: Prisma.MessageCreateManyInput[] = [];
|
||||
|
||||
for (const subscriber of subscribersToProcess) {
|
||||
const messageId = uuidV4()
|
||||
if (!campaign.content) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignContent",
|
||||
`Cron job: Campaign ${campaign.id} has no content. Skipping.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
for (const subscriber of subscribersToProcess) {
|
||||
const messageId = uuidV4();
|
||||
if (!campaign.content) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignContent",
|
||||
`Cron job: Campaign ${campaign.id} has no content. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
turnOnLogger("missingCampaignContent")
|
||||
turnOnLogger("missingCampaignContent");
|
||||
|
||||
let emailContent = campaign.Template
|
||||
? campaign.Template.content.replace(
|
||||
/{{content}}/g,
|
||||
campaign.content
|
||||
)
|
||||
: campaign.content
|
||||
let emailContent = campaign.Template
|
||||
? campaign.Template.content.replace(
|
||||
/{{content}}/g,
|
||||
campaign.content,
|
||||
)
|
||||
: campaign.content;
|
||||
|
||||
if (!campaign.subject) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignSubject",
|
||||
`Cron job: Campaign ${campaign.id} has no subject. Skipping.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (!campaign.subject) {
|
||||
oneTimeLogger(
|
||||
"missingCampaignSubject",
|
||||
`Cron job: Campaign ${campaign.id} has no subject. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
turnOnLogger("missingCampaignSubject")
|
||||
turnOnLogger("missingCampaignSubject");
|
||||
|
||||
const placeholderData: Partial<
|
||||
Record<PlaceholderDataKey, string>
|
||||
> = {
|
||||
"subscriber.email": subscriber.email,
|
||||
"campaign.name": campaign.title,
|
||||
"campaign.subject": campaign.subject,
|
||||
"organization.name": campaign.Organization.name,
|
||||
unsubscribe_link: `${generalSettings.baseURL}/unsubscribe?sid=${subscriber.id}&cid=${campaign.id}&mid=${messageId}`,
|
||||
current_date: new Date().toLocaleDateString("en-CA"),
|
||||
}
|
||||
const placeholderData: Partial<
|
||||
Record<PlaceholderDataKey, string>
|
||||
> = {
|
||||
"subscriber.email": subscriber.email,
|
||||
"campaign.name": campaign.title,
|
||||
"campaign.subject": campaign.subject,
|
||||
"organization.name": campaign.Organization.name,
|
||||
unsubscribe_link: `${generalSettings.baseURL}/unsubscribe?sid=${subscriber.id}&cid=${campaign.id}&mid=${messageId}`,
|
||||
current_date: new Date().toLocaleDateString("en-CA"),
|
||||
};
|
||||
|
||||
if (campaign.openTracking) {
|
||||
emailContent += `<img src="${generalSettings.baseURL}/img/${messageId}/img.png" alt="" width="1" height="1" style="display:none" />`
|
||||
}
|
||||
if (campaign.openTracking) {
|
||||
emailContent += `<img src="${generalSettings.baseURL}/img/${messageId}/img.png" alt="" width="1" height="1" style="display:none" />`;
|
||||
}
|
||||
|
||||
if (subscriber.name) {
|
||||
placeholderData["subscriber.name"] = subscriber.name
|
||||
}
|
||||
if (subscriber.Metadata) {
|
||||
for (const meta of subscriber.Metadata) {
|
||||
placeholderData[`subscriber.metadata.${meta.key}`] =
|
||||
meta.value
|
||||
}
|
||||
}
|
||||
if (subscriber.name) {
|
||||
placeholderData["subscriber.name"] = subscriber.name;
|
||||
}
|
||||
if (subscriber.Metadata) {
|
||||
for (const meta of subscriber.Metadata) {
|
||||
placeholderData[`subscriber.metadata.${meta.key}`] =
|
||||
meta.value;
|
||||
}
|
||||
}
|
||||
|
||||
emailContent = replacePlaceholders(emailContent, placeholderData)
|
||||
emailContent = replacePlaceholders(emailContent, placeholderData);
|
||||
|
||||
if (!generalSettings.baseURL) {
|
||||
console.error(
|
||||
`Cron job: Campaign ${campaign.id} has no baseURL. Skipping.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (!generalSettings.baseURL) {
|
||||
console.error(
|
||||
`Cron job: Campaign ${campaign.id} has no baseURL. Skipping.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { content: finalContent } =
|
||||
await linkTracker.replaceMessageContentWithTrackedLinks(
|
||||
emailContent,
|
||||
campaign.id,
|
||||
generalSettings.baseURL
|
||||
)
|
||||
const { content: finalContent } =
|
||||
await linkTracker.replaceMessageContentWithTrackedLinks(
|
||||
emailContent,
|
||||
campaign.id,
|
||||
generalSettings.baseURL,
|
||||
);
|
||||
|
||||
messagesToCreate.push({
|
||||
id: messageId,
|
||||
campaignId: campaign.id,
|
||||
subscriberId: subscriber.id,
|
||||
content: finalContent,
|
||||
status: "QUEUED",
|
||||
})
|
||||
}
|
||||
messagesToCreate.push({
|
||||
id: messageId,
|
||||
campaignId: campaign.id,
|
||||
subscriberId: subscriber.id,
|
||||
content: finalContent,
|
||||
status: "QUEUED",
|
||||
});
|
||||
}
|
||||
|
||||
if (messagesToCreate.length > 0) {
|
||||
await tx.message.createMany({
|
||||
data: messagesToCreate,
|
||||
})
|
||||
if (messagesToCreate.length > 0) {
|
||||
await tx.message.createMany({
|
||||
data: messagesToCreate,
|
||||
});
|
||||
|
||||
const subscribersLeft = await tx.subscriber.count({
|
||||
where: {
|
||||
Messages: { none: { campaignId: campaign.id } },
|
||||
ListSubscribers: {
|
||||
some: {
|
||||
listId: { in: selectedListIds },
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const subscribersLeft = await tx.subscriber.count({
|
||||
where: {
|
||||
Messages: { none: { campaignId: campaign.id } },
|
||||
ListSubscribers: {
|
||||
some: {
|
||||
listId: { in: selectedListIds },
|
||||
unsubscribedAt: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (subscribersLeft === 0) {
|
||||
await tx.campaign.update({
|
||||
where: { id: campaign.id },
|
||||
data: { status: "SENDING" },
|
||||
})
|
||||
}
|
||||
if (subscribersLeft === 0) {
|
||||
await tx.campaign.update({
|
||||
where: { id: campaign.id },
|
||||
data: { status: "SENDING" },
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Cron job: Created ${messagesToCreate.length} messages for campaign ${campaign.id}.`
|
||||
)
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 }
|
||||
) // End transaction
|
||||
console.log(
|
||||
`Cron job: Created ${messagesToCreate.length} messages for campaign ${campaign.id}.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
); // End transaction
|
||||
|
||||
turnOnLogger("errorProcessingCampaign")
|
||||
} catch (error) {
|
||||
oneTimeLogger(
|
||||
"errorProcessingCampaign",
|
||||
`Cron job: Error processing campaign ${campaign.id}:`,
|
||||
error
|
||||
)
|
||||
// Optionally, mark campaign as FAILED
|
||||
// await prisma.campaign.update({ where: { id: basicCampaignInfo.id }, data: { status: 'FAILED', statusReason: error.message }});
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
turnOnLogger("errorProcessingCampaign");
|
||||
} catch (error) {
|
||||
oneTimeLogger(
|
||||
"errorProcessingCampaign",
|
||||
`Cron job: Error processing campaign ${campaign.id}:`,
|
||||
error,
|
||||
);
|
||||
// Optionally, mark campaign as FAILED
|
||||
// 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
Reference in New Issue
Block a user