diff --git a/package-lock.json b/package-lock.json
index 54e7176c3..bd26aeae3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"axios": "^0.24.0",
+ "dayjs": "^1.10.8",
"discord.js": "^13.2.0",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
@@ -31,6 +32,7 @@
"node-fetch": "^3.0.0",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^2.4.0",
+ "query-string": "^7.1.1",
"sass": "^1.42.1",
"size-limit": "^7.0.4",
"svelte": "^3.42.6",
@@ -904,14 +906,20 @@
}
},
"node_modules/data-uri-to-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
- "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
+ "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
"dev": true,
"engines": {
- "node": ">= 6"
+ "node": ">= 12"
}
},
+ "node_modules/dayjs": {
+ "version": "1.10.8",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.8.tgz",
+ "integrity": "sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==",
+ "dev": true
+ },
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@@ -929,6 +937,15 @@
}
}
},
+ "node_modules/decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1008,15 +1025,23 @@
}
},
"node_modules/discord.js/node_modules/node-fetch": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
- "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
}
},
"node_modules/doctrine": {
@@ -1675,9 +1700,9 @@
}
},
"node_modules/fetch-blob": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz",
- "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==",
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz",
+ "integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==",
"dev": true,
"funding": [
{
@@ -1690,6 +1715,7 @@
}
],
"dependencies": {
+ "node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
@@ -1720,6 +1746,15 @@
"node": ">=8"
}
},
+ "node_modules/filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -1740,9 +1775,9 @@
"dev": true
},
"node_modules/follow-redirects": {
- "version": "1.14.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
- "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"dev": true,
"funding": [
{
@@ -1759,6 +1794,18 @@
}
}
},
+ "node_modules/formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "dev": true,
+ "dependencies": {
+ "fetch-blob": "^3.1.2"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2282,9 +2329,9 @@
"dev": true
},
"node_modules/nanoid": {
- "version": "3.1.29",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.29.tgz",
- "integrity": "sha512-dW2pUSGZ8ZnCFIlBIA31SV8huOGCHb6OwzVCc7A69rb/a+SgPBwfmLvK5TKQ3INPbRkcI8a/Owo0XbiTNH19wg==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"dev": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
@@ -2335,14 +2382,34 @@
"url": "https://nearley.js.org/#give-to-nearley"
}
},
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
"node_modules/node-fetch": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz",
- "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.1.tgz",
+ "integrity": "sha512-Ef3SPFtRWFCDyhvcwCSvacLpkwmYZcD57mmZzAsMiks9TpHpIghe32U9H06tMICnr+X7YCpzH7WvUlUoml2urA==",
"dev": true,
"dependencies": {
- "data-uri-to-buffer": "^3.0.1",
- "fetch-blob": "^3.1.2"
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -2566,6 +2633,24 @@
"node": ">=6"
}
},
+ "node_modules/query-string": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
+ "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
+ "dev": true,
+ "dependencies": {
+ "decode-uri-component": "^0.2.0",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -2993,12 +3078,30 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
+ "node_modules/split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
+ "node_modules/strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -3363,9 +3466,9 @@
}
},
"node_modules/web-streams-polyfill": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
- "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
+ "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==",
"dev": true,
"engines": {
"node": ">= 8"
@@ -4079,9 +4182,15 @@
}
},
"data-uri-to-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
- "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
+ "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
+ "dev": true
+ },
+ "dayjs": {
+ "version": "1.10.8",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.8.tgz",
+ "integrity": "sha512-wbNwDfBHHur9UOzNUjeKUOJ0fCb0a52Wx0xInmQ7Y8FstyajiV1NmK1e00cxsr9YrE9r7yAChE0VvpuY5Rnlow==",
"dev": true
},
"debug": {
@@ -4093,6 +4202,12 @@
"ms": "2.1.2"
}
},
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
"deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4153,9 +4268,9 @@
},
"dependencies": {
"node-fetch": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz",
- "integrity": "sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==",
+ "version": "2.6.7",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+ "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
@@ -4611,11 +4726,12 @@
}
},
"fetch-blob": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz",
- "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==",
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz",
+ "integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==",
"dev": true,
"requires": {
+ "node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
@@ -4637,6 +4753,12 @@
"to-regex-range": "^5.0.1"
}
},
+ "filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs=",
+ "dev": true
+ },
"flat-cache": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
@@ -4654,11 +4776,20 @@
"dev": true
},
"follow-redirects": {
- "version": "1.14.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
- "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
+ "version": "1.14.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
+ "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"dev": true
},
+ "formdata-polyfill": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+ "dev": true,
+ "requires": {
+ "fetch-blob": "^3.1.2"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -5056,9 +5187,9 @@
"dev": true
},
"nanoid": {
- "version": "3.1.29",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.29.tgz",
- "integrity": "sha512-dW2pUSGZ8ZnCFIlBIA31SV8huOGCHb6OwzVCc7A69rb/a+SgPBwfmLvK5TKQ3INPbRkcI8a/Owo0XbiTNH19wg==",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
"dev": true
},
"nanospinner": {
@@ -5095,14 +5226,21 @@
"randexp": "0.4.6"
}
},
+ "node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "dev": true
+ },
"node-fetch": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz",
- "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.1.tgz",
+ "integrity": "sha512-Ef3SPFtRWFCDyhvcwCSvacLpkwmYZcD57mmZzAsMiks9TpHpIghe32U9H06tMICnr+X7YCpzH7WvUlUoml2urA==",
"dev": true,
"requires": {
- "data-uri-to-buffer": "^3.0.1",
- "fetch-blob": "^3.1.2"
+ "data-uri-to-buffer": "^4.0.0",
+ "fetch-blob": "^3.1.4",
+ "formdata-polyfill": "^4.0.10"
}
},
"normalize-path": {
@@ -5257,6 +5395,18 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
+ "query-string": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
+ "integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
+ "dev": true,
+ "requires": {
+ "decode-uri-component": "^0.2.0",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ }
+ },
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5556,12 +5706,24 @@
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
+ "split-on-first": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
+ "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
+ "dev": true
+ },
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
},
+ "strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=",
+ "dev": true
+ },
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -5790,9 +5952,9 @@
}
},
"web-streams-polyfill": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz",
- "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
+ "integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==",
"dev": true
},
"webidl-conversions": {
diff --git a/package.json b/package.json
index 187fbd663..8303cff3e 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"axios": "^0.24.0",
+ "dayjs": "^1.10.8",
"discord.js": "^13.2.0",
"dotenv": "^10.0.0",
"eslint": "^7.32.0",
@@ -46,6 +47,7 @@
"node-fetch": "^3.0.0",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^2.4.0",
+ "query-string": "^7.1.1",
"sass": "^1.42.1",
"size-limit": "^7.0.4",
"svelte": "^3.42.6",
diff --git a/src/lib/calendar-link/README.md b/src/lib/calendar-link/README.md
new file mode 100644
index 000000000..8bae55f77
--- /dev/null
+++ b/src/lib/calendar-link/README.md
@@ -0,0 +1,6 @@
+# `calendar-link`
+
+## Credit
+
+- Demo:
+- Repository:
diff --git a/src/lib/calendar-link/calendar-link.ts b/src/lib/calendar-link/calendar-link.ts
new file mode 100644
index 000000000..267b88855
--- /dev/null
+++ b/src/lib/calendar-link/calendar-link.ts
@@ -0,0 +1,178 @@
+import dayjs from 'dayjs';
+import utc from 'dayjs/plugin/utc.js';
+import { stringify } from 'query-string';
+
+import type { CalendarEvent, NormalizedCalendarEvent, Google, Outlook, Yahoo } from './interfaces';
+import { TimeFormats } from './utils';
+
+dayjs.extend(utc);
+
+function formatTimes(
+ { startUtc, endUtc }: NormalizedCalendarEvent,
+ dateTimeFormat: keyof typeof TimeFormats
+): { start: string; end: string } {
+ const format = TimeFormats[dateTimeFormat];
+ return { start: startUtc.format(format), end: endUtc.format(format) };
+}
+
+export function eventify(event: CalendarEvent): NormalizedCalendarEvent {
+ const { start, end, duration, ...rest } = event;
+ const startUtc = dayjs(start).utc();
+ const endUtc = end
+ ? dayjs(end).utc()
+ : (() => {
+ if (event.allDay) {
+ return startUtc.add(1, 'day');
+ }
+ if (duration && duration.length == 2) {
+ const value = Number(duration[0]);
+ const unit = duration[1];
+ return startUtc.add(value, unit);
+ }
+ return dayjs().utc();
+ })();
+ return {
+ ...rest,
+ startUtc,
+ endUtc,
+ };
+}
+
+export function google(calendarEvent: CalendarEvent): string {
+ const event = eventify(calendarEvent);
+ const { start, end } = formatTimes(event, event.allDay ? 'allDay' : 'dateTimeUTC');
+ const details: Google = {
+ action: 'TEMPLATE',
+ text: event.title,
+ details: event.description,
+ location: event.location,
+ trp: event.busy,
+ dates: start + '/' + end,
+ };
+ if (event.guests && event.guests.length) {
+ details.add = event.guests.join();
+ }
+ return `https://calendar.google.com/calendar/render?${stringify(details)}`;
+}
+
+export function outlook(calendarEvent: CalendarEvent): string {
+ const event = eventify(calendarEvent);
+ const { start, end } = formatTimes(event, 'dateTimeWithOffset');
+ const details: Outlook = {
+ path: '/calendar/action/compose',
+ rru: 'addevent',
+ startdt: start,
+ enddt: end,
+ subject: event.title,
+ body: event.description,
+ location: event.location,
+ allday: event.allDay || false,
+ };
+ return `https://outlook.live.com/calendar/0/deeplink/compose?${stringify(details)}`;
+}
+
+export function office365(calendarEvent: CalendarEvent): string {
+ const event = eventify(calendarEvent);
+ const { start, end } = formatTimes(event, 'dateTimeWithOffset');
+ const details: Outlook = {
+ path: '/calendar/action/compose',
+ rru: 'addevent',
+ startdt: start,
+ enddt: end,
+ subject: event.title,
+ body: event.description,
+ location: event.location,
+ allday: event.allDay || false,
+ };
+ return `https://outlook.office.com/calendar/0/deeplink/compose?${stringify(details)}`;
+}
+
+export function yahoo(calendarEvent: CalendarEvent): string {
+ const event = eventify(calendarEvent);
+ const { start, end } = formatTimes(event, event.allDay ? 'allDay' : 'dateTimeUTC');
+ const details: Yahoo = {
+ v: 60,
+ title: event.title,
+ st: start,
+ et: end,
+ desc: event.description,
+ in_loc: event.location,
+ dur: event.allDay ? 'allday' : false,
+ };
+ return `https://calendar.yahoo.com/?${stringify(details)}`;
+}
+
+export function ics(calendarEvent: CalendarEvent): string {
+ const event = eventify(calendarEvent);
+ const formattedDescription: string = (event.description || '')
+ .replace(/,/gm, ',')
+ .replace(/;/gm, ';')
+ .replace(/\n/gm, '\\n')
+ .replace(/(\\n)[\s\t]+/gm, '\\n');
+
+ const formattedLocation: string = (event.location || '')
+ .replace(/,/gm, ',')
+ .replace(/;/gm, ';')
+ .replace(/\n/gm, '\\n')
+ .replace(/(\\n)[\s\t]+/gm, '\\n');
+
+ const { start, end } = formatTimes(event, event.allDay ? 'allDay' : 'dateTimeUTC');
+ const calendarChunks = [
+ {
+ key: 'BEGIN',
+ value: 'VCALENDAR',
+ },
+ {
+ key: 'VERSION',
+ value: '2.0',
+ },
+ {
+ key: 'BEGIN',
+ value: 'VEVENT',
+ },
+ {
+ key: 'URL',
+ value: event.url,
+ },
+ {
+ key: 'DTSTART',
+ value: start,
+ },
+ {
+ key: 'DTEND',
+ value: end,
+ },
+ {
+ key: 'SUMMARY',
+ value: event.title,
+ },
+ {
+ key: 'DESCRIPTION',
+ value: formattedDescription,
+ },
+ {
+ key: 'LOCATION',
+ value: formattedLocation,
+ },
+ {
+ key: 'END',
+ value: 'VEVENT',
+ },
+ {
+ key: 'END',
+ value: 'VCALENDAR',
+ },
+ ];
+
+ let calendarUrl = '';
+
+ calendarChunks.forEach((chunk) => {
+ if (chunk.value) {
+ calendarUrl += `${chunk.key}:${encodeURIComponent(`${chunk.value}\n`)}`;
+ }
+ });
+
+ return `data:text/calendar;charset=utf8,${calendarUrl}`;
+}
+
+export type { CalendarEvent };
diff --git a/src/lib/calendar-link/index.ts b/src/lib/calendar-link/index.ts
new file mode 100644
index 000000000..2c8019839
--- /dev/null
+++ b/src/lib/calendar-link/index.ts
@@ -0,0 +1 @@
+export * from './calendar-link';
diff --git a/src/lib/calendar-link/interfaces.ts b/src/lib/calendar-link/interfaces.ts
new file mode 100644
index 000000000..fa6f5e5d2
--- /dev/null
+++ b/src/lib/calendar-link/interfaces.ts
@@ -0,0 +1,54 @@
+import type dayjs from 'dayjs';
+
+interface CalendarEvent {
+ title: string;
+ start: Date;
+ end?: Date;
+ duration?: [number, dayjs.UnitType];
+ allDay?: boolean;
+ description?: string;
+ location?: string;
+ busy?: boolean;
+ guests?: string[];
+ url?: string;
+}
+
+interface NormalizedCalendarEvent extends Omit {
+ startUtc: dayjs.Dayjs;
+ endUtc: dayjs.Dayjs;
+}
+
+interface Google extends Record {
+ action: string;
+ text: string;
+ dates: string;
+ details?: string;
+ location?: string;
+ trp?: boolean;
+ sprop?: string;
+ add?: string;
+ src?: string;
+ recur?: string;
+}
+
+interface Outlook extends Record {
+ path: string;
+ rru: string;
+ startdt: string;
+ enddt: string;
+ subject: string;
+ allday?: boolean;
+ body?: string;
+ location?: string;
+}
+
+interface Yahoo extends Record {
+ v: number;
+ title: string;
+ st: string;
+ et: string;
+ desc?: string;
+ in_loc?: string;
+}
+
+export type { CalendarEvent, NormalizedCalendarEvent, Outlook, Yahoo, Google };
diff --git a/src/lib/calendar-link/utils.ts b/src/lib/calendar-link/utils.ts
new file mode 100644
index 000000000..5a78feb88
--- /dev/null
+++ b/src/lib/calendar-link/utils.ts
@@ -0,0 +1,5 @@
+export const TimeFormats = {
+ dateTimeWithOffset: 'YYYY-MM-DD[T]HH:mm:ssZ',
+ dateTimeUTC: 'YYYYMMDD[T]HHmmss[Z]',
+ allDay: 'YYYYMMDD',
+};
diff --git a/src/lib/components/events/event-item.svelte b/src/lib/components/events/event-item.svelte
index cc8a05191..c938142b6 100644
--- a/src/lib/components/events/event-item.svelte
+++ b/src/lib/components/events/event-item.svelte
@@ -1,11 +1,11 @@