Compare commits
267 Commits
56acc42f17
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 699f7f0944 | |||
| bf220f6b4e | |||
| 02ee99d52f | |||
| e50dedca66 | |||
| 760025f414 | |||
| 07d9dfaaaa | |||
| d3f549c92f | |||
| bf64a3d30e | |||
| b27c4811da | |||
| c462c2b11a | |||
| a774254e71 | |||
| 0a665ab553 | |||
| c2d8cc3a40 | |||
| eed07ba84b | |||
| e8f1dca2a0 | |||
| 4037098927 | |||
| e007dd2b83 | |||
| b86cf8e30f | |||
| e46f174a1e | |||
| 32b8f00308 | |||
| c9e698c906 | |||
| 286248e403 | |||
| f9e03f47ba | |||
| b54174eb5e | |||
| 62d19d1a04 | |||
| 6b3866a99f | |||
| 18c30b6f98 | |||
| 77b27a56f7 | |||
| 4c48700086 | |||
| 6f68a50eb7 | |||
| ef9be8ef9a | |||
| a81343c519 | |||
| 60ad068939 | |||
| df6965cb3a | |||
| 9e95bec659 | |||
| 88b866c96a | |||
| 5784c08e1b | |||
| 753840f4f1 | |||
| dbe27d68d3 | |||
| 9b058fd44b | |||
| 17d34f6a80 | |||
| 17fd285d67 | |||
| 097baed957 | |||
| b983fbaa1f | |||
| afe337d44a | |||
| 6fe8c84ef4 | |||
| e71a6987fc | |||
| 938878d860 | |||
| 04abff06ec | |||
| 52d5a314de | |||
| 5b17eb15a2 | |||
| 49ef28a5bf | |||
| 0d481e7798 | |||
| 827566d57d | |||
| 6444cf21eb | |||
| 216e7f59ef | |||
| 479b044798 | |||
| 7c6bde690f | |||
| 981205efaa | |||
| b6408f3244 | |||
| fab895b85e | |||
| 9d869c4a37 | |||
| 583b6984d8 | |||
| ad912e8f03 | |||
| fe1170b22e | |||
| b69e0a104c | |||
| e23bc927e2 | |||
| 29a17b2e94 | |||
| 18693faa5f | |||
| 378f81efe4 | |||
| 05e3cf811a | |||
| 5815ac2bf4 | |||
| cafa6476ca | |||
| 3007150aeb | |||
| 2199bb3f39 | |||
| 7f52da5f21 | |||
| 4ae88132af | |||
| 327885dd17 | |||
| cf758c2494 | |||
| 6e82d2af37 | |||
| f42a48dbcc | |||
| e95145ff1a | |||
| 4031f35c73 | |||
| 522dc33225 | |||
| 02b7dcb292 | |||
| 5dbcf81d84 | |||
| ff2d4eefb5 | |||
| d745b10d24 | |||
| 2d4786a4fd | |||
| 2d0983f4d1 | |||
| dd344ecc3f | |||
| 93c31888b2 | |||
| 6779caccef | |||
| bf69b5acd1 | |||
| 0424f27264 | |||
| 5ae102071b | |||
| 8b716aeb97 | |||
| 1f5bbb6ce5 | |||
| a94c24581f | |||
| 5383f9bfc4 | |||
| beddc9c7ef | |||
| 0e81334c5f | |||
| dbd269c43b | |||
| 5c3d7d77fc | |||
| 0c48b8f7bf | |||
| 8c09fb4cda | |||
| 0a84598fbe | |||
| ee1e347405 | |||
| 6f8d3b1ad2 | |||
| a91350bcab | |||
| b00425e9c0 | |||
| c82a3477bd | |||
| 040dca3829 | |||
| 8674c1d361 | |||
| 6c9cb4201a | |||
| 7c32a5e824 | |||
| 6c2f9cb62e | |||
| 3cece877f7 | |||
| 8dd700f62e | |||
| 804cdcbf59 | |||
| 885fe7c9e9 | |||
| 930cc52373 | |||
| 4636a213b3 | |||
| 469954f613 | |||
| 5bdcd65d42 | |||
| cb91cd4cc7 | |||
| e04ddaed9b | |||
| 605da5e46c | |||
| 360e8ece85 | |||
| 1543bf9d03 | |||
| 287932d9d7 | |||
| 7c4de31c12 | |||
| 497c6150f7 | |||
| 514f51f111 | |||
| 244dc4fff6 | |||
| 4b39f787c9 | |||
| fb92b9c973 | |||
| 181358db7d | |||
| 3790567d44 | |||
| b3af993236 | |||
| 7ae2bb7635 | |||
| a75f0662ad | |||
| ffd68d5037 | |||
| adf9e2334c | |||
| 4125811017 | |||
| 4e8356b072 | |||
| bad116cdc0 | |||
| 9eac6871a4 | |||
| 3b694689f3 | |||
| 1604ddaa78 | |||
| 16181923b6 | |||
| 62dcaba875 | |||
| f67c05f034 | |||
| 8fd792b85c | |||
| 2595fcc365 | |||
| 0af5b8d84c | |||
| d1e8ff48c5 | |||
| d7964f6fc2 | |||
| 0394093407 | |||
| adcd6abfc7 | |||
| 721eaa5d07 | |||
| 994908ef6a | |||
| dda76ac7b0 | |||
| e7eac26e66 | |||
| 15b61ed56f | |||
| f9a51ffdce | |||
| a47b9a86dc | |||
| 89ca64dd88 | |||
| e5e1c5f82b | |||
| ab1e642c36 | |||
| 1cae3ac092 | |||
| afe5876162 | |||
| 72129f69ee | |||
| a2e2704a05 | |||
| 65af850d68 | |||
| f7425cdc03 | |||
| d9f432ef73 | |||
| 3a86651891 | |||
| 20f304c9cd | |||
| 1f58f989f7 | |||
| 96b1a99e04 | |||
| 1bc7dd3a43 | |||
| a048970be6 | |||
| e3171e5693 | |||
| d463b3df5c | |||
| 1ae7befa4b | |||
| dc80e1e199 | |||
| 35ffc2b0f7 | |||
| 383844dda8 | |||
| 77fb6595a2 | |||
| aa3adbe634 | |||
| 59a700c2f3 | |||
| 18f5a6f476 | |||
| 63a178ca57 | |||
| 0ae949a616 | |||
| edc9720623 | |||
| bbc3fbb0a5 | |||
| 1fc468efac | |||
| 143c415797 | |||
| e79a4d48e1 | |||
| 343325440e | |||
| 5c018892d3 | |||
| be61293cbe | |||
| e493194ce1 | |||
| b61631c33c | |||
| fa65da75bc | |||
| a4468f18dc | |||
| fea28b71ba | |||
| 884ad891f3 | |||
| 058161f798 | |||
| aa68748817 | |||
| 928857596f | |||
| e6919a4283 | |||
| f7993a9108 | |||
| 134664095a | |||
| 7b2495cef5 | |||
| 27ce8b0ceb | |||
| 0e5724533f | |||
| cea58d12ff | |||
| 25034e7a4b | |||
| cd4a7ce9c8 | |||
| 2229255ff4 | |||
| 299dbe3239 | |||
| deced84c59 | |||
| 0150ae8d1c | |||
| 6877ee75ed | |||
| b5726aec84 | |||
| 6c1f1c2a1c | |||
| 9a59448933 | |||
| c55f6d2cf2 | |||
| 16bf7b019f | |||
| 1ff5b6b036 | |||
| 3c47a1b14e | |||
| da6ccccab4 | |||
| f79ef53ebd | |||
| 185984bf0c | |||
| dd522c1fb1 | |||
| 07f290ad85 | |||
| eefc213144 | |||
| 849b4f0de7 | |||
| bf3a712de8 | |||
| 46839b48cf | |||
| f4447b23de | |||
| 8a734b9fa9 | |||
| 34ca806e84 | |||
| b7ac8917e9 | |||
| 39b25209f4 | |||
| ce287bc006 | |||
| f0e3e741ff | |||
| 856928760d | |||
| a4bd5d0d83 | |||
| 09b6db6774 | |||
| be061b956e | |||
| 074b5a4ae4 | |||
| e1e83994e0 | |||
| 9cc4b21b76 | |||
| 6634ecaa48 | |||
| b31f74ed29 | |||
| 30c5fc4c88 | |||
| a45fd152df | |||
| 60f01d859e | |||
| 6d4e033fb7 | |||
| bf635df863 | |||
| 1732b4cf4d | |||
| a2e268913a | |||
| dfb04e5e01 | |||
| c0ce844169 |
3
.babelrc
@@ -5,7 +5,6 @@
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-optional-chaining",
|
||||
"@babel/plugin-syntax-import-attributes"
|
||||
"@babel/plugin-transform-optional-chaining"
|
||||
],
|
||||
}
|
||||
|
||||
52
.eslintrc
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"plugin:import/errors",
|
||||
"plugin:vue/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@babel/eslint-parser",
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"default-param-last": 0,
|
||||
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
|
||||
"import/prefer-default-export": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"import/extensions": [2, "always"],
|
||||
"indent": ["error", "tab"],
|
||||
"max-len": 0,
|
||||
"no-console": 0,
|
||||
"no-param-reassign": ["error", {
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
||||
}],
|
||||
"vue/multi-word-component-names": 0,
|
||||
"vue/no-reserved-component-names": 0,
|
||||
"no-tabs": "off",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
|
||||
"prefer-destructuring": 0,
|
||||
"template-curly-spacing": "off",
|
||||
"vue/html-indent": ["error", "tab"],
|
||||
"vue/multiline-html-element-content-newline": 0,
|
||||
"vue/no-v-html": 0,
|
||||
"vue/singleline-html-element-content-newline": 0,
|
||||
"vue/comment-directive": 0,
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"alias": {
|
||||
"extensions": [".js"],
|
||||
"map": [
|
||||
["#", "."],
|
||||
["#root", "."]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"CONFIG": true
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
|
||||
|
||||
<!-- RTA restricted to adults label -->
|
||||
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
|
||||
|
||||
<title>traxxx - Consent</title>
|
||||
<style>
|
||||
:root {
|
||||
@@ -156,6 +159,12 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.rta {
|
||||
position: fixed;
|
||||
bottom: .5rem;
|
||||
right: .5rem;
|
||||
}
|
||||
|
||||
@media(max-width: 800px) {
|
||||
.heading {
|
||||
font-size: 1.25rem;
|
||||
@@ -219,5 +228,12 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src="/img/rta.gif"
|
||||
alt="RTA Restricted To Adults"
|
||||
title="RTA Restricted To Adults"
|
||||
class="rta"
|
||||
>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,3 +8,5 @@
|
||||
@custom-media --small (max-width: 900px);
|
||||
@custom-media --compact (max-width: 1200px);
|
||||
@custom-media --big (max-width: 1500px);
|
||||
@custom-media --big-10 (max-width: 1750px);
|
||||
@custom-media --big-20 (max-width: 2000px);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.input {
|
||||
width: 15rem;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem .75rem;
|
||||
font-size: 1rem;
|
||||
@@ -25,7 +26,8 @@
|
||||
.button {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: stretch;
|
||||
/* align-items: stretch; */
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
border: none;
|
||||
@@ -42,7 +44,7 @@
|
||||
fill: var(--glass);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background: var(--primary);
|
||||
color: var(--text-light);
|
||||
@@ -55,6 +57,10 @@
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: var(--glass-weak-30);
|
||||
}
|
||||
}
|
||||
|
||||
.button-label {
|
||||
@@ -119,12 +125,22 @@
|
||||
|
||||
.button-cancel {
|
||||
background: none;
|
||||
color: var(--glass);
|
||||
color: var(--error);
|
||||
font-weight: normal;
|
||||
box-shadow: none;
|
||||
|
||||
.icon {
|
||||
fill: var(--error);
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--error);
|
||||
color: var(--text-light);
|
||||
background: var(--error);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@@ -295,3 +311,23 @@
|
||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||
}
|
||||
}
|
||||
|
||||
.search-cancel {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: .5rem;
|
||||
|
||||
.icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: var(--glass);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,12 @@
|
||||
.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
|
||||
--text: #222;
|
||||
--text-light: #fff;
|
||||
--text-piss: #b92;
|
||||
|
||||
/* --link: #48f; */
|
||||
--link: var(--primary);
|
||||
@@ -101,6 +102,7 @@
|
||||
--background-dim: var(--shadow-weak-10);
|
||||
|
||||
--text: #fcfcfc;
|
||||
--text-piss: #ba0;
|
||||
|
||||
--glass-weak-50: rgba(255, 255, 255, .02);
|
||||
--glass-weak-40: rgba(255, 255, 255, .05);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Content */
|
||||
|
||||
.v-popper__popper {
|
||||
z-index: 10000;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
outline: none;
|
||||
@@ -45,6 +45,10 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.no-outer-scroll .v-popper__inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.v-popper__inner > div {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -82,6 +86,10 @@
|
||||
border-width: 6px;
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-container {
|
||||
bottom: -6px;
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="top"] .v-popper__arrow-inner,
|
||||
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner {
|
||||
left: -2px;
|
||||
@@ -105,7 +113,7 @@
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-container {
|
||||
top: 0;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="bottom"] .v-popper__arrow-inner,
|
||||
@@ -151,7 +159,11 @@
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-container {
|
||||
right: -10px;
|
||||
right: -7px;
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="right"] .v-popper__arrow-container {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.v-popper__popper[data-popper-placement^="left"] .v-popper__arrow-inner,
|
||||
|
||||
4
assets/img/icons/at-sign.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M13.657 2.343c-1.511-1.511-3.52-2.343-5.657-2.343s-4.146 0.832-5.657 2.343c-1.511 1.511-2.343 3.52-2.343 5.657s0.832 4.146 2.343 5.657c1.511 1.511 3.52 2.343 5.657 2.343 1.199 0 2.353-0.259 3.429-0.77 0.499-0.237 0.711-0.834 0.474-1.332s-0.834-0.711-1.332-0.474c-0.806 0.383-1.671 0.577-2.571 0.577-3.308 0-6-2.692-6-6s2.692-6 6-6 6 2.692 6 6v1c0 0.551-0.449 1-1 1s-1-0.449-1-1v-4c0-0.552-0.448-1-1-1-0.406 0-0.755 0.242-0.912 0.59-0.608-0.374-1.323-0.59-2.088-0.59-2.206 0-4 1.794-4 4s1.794 4 4 4c1.045 0 1.998-0.403 2.712-1.062 0.551 0.649 1.372 1.062 2.288 1.062 1.654 0 3-1.346 3-3v-1c0-2.137-0.832-4.146-2.343-5.657zM8 10c-1.103 0-2-0.897-2-2s0.897-2 2-2c1.103 0 2 0.897 2 2s-0.897 2-2 2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 850 B |
4
assets/img/icons/cancel.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M11.5 0h-7l-4.5 4.5v7l4.5 4.5h7l4.5-4.5v-7l-4.5-4.5zM12.5 11l-1.5 1.5-3-3-3 3-1.5-1.5 3-3-3-3 1.5-1.5 3 3 3-3 1.5 1.5-3 3 3 3z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
5
assets/img/icons/checkbox-checked.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14z"></path>
|
||||
<path d="M2.5 8l1.5-1.5 2.5 2.5 5.5-5.5 1.5 1.5-7 7z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
4
assets/img/icons/checkbox-checked2.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14 0h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2zM7 12.414l-3.707-3.707 1.414-1.414 2.293 2.293 4.793-4.793 1.414 1.414-6.207 6.207z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
4
assets/img/icons/checkbox-partial.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14zM4 4h8v8h-8z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 209 B |
4
assets/img/icons/checkbox-partial2.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14.5 0h-13c-0.825 0-1.5 0.675-1.5 1.5v13c0 0.825 0.675 1.5 1.5 1.5h13c0.825 0 1.5-0.675 1.5-1.5v-13c0-0.825-0.675-1.5-1.5-1.5zM14 14h-12v-12h12v12zM5 12h6c0.55 0 1-0.45 1-1v-6c0-0.55-0.45-1-1-1h-6c-0.55 0-1 0.45-1 1v6c0 0.55 0.45 1 1 1zM5 10.875h6v0.123c-0.001 0.001-0.001 0.001-0.002 0.002h-5.996c-0.001-0.001-0.001-0.001-0.002-0.002v-0.123zM5.002 5h5.996c0.001 0.001 0.001 0.001 0.002 0.002v0.123h-6v-0.123c0.001-0.001 0.001-0.001 0.002-0.002zM11 5.375v0.25h-6v-0.25h6zM11 5.875v0.25h-6v-0.25h6zM11 6.375v0.25h-6v-0.25h6zM11 6.875v0.25h-6v-0.25h6zM11 7.375v0.25h-6v-0.25h6zM11 7.875v0.25h-6v-0.25h6zM11 8.375v0.25h-6v-0.25h6zM11 8.875v0.25h-6v-0.25h6zM11 9.375v0.25h-6v-0.25h6zM11 9.875v0.25h-6v-0.25h6zM11 10.375v0.25h-6v-0.25h6z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 890 B |
4
assets/img/icons/checkbox-unchecked.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M0 0v16h16v-16h-16zM15 15h-14v-14h14v14z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 197 B |
4
assets/img/icons/checkbox-unchecked2.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14 0h-12c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2v-12c0-1.1-0.9-2-2-2zM14 14h-12v-12h12v12z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 263 B |
12
assets/img/icons/collaboration.svg
Executable file
@@ -0,0 +1,12 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M4 3.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||
<path d="M2.5 5c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||
<path d="M14 3.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||
<path d="M12.5 5c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||
<path d="M9 11.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5z"></path>
|
||||
<path d="M7.5 13c-1.381 0-2.5 0.448-2.5 1v1h5v-1c0-0.552-1.119-1-2.5-1z"></path>
|
||||
<path d="M9.5 4h-4c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h4c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
|
||||
<path d="M10.5 12c-0.095 0-0.192-0.027-0.277-0.084-0.23-0.153-0.292-0.464-0.139-0.693l2-3c0.153-0.23 0.464-0.292 0.693-0.139s0.292 0.464 0.139 0.693l-2 3c-0.096 0.145-0.255 0.223-0.416 0.223z"></path>
|
||||
<path d="M4.5 12c-0.162 0-0.32-0.078-0.417-0.223l-2-3c-0.153-0.23-0.091-0.54 0.139-0.693s0.54-0.091 0.693 0.139l2 3c0.153 0.23 0.091 0.54-0.139 0.693-0.085 0.057-0.182 0.084-0.277 0.084z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
assets/img/icons/database-refresh.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14.422 7.078c-0.786-0.672-1.807-1.078-2.922-1.078-2.202 0-4.035 1.582-4.424 3.672h1.54c0.36-1.253 1.517-2.172 2.884-2.172 0.7 0 1.344 0.241 1.855 0.645l-1.855 1.855h4.5v-4.5l-1.578 1.578z"></path>
|
||||
<path d="M11.5 13.5c-0.7 0-1.344-0.241-1.855-0.645l1.855-1.855h-4.5v4.5l1.578-1.578c0.786 0.672 1.807 1.078 2.922 1.078 2.202 0 4.035-1.582 4.424-3.672h-1.54c-0.36 1.253-1.517 2.172-2.884 2.172z"></path>
|
||||
<path d="M2.158 9.146c-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.314c-0.165 0.008-0.332 0.012-0.5 0.012-1.152 0-2.252-0.181-3.098-0.51-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677 0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.061 0.090-0.252 0.287-0.674 0.5h1.892c0.072-0.162 0.11-0.329 0.11-0.5 0-1.381-2.462-2.5-5.5-2.5s-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
5
assets/img/icons/database-time.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
|
||||
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5 4.5-2.015 4.5-4.5-2.015-4.5-4.5-4.5zM14 12h-3v-3h1v2h2v1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
6
assets/img/icons/database-time2.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5c2.485 0 4.5-2.015 4.5-4.5s-2.015-4.5-4.5-4.5zM11.5 14.688c-1.758 0-3.188-1.43-3.188-3.188s1.43-3.188 3.188-3.188 3.188 1.43 3.188 3.188-1.43 3.188-3.188 3.188z"></path>
|
||||
<path d="M12 11v-2h-1v3h3v-1z"></path>
|
||||
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
assets/img/icons/drawer-out.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M15.89 10.188l-4-5c-0.172-0.216-0.487-0.251-0.703-0.078s-0.251 0.487-0.078 0.703l3.89 4.863v0.325h-3.5l-2 2h-3l-2-2h-3.5v-0.325l3.89-4.863c0.173-0.216 0.138-0.53-0.078-0.703s-0.53-0.138-0.703 0.078l-4 5c-0.071 0.089-0.11 0.199-0.11 0.312v4.5c0 0.552 0.448 1 1 1h14c0.552 0 1-0.448 1-1v-4.5c0-0.114-0.039-0.224-0.11-0.312z"></path>
|
||||
<path d="M8 0.5l-3.5 3.5h2.5v5h2v-5h2.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 528 B |
4
assets/img/icons/exclude.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM14 6.125h-3v-0.125h3v0.125zM6 14v-0.125h8v0.125h-8zM6 13.625v-0.25h8v0.25h-8zM6 13.125v-0.25h8v0.25h-8zM6 12.625v-0.25h8v0.25h-8zM6 12.125v-0.25h8v0.25h-8zM6 11.625v-0.25h8v0.25h-8zM6 11.125v-0.125h5v-0.125h3v0.25h-8zM11 10.625v-0.25h3v0.25h-3zM11 10.125v-0.25h3v0.25h-3zM11 9.625v-0.25h3v0.25h-3zM11 9.125v-0.25h3v0.25h-3zM11 8.625v-0.25h3v0.25h-3zM11 8.125v-0.25h3v0.25h-3zM11 7.625v-0.25h3v0.25h-3zM11 7.125v-0.25h3v0.25h-3zM11 6.625v-0.25h3v0.25h-3zM9 4h-5v0.125h-2v-0.25h7v0.125zM2 9v-0.125h2v0.125h-2zM2 8.625v-0.25h2v0.25h-2zM2 8.125v-0.25h2v0.25h-2zM2 7.625v-0.25h2v0.25h-2zM2 7.125v-0.25h2v0.25h-2zM2 6.625v-0.25h2v0.25h-2zM2 6.125v-0.25h2v0.25h-2zM2 5.625v-0.25h2v0.25h-2zM2 5.125v-0.25h2v0.25h-2zM2 4.625v-0.25h2v0.25h-2zM9 3.625h-7v-0.25h7v0.25zM9 3.125h-7v-0.25h7v0.25zM9 2.625h-7v-0.25h7v0.25zM9 2.125h-7v-0.125h7v0.125zM5 5h5v5h-5v-5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
6
assets/img/icons/file-video.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14.341 3.579c-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.806-0.591-1.197-0.659-1.421-0.659h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h11.5c0.689 0 1.25-0.561 1.25-1.25v-9.75c0-0.224-0.068-0.615-0.659-1.421v0 0zM12.271 2.729c0.48 0.48 0.856 0.912 1.134 1.271h-2.406v-2.405c0.359 0.278 0.792 0.654 1.271 1.134v0 0zM14 14.75c0 0.136-0.114 0.25-0.25 0.25h-11.5c-0.135 0-0.25-0.114-0.25-0.25v-13.5c0-0.135 0.115-0.25 0.25-0.25 0 0 7.749-0 7.75 0v3.5c0 0.276 0.224 0.5 0.5 0.5h3.5v9.75z"></path>
|
||||
<path d="M4 8h5v5h-5v-5z"></path>
|
||||
<path d="M9 10l3-2v5l-3-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 756 B |
4
assets/img/icons/interset.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM10 6.125h-4v-0.125h4v0.125zM10 10h-4v-0.125h4v0.125zM10 9.625h-4v-0.25h4v0.25zM10 9.125h-4v-0.25h4v0.25zM10 8.625h-4v-0.25h4v0.25zM10 8.125h-4v-0.25h4v0.25zM10 7.625h-4v-0.25h4v0.25zM10 7.125h-4v-0.25h4v0.25zM10 6.625h-4v-0.25h4v0.25zM4 10h-3v-9h9v3h-6v6zM15 15h-10v-3h7v-7h3v10z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 467 B |
5
assets/img/icons/loop3.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M13.901 2.599c-1.463-1.597-3.565-2.599-5.901-2.599-4.418 0-8 3.582-8 8h1.5c0-3.59 2.91-6.5 6.5-6.5 1.922 0 3.649 0.835 4.839 2.161l-2.339 2.339h5.5v-5.5l-2.099 2.099z"></path>
|
||||
<path d="M14.5 8c0 3.59-2.91 6.5-6.5 6.5-1.922 0-3.649-0.835-4.839-2.161l2.339-2.339h-5.5v5.5l2.099-2.099c1.463 1.597 3.565 2.599 5.901 2.599 4.418 0 8-3.582 8-8h-1.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 500 B |
52
assets/img/icons/matrix-full.svg
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
viewBox="0 0 75 32"
|
||||
id="svg1"
|
||||
sodipodi:docname="matrix-full.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="12.000629"
|
||||
inkscape:cx="27.79021"
|
||||
inkscape:cy="22.165505"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<title
|
||||
id="title1">Matrix (protocol) logo</title>
|
||||
<g
|
||||
id="g1">
|
||||
<path
|
||||
d="m0.936 0.732v30.52h2.194v0.732h-3.035v-31.98h3.034v0.732zm8.45 9.675v1.544h0.044a4.461 4.461 0 0 1 1.487-1.368c0.58-0.323 1.245-0.485 1.993-0.485 0.72 0 1.377 0.14 1.972 0.42 0.595 0.279 1.047 0.771 1.355 1.477 0.338-0.5 0.796-0.941 1.377-1.323 0.58-0.383 1.266-0.574 2.06-0.574 0.602 0 1.16 0.074 1.674 0.22 0.514 0.148 0.954 0.383 1.322 0.707 0.366 0.323 0.653 0.746 0.859 1.268 0.205 0.522 0.308 1.15 0.308 1.887v7.633h-3.127v-6.464c0-0.383-0.015-0.743-0.044-1.082a2.305 2.305 0 0 0-0.242-0.882 1.473 1.473 0 0 0-0.584-0.596c-0.257-0.146-0.606-0.22-1.047-0.22-0.44 0-0.796 0.085-1.068 0.253-0.272 0.17-0.485 0.39-0.639 0.662a2.654 2.654 0 0 0-0.308 0.927 7.074 7.074 0 0 0-0.078 1.048v6.354h-3.128v-6.398c0-0.338-7e-3 -0.673-0.021-1.004a2.825 2.825 0 0 0-0.188-0.916 1.411 1.411 0 0 0-0.55-0.673c-0.258-0.168-0.636-0.253-1.135-0.253a2.33 2.33 0 0 0-0.584 0.1 1.94 1.94 0 0 0-0.705 0.374c-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.357v6.619h-3.129v-11.41zm16.46 1.677a3.751 3.751 0 0 1 1.233-1.17 5.37 5.37 0 0 1 1.685-0.629 9.579 9.579 0 0 1 1.884-0.187c0.573 0 1.153 0.04 1.74 0.121 0.588 0.081 1.124 0.24 1.609 0.475 0.484 0.235 0.88 0.562 1.19 0.981 0.308 0.42 0.462 0.975 0.462 1.666v5.934c0 0.516 0.03 1.008 0.088 1.478 0.058 0.471 0.161 0.824 0.308 1.06h-3.171a4.435 4.435 0 0 1-0.22-1.104c-0.5 0.515-1.087 0.876-1.762 1.081a7.084 7.084 0 0 1-2.071 0.31c-0.544 0-1.05-0.067-1.52-0.2a3.472 3.472 0 0 1-1.234-0.617 2.87 2.87 0 0 1-0.826-1.059c-0.199-0.426-0.298-0.934-0.298-1.522 0-0.647 0.114-1.18 0.342-1.6 0.227-0.419 0.52-0.753 0.881-1.004 0.36-0.25 0.771-0.437 1.234-0.562 0.462-0.125 0.929-0.224 1.399-0.298 0.47-0.073 0.932-0.132 1.387-0.176 0.456-0.044 0.86-0.11 1.212-0.199 0.353-0.088 0.631-0.217 0.837-0.386s0.301-0.415 0.287-0.74c0-0.337-0.055-0.606-0.166-0.804a1.217 1.217 0 0 0-0.44-0.464 1.737 1.737 0 0 0-0.639-0.22 5.292 5.292 0 0 0-0.782-0.055c-0.617 0-1.101 0.132-1.454 0.397-0.352 0.264-0.558 0.706-0.617 1.323h-3.128c0.044-0.735 0.227-1.345 0.55-1.83zm6.179 4.423a5.095 5.095 0 0 1-0.639 0.165 9.68 9.68 0 0 1-0.716 0.11c-0.25 0.03-0.5 0.067-0.749 0.11a5.616 5.616 0 0 0-0.694 0.177 2.057 2.057 0 0 0-0.594 0.298c-0.17 0.125-0.305 0.284-0.408 0.474-0.103 0.192-0.154 0.434-0.154 0.728 0 0.28 0.051 0.515 0.154 0.706 0.103 0.192 0.242 0.342 0.419 0.453 0.176 0.11 0.381 0.187 0.617 0.231 0.234 0.044 0.477 0.066 0.726 0.066 0.617 0 1.094-0.102 1.432-0.309 0.338-0.205 0.587-0.452 0.75-0.739 0.16-0.286 0.26-0.576 0.297-0.87 0.036-0.295 0.055-0.53 0.055-0.707v-1.17a1.4 1.4 0 0 1-0.496 0.277zm11.86-6.1v2.096h-2.291v5.647c0 0.53 0.088 0.883 0.264 1.059 0.176 0.177 0.529 0.265 1.057 0.265 0.177 0 0.345-7e-3 0.507-0.022 0.161-0.015 0.316-0.037 0.463-0.066v2.426a7.49 7.49 0 0 1-0.882 0.089 21.67 21.67 0 0 1-0.947 0.022c-0.484 0-0.944-0.034-1.377-0.1a3.233 3.233 0 0 1-1.145-0.386 2.04 2.04 0 0 1-0.782-0.816c-0.191-0.353-0.287-0.816-0.287-1.39v-6.728h-1.894v-2.096h1.894v-3.42h3.129v3.42h2.29zm4.471 0v2.118h0.044a3.907 3.907 0 0 1 1.454-1.754 4.213 4.213 0 0 1 1.036-0.497 3.734 3.734 0 0 1 1.145-0.176c0.206 0 0.433 0.037 0.683 0.11v2.912a5.862 5.862 0 0 0-0.528-0.077 5.566 5.566 0 0 0-0.595-0.033c-0.573 0-1.058 0.096-1.454 0.287a2.52 2.52 0 0 0-0.958 0.783 3.143 3.143 0 0 0-0.518 1.158 6.32 6.32 0 0 0-0.154 1.434v5.14h-3.128v-11.4zm5.684-1.765v-2.582h3.128v2.582h-3.127zm3.128 1.765v11.4h-3.127v-11.4h3.128zm1.63 0h3.569l2.005 2.978 1.982-2.978h3.459l-3.745 5.339 4.208 6.067h-3.57l-2.378-3.596-2.38 3.596h-3.502l4.097-6.001zm15.3 20.84v-30.52h-2.194v-0.732h3.035v31.98h-3.035v-0.732z"
|
||||
id="path1" />
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>Matrix (protocol) logo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
41
assets/img/icons/matrix.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 32 32"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="matrix.svg"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.23142407"
|
||||
inkscape:cx="412.66234"
|
||||
inkscape:cy="557.41825"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M0.844 0.735v30.531h2.197v0.735h-3.041v-32h3.041v0.735zM10.235 10.412v1.547h0.041c0.412-0.595 0.912-1.047 1.489-1.371 0.579-0.323 1.251-0.484 2-0.484 0.719 0 1.38 0.141 1.975 0.417 0.599 0.281 1.047 0.776 1.359 1.479 0.339-0.5 0.803-0.943 1.38-1.323 0.579-0.38 1.267-0.573 2.063-0.573 0.604 0 1.161 0.073 1.677 0.224 0.521 0.145 0.959 0.38 1.328 0.703 0.365 0.329 0.651 0.751 0.86 1.272 0.203 0.52 0.307 1.151 0.307 1.891v7.635h-3.129v-6.468c0-0.381-0.016-0.745-0.048-1.084-0.020-0.307-0.099-0.604-0.239-0.88-0.131-0.251-0.333-0.459-0.584-0.593-0.255-0.152-0.609-0.224-1.047-0.224-0.443 0-0.797 0.083-1.068 0.249-0.265 0.167-0.489 0.396-0.64 0.667-0.161 0.287-0.265 0.604-0.308 0.927-0.052 0.349-0.077 0.699-0.083 1.048v6.359h-3.131v-6.401c0-0.339-0.005-0.672-0.025-1-0.011-0.317-0.073-0.624-0.193-0.916-0.104-0.281-0.301-0.516-0.552-0.672-0.255-0.167-0.636-0.255-1.136-0.255-0.151 0-0.348 0.031-0.588 0.099-0.24 0.067-0.479 0.192-0.703 0.375-0.229 0.188-0.428 0.453-0.589 0.797-0.161 0.343-0.239 0.796-0.239 1.359v6.62h-3.131v-11.421zM31.156 31.265v-30.531h-2.197v-0.735h3.041v32h-3.041v-0.735z"
|
||||
id="path1"
|
||||
style="fill-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
5
assets/img/icons/merge.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M9 5h3.5l-4.5-4.5-4.5 4.5h3.5v3.586l-5.957 5.957 1.414 1.414 6.543-6.543z"></path>
|
||||
<path d="M8.543 11.957l1.414-1.414 4 4-1.414 1.414-4-4z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 295 B |
4
assets/img/icons/popout.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14.5 2h-9c-0.825 0-1.5 0.675-1.5 1.5v1.5h-2.5c-0.825 0-1.5 0.675-1.5 1.5v6c0 0.825 0.675 1.5 1.5 1.5h9c0.825 0 1.5-0.675 1.5-1.5v-1.5h2.5c0.825 0 1.5-0.675 1.5-1.5v-6c0-0.825-0.675-1.5-1.5-1.5zM11 12.5c0 0.271-0.229 0.5-0.5 0.5h-9c-0.271 0-0.5-0.229-0.5-0.5v-6c0-0.271 0.229-0.5 0.5-0.5h2.5v3.5c0 0.825 0.675 1.5 1.5 1.5h5.5v1.5zM15 9.5c0 0.271-0.229 0.5-0.5 0.5h-9c-0.271 0-0.5-0.229-0.5-0.5v-6c0-0.271 0.229-0.5 0.5-0.5h9c0.271 0 0.5 0.229 0.5 0.5v6z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 610 B |
4
assets/img/icons/profile.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M13.5 0h-12c-0.825 0-1.5 0.675-1.5 1.5v13c0 0.825 0.675 1.5 1.5 1.5h12c0.825 0 1.5-0.675 1.5-1.5v-13c0-0.825-0.675-1.5-1.5-1.5zM13 14h-11v-12h11v12zM4 9h7v1h-7zM4 11h7v1h-7zM5 4.5c0-0.828 0.672-1.5 1.5-1.5s1.5 0.672 1.5 1.5c0 0.828-0.672 1.5-1.5 1.5s-1.5-0.672-1.5-1.5zM7.5 6h-2c-0.825 0-1.5 0.45-1.5 1v1h5v-1c0-0.55-0.675-1-1.5-1z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 488 B |
4
assets/img/icons/reset.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M4.126 9c-0.082 0.32-0.126 0.655-0.126 1 0 2.209 1.791 4 4 4s4-1.791 4-4-1.791-4-4-4v3l-5-4 5-4v3c3.314 0 6 2.686 6 6s-2.686 6-6 6-6-2.686-6-6c0-0.341 0.029-0.675 0.084-1h2.043z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
18
assets/img/icons/sort-add-asc.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg3"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs3" /><path
|
||||
d="M5 12v-12h-2v12h-2.5l3.5 3.5 3.5-3.5h-2.5z"
|
||||
id="path1" /><path
|
||||
id="path1-3"
|
||||
style="stroke-width:0.348033"
|
||||
d="M 10.443359 2.2773438 C 10.354073 2.2861032 10.283203 2.3587981 10.283203 2.4472656 L 10.283203 4.328125 L 8.3339844 4.328125 C 8.2362054 4.328125 8.1582031 4.4056346 8.1582031 4.5 L 8.1582031 5.5253906 C 8.1582031 5.6197556 8.2362047 5.6953125 8.3339844 5.6953125 L 10.283203 5.6953125 L 10.283203 7.5292969 L 11.556641 6.46875 A 0.14617623 0.14617623 0 0 0 11.556641 6.4667969 C 11.601003 6.4299705 11.650097 6.3991617 11.701172 6.375 L 11.701172 5.6953125 L 13.648438 5.6953125 C 13.746218 5.6953125 13.826172 5.619755 13.826172 5.5253906 L 13.826172 4.5 C 13.826172 4.405635 13.746225 4.328125 13.648438 4.328125 L 11.701172 4.328125 L 11.701172 2.4472656 C 11.701172 2.3529006 11.621224 2.2773437 11.523438 2.2773438 L 10.460938 2.2773438 C 10.454826 2.2773438 10.449312 2.2767598 10.443359 2.2773438 z " /><path
|
||||
d="M 16,5 C 16,2.243 13.757,0 11,0 8.243,0 6,2.243 6,5 V 5.5 C 6,5.776 6.224,6 6.5,6 6.776,6 7,5.776 7,5.5 V 5 c 0,-2.206 1.794,-4 4,-4 2.206,0 4,1.794 4,4 0,1.86 -1.277,3.428 -3,3.873 V 7 L 9,9.5 12,12 V 9.899 C 14.279,9.434 16,7.414 16,5 Z"
|
||||
id="path3" /></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
18
assets/img/icons/sort-add-desc.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg3"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs3" /><path
|
||||
d="M5 12v-12h-2v12h-2.5l3.5 3.5 3.5-3.5h-2.5z"
|
||||
id="path1" /><path
|
||||
id="path1-3"
|
||||
style="stroke-width:0.348033"
|
||||
d="M 10.443359 2.2773438 C 10.354073 2.2861032 10.283203 2.3587981 10.283203 2.4472656 L 10.283203 4.328125 L 8.3339844 4.328125 C 8.2362054 4.328125 8.1582031 4.4056346 8.1582031 4.5 L 8.1582031 5.5253906 C 8.1582031 5.6197556 8.2362047 5.6953125 8.3339844 5.6953125 L 10.283203 5.6953125 L 10.283203 6.3671875 C 10.340222 6.3923929 10.394355 6.4261166 10.443359 6.4667969 A 0.14617623 0.14617623 0 0 1 10.443359 6.46875 L 11.701172 7.5175781 L 11.701172 5.6953125 L 13.648438 5.6953125 C 13.746218 5.6953125 13.826172 5.619755 13.826172 5.5253906 L 13.826172 4.5 C 13.826172 4.405635 13.746225 4.328125 13.648438 4.328125 L 11.701172 4.328125 L 11.701172 2.4472656 C 11.701172 2.3529006 11.621224 2.2773437 11.523438 2.2773438 L 10.460938 2.2773438 C 10.454826 2.2773438 10.449312 2.2767598 10.443359 2.2773438 z " /><path
|
||||
d="m 6,5 c 0,-2.757 2.243,-5 5,-5 2.757,0 5,2.243 5,5 V 5.5 C 16,5.776 15.776,6 15.5,6 15.224,6 15,5.776 15,5.5 V 5 C 15,2.794 13.206,1 11,1 8.794,1 7,2.794 7,5 7,6.86 8.277,8.428 10,8.873 V 7 L 13,9.5 10,12 V 9.899 C 7.721,9.434 6,7.414 6,5 Z"
|
||||
id="path3" /></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
16
assets/img/icons/sort-add.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg3"
|
||||
xml:space="preserve"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs3" /><path
|
||||
d="M5 12v-12h-2v12h-2.5l3.5 3.5 3.5-3.5h-2.5z"
|
||||
id="path1" /><path
|
||||
d="M 14.334849,4.1517112 H 11.883382 V 1.785868 c 0,-0.1187227 -0.09983,-0.2150766 -0.222859,-0.2150766 h -1.337165 c -0.12302,0 -0.222861,0.096353 -0.222861,0.2150766 V 4.1517112 H 7.6490291 c -0.1230181,0 -0.2228598,0.096353 -0.2228598,0.2150768 v 1.2904597 c 0,0.1187229 0.099841,0.2150767 0.2228598,0.2150767 h 2.4514679 v 2.365843 c 0,0.1187227 0.09983,0.2150768 0.222861,0.2150768 h 1.337165 c 0.123019,0 0.222859,-0.096353 0.222859,-0.2150768 v -2.365843 h 2.451467 c 0.12302,0 0.22286,-0.096355 0.22286,-0.2150767 V 4.366788 c 0,-0.1187229 -0.09983,-0.2150768 -0.22286,-0.2150768 z"
|
||||
id="path1-3"
|
||||
style="stroke-width:0.437868" /></svg>
|
||||
|
After Width: | Height: | Size: 1010 B |
38
assets/img/icons/sort-heart.svg
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg3"
|
||||
sodipodi:docname="sort-heart.svg"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs3" /><sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="26.077075"
|
||||
inkscape:cx="4.0265253"
|
||||
inkscape:cy="4.640091"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3"
|
||||
showguides="true" /><path
|
||||
d="M5 12v-12h-2v12h-2.5l3.5 3.5 3.5-3.5h-2.5z"
|
||||
id="path1" /><path
|
||||
d="M 13.78723,0 C 12.941692,0 12.207573,0.47431375 11.834786,1.1709458 11.461999,0.47431375 10.72788,0 9.8823418,0 c -1.2219697,0 -2.21277,0.99080032 -2.21277,2.2127699 0,2.9937477 4.1652142,5.0763547 4.1652142,5.0763547 0,0 4.165214,-2.082607 4.165214,-5.0763547 C 16,0.99080032 15.0092,0 13.78723,0 Z"
|
||||
id="path1-5"
|
||||
style="stroke-width:0.520651" /></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
assets/img/icons/stack-plus.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M5 1v1.155l-2.619 0.368 0.17 1.211-2.551 0.732 3.308 11.535 10.189-2.921 0.558-0.079h1.945v-12h-11zM3.929 14.879l-2.808-9.793 1.558-0.447 1.373 9.766 2.997-0.421-3.119 0.894zM4.822 13.382l-1.418-10.088 1.595-0.224v9.93h2.543l-2.721 0.382zM15 12h-9v-10h9v10zM13 8h-2v2h-1v-2h-2v-1h2v-2h1v2h2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
4
assets/img/icons/stack.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M13.75 4h-0.75v-0.75c0-0.689-0.561-1.25-1.25-1.25h-0.75v-0.75c0-0.689-0.561-1.25-1.25-1.25h-7.5c-0.689 0-1.25 0.561-1.25 1.25v9.5c0 0.689 0.561 1.25 1.25 1.25h0.75v0.75c0 0.689 0.561 1.25 1.25 1.25h0.75v0.75c0 0.689 0.561 1.25 1.25 1.25h7.5c0.689 0 1.25-0.561 1.25-1.25v-9.5c0-0.689-0.561-1.25-1.25-1.25zM2.25 11c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.137 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.113 0.25 0.25v0.75h-5.75c-0.689 0-1.25 0.561-1.25 1.25v7.75h-0.75zM4.25 13c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.138 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.112 0.25 0.25v0.75h-5.75c-0.689 0-1.25 0.561-1.25 1.25v7.75h-0.75zM14 14.75c0 0.137-0.113 0.25-0.25 0.25h-7.5c-0.138 0-0.25-0.113-0.25-0.25v-9.5c0-0.138 0.112-0.25 0.25-0.25h7.5c0.137 0 0.25 0.112 0.25 0.25v9.5z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 922 B |
4
assets/img/icons/stack2.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M16 5l-8-4-8 4 8 4 8-4zM8 2.328l5.345 2.672-5.345 2.672-5.345-2.672 5.345-2.672zM14.398 7.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199zM14.398 10.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 354 B |
4
assets/img/icons/stack3.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M8 9l-8-4 8-4 8 4zM14.398 7.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199zM14.398 10.199l1.602 0.801-8 4-8-4 1.602-0.801 6.398 3.199z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
4
assets/img/icons/unite.svg
Executable file
@@ -0,0 +1,4 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M11 4v-4h-11v11h4v5h12v-12h-5zM10 5.125h-5v-0.125h5v0.125zM14 6.125h-3v-0.125h3v0.125zM5 9.875h5v0.125h-5v-0.125zM2 8.875h2v0.125h-2v-0.125zM4 5.875v0.25h-2v-0.25h2zM2 5.625v-0.25h2v0.25h-2zM4 6.375v0.25h-2v-0.25h2zM5 8.125v-0.25h5v0.25h-5zM10 8.375v0.25h-5v-0.25h5zM5 7.625v-0.25h5v0.25h-5zM5 7.125v-0.25h5v0.25h-5zM5 6.625v-0.25h5v0.25h-5zM4 6.875v0.25h-2v-0.25h2zM4 7.375v0.25h-2v-0.25h2zM4 7.875v0.25h-2v-0.25h2zM4 8.375v0.25h-2v-0.25h2zM5 8.875h5v0.25h-5v-0.25zM10 9.375v0.25h-5v-0.25h5zM6 14v-0.125h8v0.125h-8zM6 13.625v-0.25h8v0.25h-8zM6 13.125v-0.25h8v0.25h-8zM6 12.625v-0.25h8v0.25h-8zM6 12.125v-0.25h8v0.25h-8zM6 11.625v-0.25h8v0.25h-8zM6 11.125v-0.125h5v-0.125h3v0.25h-8zM11 10.625v-0.25h3v0.25h-3zM11 10.125v-0.25h3v0.25h-3zM11 9.625v-0.25h3v0.25h-3zM11 9.125v-0.25h3v0.25h-3zM11 8.625v-0.25h3v0.25h-3zM11 8.125v-0.25h3v0.25h-3zM11 7.625v-0.25h3v0.25h-3zM11 7.125v-0.25h3v0.25h-3zM11 6.625v-0.25h3v0.25h-3zM10 6.125h-5v-0.25h5v0.25zM5 5.625v-0.25h5v0.25h-5zM4 5.125h-2v-0.25h2v0.25zM9 2v0.125h-7v-0.125h7zM9 2.375v0.25h-7v-0.25h7zM9 2.875v0.25h-7v-0.25h7zM9 3.375v0.25h-7v-0.25h7zM9 3.875v0.125h-5v0.125h-2v-0.25h7zM4 4.375v0.25h-2v-0.25h2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
11
assets/img/icons/unlink5.svg
Executable file
@@ -0,0 +1,11 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16">
|
||||
<path d="M7 12h-3c-2.206 0-4-1.794-4-4s1.794-4 4-4h3c0.552 0 1 0.448 1 1s-0.448 1-1 1h-3c-1.103 0-2 0.897-2 2s0.897 2 2 2h3c0.552 0 1 0.448 1 1s-0.448 1-1 1z"></path>
|
||||
<path d="M13 12h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h3c1.103 0 2-0.897 2-2s-0.897-2-2-2h-3c-0.552 0-1-0.448-1-1s0.448-1 1-1h3c2.206 0 4 1.794 4 4s-1.794 4-4 4z"></path>
|
||||
<path d="M8.5 3c-0.276 0-0.5-0.224-0.5-0.5v-2c0-0.276 0.224-0.5 0.5-0.5s0.5 0.224 0.5 0.5v2c0 0.276-0.224 0.5-0.5 0.5z"></path>
|
||||
<path d="M11.5 3c-0.128 0-0.256-0.049-0.354-0.146-0.195-0.195-0.195-0.512 0-0.707l2-2c0.195-0.195 0.512-0.195 0.707 0s0.195 0.512 0 0.707l-2 2c-0.098 0.098-0.226 0.146-0.354 0.146z"></path>
|
||||
<path d="M5.5 3c-0.128 0-0.256-0.049-0.354-0.146l-2-2c-0.195-0.195-0.195-0.512 0-0.707s0.512-0.195 0.707 0l2 2c0.195 0.195 0.195 0.512 0 0.707-0.098 0.098-0.226 0.146-0.354 0.146z"></path>
|
||||
<path d="M8.5 13c0.276 0 0.5 0.224 0.5 0.5v2c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-2c0-0.276 0.224-0.5 0.5-0.5z"></path>
|
||||
<path d="M5.5 13c0.128 0 0.256 0.049 0.354 0.146 0.195 0.195 0.195 0.512 0 0.707l-2 2c-0.195 0.195-0.512 0.195-0.707 0s-0.195-0.512 0-0.707l2-2c0.098-0.098 0.226-0.146 0.354-0.146z"></path>
|
||||
<path d="M11.5 13c0.128 0 0.256 0.049 0.354 0.146l2 2c0.195 0.195 0.195 0.512 0 0.707s-0.512 0.195-0.707 0l-2-2c-0.195-0.195-0.195-0.512 0-0.707 0.098-0.098 0.226-0.146 0.354-0.146z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
27
assets/img/icons/user-tags.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<path
|
||||
id="path2"
|
||||
d="M 7 0 C 4 0 4 2.015 4 4.5 C 4 6.048 4.898 7.5957969 6 8.2167969 L 6 9.0410156 C 2.608 9.3180156 0 10.985 0 13 L 4.9726562 13 C 4.6689986 12.449922 4.7486357 11.731833 5.2109375 11.269531 L 8 8.4804688 L 8 8.2167969 C 9.102 7.5957969 10 6.048 10 4.5 C 10 2.015 10 0 7 0 z M 9 11.509766 L 8.2167969 12.292969 L 8.9238281 13 L 9 13 L 9 11.509766 z " />
|
||||
<g
|
||||
id="g2"
|
||||
transform="matrix(0.51568847,0,0,0.51568847,5.8471254,8.4255678)">
|
||||
<path
|
||||
d="m 18.938,-1 h -6 c -0.412,0 -0.989,0.239 -1.28,0.53 L 4.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 L 19.158,7.03 c 0.292,-0.292 0.53,-0.868 0.53,-1.28 v -6 c 0,-0.412 -0.337,-0.75 -0.75,-0.75 z m -3.75,6 c -0.828,0 -1.5,-0.672 -1.5,-1.5 0,-0.828 0.672,-1.5 1.5,-1.5 0.828,0 1.5,0.672 1.5,1.5 0,0.828 -0.672,1.5 -1.5,1.5 z"
|
||||
id="path1-5" />
|
||||
<path
|
||||
d="m 1.688,7.5 8.5,-8.5 h -1.25 c -0.412,0 -0.989,0.239 -1.28,0.53 L 0.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 l 0.47,-0.47 -6.5,-6.5 z"
|
||||
id="path2-3" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
2
common
@@ -1,3 +1,132 @@
|
||||
<script setup>
|
||||
import { format, subYears } from 'date-fns';
|
||||
import { parse } from 'path-to-regexp';
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import ActorTile from '#/components/actors/tile.vue';
|
||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||
|
||||
import CountryFilter from '#/components/filters/country.vue';
|
||||
import Filters from '#/components/filters/filters.vue';
|
||||
import GenderFilter from '#/components/filters/gender.vue';
|
||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Pagination from '#/components/pagination/pagination.vue';
|
||||
import { get } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||
|
||||
const q = ref(urlParsed.search.q);
|
||||
const actors = ref([]);
|
||||
const pageStash = pageProps.stash;
|
||||
const countries = ref(pageProps.countries);
|
||||
const cupRange = ref(pageProps.cupRange);
|
||||
|
||||
actors.value = pageProps.actors;
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
||||
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||
|
||||
const filters = ref({
|
||||
gender: urlParsed.search.gender,
|
||||
ageRequired: !!urlParsed.search.age,
|
||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||
dobRequired: !!urlParsed.search.dob,
|
||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||
country: urlParsed.search.c,
|
||||
braSizeRequired: !!urlParsed.search.cup,
|
||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||
heightRequired: !!urlParsed.search.height,
|
||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||
weightRequired: !!urlParsed.search.weight,
|
||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||
avatarRequired: !!urlParsed.search.avatar,
|
||||
});
|
||||
|
||||
function getPath(preserveQuery) {
|
||||
const path = parse(routeParams.path).map((segment) => {
|
||||
if (segment.name === 'page') {
|
||||
return `${segment.prefix}${currentPage.value || 1}`;
|
||||
}
|
||||
|
||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||
}).join('');
|
||||
|
||||
if (preserveQuery && urlParsed.searchOriginal) {
|
||||
return `${path}${urlParsed.searchOriginal}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function search(options = {}) {
|
||||
if (options.resetPage !== false) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
if (options.autoScope !== false) {
|
||||
if (q.value) {
|
||||
order.value = 'results.desc';
|
||||
}
|
||||
|
||||
if (!q.value && order.value.includes('results')) {
|
||||
order.value = 'likes.desc';
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
q: q.value || undefined,
|
||||
order: order.value,
|
||||
gender: filters.value.gender || undefined,
|
||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||
c: filters.value.country || undefined,
|
||||
nb: filters.value.naturalBoobs,
|
||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||
avatar: filters.value.avatarRequired || undefined,
|
||||
stashId: pageStash?.id,
|
||||
};
|
||||
|
||||
navigate(getPath(false), query, { redirect: false });
|
||||
|
||||
const res = await get('/actors', {
|
||||
...query,
|
||||
page: currentPage.value, // client uses param rather than query pagination
|
||||
});
|
||||
|
||||
actors.value = res.actors;
|
||||
total.value = res.total;
|
||||
|
||||
countries.value = res.countries;
|
||||
|
||||
events.emit('scrollUp');
|
||||
}
|
||||
|
||||
function paginate({ page }) {
|
||||
currentPage.value = page;
|
||||
|
||||
search({ resetPage: false });
|
||||
}
|
||||
|
||||
function updateFilter(prop, value, reload = true) {
|
||||
filters.value[prop] = value;
|
||||
|
||||
if (reload) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<Filters :results="total">
|
||||
@@ -5,12 +134,19 @@
|
||||
<!-- onsearch not compatible with FF and Safari -->
|
||||
<input
|
||||
v-model="q"
|
||||
type="search"
|
||||
type="input"
|
||||
placeholder="Search actors"
|
||||
class="input search"
|
||||
@search="search"
|
||||
@keydown.enter="search"
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-if="q"
|
||||
icon="cross2"
|
||||
class="search-button"
|
||||
@click="q = ''; search()"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon="search"
|
||||
class="search-button"
|
||||
@@ -104,135 +240,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { format, subYears } from 'date-fns';
|
||||
import { parse } from 'path-to-regexp';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import { get } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
import ActorTile from '#/components/actors/tile.vue';
|
||||
import Pagination from '#/components/pagination/pagination.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Filters from '#/components/filters/filters.vue';
|
||||
import GenderFilter from '#/components/filters/gender.vue';
|
||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||
import CountryFilter from '#/components/filters/country.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||
|
||||
const q = ref(urlParsed.search.q);
|
||||
const actors = ref([]);
|
||||
const pageStash = pageProps.stash;
|
||||
const countries = ref(pageProps.countries);
|
||||
const cupRange = ref(pageProps.cupRange);
|
||||
|
||||
actors.value = pageProps.actors;
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
const total = ref(Number(pageProps.actorTotal || pageProps.total));
|
||||
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||
|
||||
const filters = ref({
|
||||
gender: urlParsed.search.gender,
|
||||
ageRequired: !!urlParsed.search.age,
|
||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||
dobRequired: !!urlParsed.search.dob,
|
||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||
country: urlParsed.search.c,
|
||||
braSizeRequired: !!urlParsed.search.cup,
|
||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||
heightRequired: !!urlParsed.search.height,
|
||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||
weightRequired: !!urlParsed.search.weight,
|
||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||
avatarRequired: !!urlParsed.search.avatar,
|
||||
});
|
||||
|
||||
function getPath(preserveQuery) {
|
||||
const path = parse(routeParams.path).map((segment) => {
|
||||
if (segment.name === 'page') {
|
||||
return `${segment.prefix}${1}`;
|
||||
}
|
||||
|
||||
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||
}).join('');
|
||||
|
||||
if (preserveQuery && urlParsed.searchOriginal) {
|
||||
return `${path}${urlParsed.searchOriginal}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function search(options = {}) {
|
||||
if (options.resetPage !== false) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
if (options.autoScope !== false) {
|
||||
if (q.value) {
|
||||
order.value = 'results.desc';
|
||||
}
|
||||
|
||||
if (!q.value && order.value.includes('results')) {
|
||||
order.value = 'likes.desc';
|
||||
}
|
||||
}
|
||||
|
||||
const query = {
|
||||
q: q.value || undefined,
|
||||
order: order.value,
|
||||
gender: filters.value.gender || undefined,
|
||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||
c: filters.value.country || undefined,
|
||||
nb: filters.value.naturalBoobs,
|
||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||
avatar: filters.value.avatarRequired || undefined,
|
||||
stashId: pageStash?.id,
|
||||
};
|
||||
|
||||
navigate(getPath(false), query, { redirect: false });
|
||||
|
||||
const res = await get('/actors', {
|
||||
...query,
|
||||
page: currentPage.value, // client uses param rather than query pagination
|
||||
});
|
||||
|
||||
actors.value = res.actors;
|
||||
total.value = res.total;
|
||||
|
||||
countries.value = res.countries;
|
||||
|
||||
events.emit('scrollUp');
|
||||
}
|
||||
|
||||
function paginate({ page }) {
|
||||
currentPage.value = page;
|
||||
|
||||
search({ resetPage: false });
|
||||
}
|
||||
|
||||
function updateFilter(prop, value, reload = true) {
|
||||
filters.value[prop] = value;
|
||||
|
||||
if (reload) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.gender-button {
|
||||
&.selected .gender .icon {
|
||||
@@ -296,6 +303,7 @@ function updateFilter(prop, value, reload = true) {
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-auto-rows: max-content; /* prevent row gaps on large screens */
|
||||
gap: .25rem;
|
||||
}
|
||||
|
||||
|
||||
180
components/actors/add.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const nameInput = ref(null);
|
||||
const isSubmitting = ref(false);
|
||||
const isSubmitted = ref(false);
|
||||
|
||||
const name = ref(null);
|
||||
const gender = ref('female');
|
||||
const allowGlobalMatch = ref(true);
|
||||
|
||||
const newActor = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nameInput.value.focus();
|
||||
});
|
||||
|
||||
async function addActor() {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
const { actor } = await post('/actors', {
|
||||
actor: {
|
||||
name: name.value,
|
||||
gender: gender.value,
|
||||
allowGlobalMatch: allowGlobalMatch.value,
|
||||
},
|
||||
}, {
|
||||
successFeedback: 'Actor has been added.',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
name.value = null;
|
||||
gender.value = null;
|
||||
allowGlobalMatch.value = true;
|
||||
|
||||
newActor.value = actor;
|
||||
}
|
||||
catch (error) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
isSubmitting.value = false;
|
||||
isSubmitted.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
title="Add actor"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div
|
||||
v-if="isSubmitted && newActor"
|
||||
class="dialog-body"
|
||||
>
|
||||
<strong>Added '{{ newActor.name }}' (#{{ newActor.id }})</strong>
|
||||
|
||||
<ul
|
||||
v-if="isSubmitted && newActor"
|
||||
class="dialog-body options"
|
||||
>
|
||||
<li class="option">
|
||||
<a
|
||||
:href="`/actor/edit/${newActor.id}/${newActor.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Edit bio</a>
|
||||
</li>
|
||||
|
||||
<li class="option">
|
||||
<a
|
||||
:href="`/actor/${newActor.id}/${newActor.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Go to actor</a>
|
||||
</li>
|
||||
|
||||
<li class="option">
|
||||
<span
|
||||
class="link"
|
||||
@click="newActor = null; isSubmitted = false;"
|
||||
>Add another actor</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form
|
||||
v-else
|
||||
class="dialog-body"
|
||||
@submit.prevent="addActor"
|
||||
>
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="name"
|
||||
class="input"
|
||||
placeholder="Name"
|
||||
required
|
||||
>
|
||||
|
||||
<select
|
||||
v-model="gender"
|
||||
placeholder="Gender"
|
||||
class="input"
|
||||
>
|
||||
<option value="female">Female</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="transsexual">Transsexual</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
|
||||
<Checkbox
|
||||
label="Allow global match"
|
||||
:checked="allowGlobalMatch"
|
||||
@change="(isChecked) => allowGlobalMatch = isChecked"
|
||||
/>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<Ellipsis v-if="isSubmitting" />
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="button button-primary"
|
||||
>Add actor</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.load-container {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.options {
|
||||
padding: 0 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,102 @@
|
||||
<script setup>
|
||||
import formatTemplate from 'template-format';
|
||||
import { inject, ref, useId } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const expanded = ref(false);
|
||||
const enhancedTooltipId = useId();
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, env } = pageContext;
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
// if the profile is empty, the expand button overlaps the header
|
||||
const showExpand = [
|
||||
'age',
|
||||
'bust',
|
||||
'cup',
|
||||
'eyes',
|
||||
'ethnicity',
|
||||
'hairColor',
|
||||
'hasPiercings',
|
||||
'hasTattoos',
|
||||
'height',
|
||||
'hip',
|
||||
'naturalBoobs',
|
||||
'origin',
|
||||
'residence',
|
||||
'waist',
|
||||
'weight',
|
||||
].some((attribute) => !!props.actor[attribute]);
|
||||
|
||||
const augmentationMap = {
|
||||
bbl: 'BBL',
|
||||
fat: 'fat transfer',
|
||||
lift: 'direct lift',
|
||||
lipo: 'lipo without BBL',
|
||||
filler: 'filler',
|
||||
mms: 'MMS',
|
||||
over: 'over-muscle',
|
||||
under: 'under-muscle',
|
||||
mammary: 'under breast',
|
||||
areolar: 'areolar',
|
||||
crescent: 'crescent areolar',
|
||||
lollipop: 'lollipop',
|
||||
axillary: 'armpit',
|
||||
umbilical: 'navel',
|
||||
};
|
||||
|
||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||
.filter((profile) => !!profile.description)
|
||||
.map((profile) => [profile.descriptionHash, {
|
||||
text: profile.description,
|
||||
entity: profile.entity,
|
||||
}])));
|
||||
|
||||
function getSocialUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (pageContext.env.socials.urls[social.platform]) {
|
||||
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const socials = props.actor.socials
|
||||
.filter((social) => social.platform)
|
||||
.slice(0, 10)
|
||||
.map((social) => ({
|
||||
...social,
|
||||
handle: social.url
|
||||
? new URL(social.url).hostname
|
||||
: social.handle,
|
||||
}));
|
||||
|
||||
const aliases = Object
|
||||
.entries(props.actor.aliases.reduce((acc, alias) => {
|
||||
acc[alias.name] = (props.actor[alias.name] || 0) + 1;
|
||||
return acc;
|
||||
}, {}))
|
||||
.toSorted(([, countA], [, countB]) => countB - countA)
|
||||
.map(([alias]) => alias)
|
||||
.filter((alias) => alias !== props.actor.name);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="profile"
|
||||
@@ -20,6 +119,15 @@
|
||||
</div>
|
||||
|
||||
<ul class="bio nolist">
|
||||
<li
|
||||
v-if="aliases.length > 0"
|
||||
class="bio-item"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="at-sign" />Aliases</dfn>
|
||||
|
||||
<span class="orientation">{{ aliases.join(', ') }}</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="actor.dateOfBirth"
|
||||
class="bio-item"
|
||||
@@ -44,7 +152,7 @@
|
||||
<dfn class="bio-label"><Icon icon="cake" />Age</dfn>
|
||||
|
||||
<span
|
||||
:title="'Exact date of birth or age unknown'"
|
||||
title="Exact date of birth or age unknown"
|
||||
class="birthdate"
|
||||
>> {{ actor.age }}</span>
|
||||
</li>
|
||||
@@ -80,10 +188,17 @@
|
||||
<span
|
||||
v-if="actor.origin.city"
|
||||
class="city"
|
||||
>{{ actor.origin.city }}</span><span
|
||||
>{{ actor.origin.city }}</span>
|
||||
|
||||
<span
|
||||
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
|
||||
class="state"
|
||||
>{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span>
|
||||
>
|
||||
{{ actor.origin.city
|
||||
? [',', actor.origin.state].join(' ')
|
||||
: actor.origin.state
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="actor.origin.country"
|
||||
@@ -105,7 +220,7 @@
|
||||
class="bio-item residence"
|
||||
:class="{ hideable: !!actor.origin }"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
|
||||
<dfn class="bio-label"><Icon icon="location" />{{ actor.dateOfDeath ? 'Lived' : 'Lives' }} in</dfn>
|
||||
|
||||
<span>
|
||||
<span
|
||||
@@ -146,7 +261,7 @@
|
||||
{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
||||
<Icon
|
||||
v-if="actor.naturalBoobs === false || actor.naturalButt === false"
|
||||
v-tooltip="[actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', ')"
|
||||
v-tooltip="{ content: [actor.naturalBoobs === false ? 'Enhanced boobs' : null, actor.naturalButt === false ? 'Enhanced butt' : null].filter(Boolean).join(', '), ariaId: enhancedTooltipId }"
|
||||
icon="magic-wand2"
|
||||
class="enhanced"
|
||||
/>
|
||||
@@ -168,7 +283,7 @@
|
||||
actor.boobsVolume && `${actor.boobsVolume}cc`,
|
||||
augmentationMap[actor.boobsPlacement] || actor.boobsPlacement,
|
||||
augmentationMap[actor.boobsIncision] || actor.boobsIncision,
|
||||
augmentationMap[actor.boobsImplant] || actor.boobsImplant
|
||||
augmentationMap[actor.boobsImplant] || actor.boobsImplant,
|
||||
].filter(Boolean).join(' ')"
|
||||
class="augmentations-section"
|
||||
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
||||
@@ -208,7 +323,7 @@
|
||||
<span>
|
||||
<Icon
|
||||
v-if="actor.isCircumcised"
|
||||
:title="'Circumcised'"
|
||||
title="Circumcised"
|
||||
icon="pagebreak"
|
||||
class="circumcised"
|
||||
/>
|
||||
@@ -312,10 +427,10 @@
|
||||
<a
|
||||
v-for="social in socials"
|
||||
:key="`social-${social.id}`"
|
||||
v-tooltip="{ content: social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url, ariaId: `social-${social.id}` }"
|
||||
:href="getSocialUrl(social)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
:title="social.platform || social.url"
|
||||
class="social ellipsis"
|
||||
>
|
||||
<Icon
|
||||
@@ -339,7 +454,9 @@
|
||||
:class="`icon-social icon-${social.platform} icon-generic`"
|
||||
/>
|
||||
|
||||
<!--
|
||||
<template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }}
|
||||
-->
|
||||
</a>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -356,7 +473,7 @@
|
||||
:href="`/actor/edit/${actor.id}/${actor.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Edit bio</a>
|
||||
>Edit</a>
|
||||
|
||||
<a
|
||||
:href="`/actor/revs/${actor.id}/${actor.slug}`"
|
||||
@@ -420,92 +537,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const expanded = ref(false);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, env } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
// if the profile is empty, the expand button overlaps the header
|
||||
const showExpand = [
|
||||
'age',
|
||||
'bust',
|
||||
'cup',
|
||||
'eyes',
|
||||
'ethnicity',
|
||||
'hairColor',
|
||||
'hasPiercings',
|
||||
'hasTattoos',
|
||||
'height',
|
||||
'hip',
|
||||
'naturalBoobs',
|
||||
'origin',
|
||||
'residence',
|
||||
'waist',
|
||||
'weight',
|
||||
].some((attribute) => !!props.actor[attribute]);
|
||||
|
||||
const augmentationMap = {
|
||||
bbl: 'BBL',
|
||||
fat: 'fat transfer',
|
||||
lift: 'direct lift',
|
||||
lipo: 'lipo without BBL',
|
||||
filler: 'filler',
|
||||
mms: 'MMS',
|
||||
over: 'over-muscle',
|
||||
under: 'under-muscle',
|
||||
mammary: 'under breast',
|
||||
areolar: 'areolar',
|
||||
crescent: 'crescent areolar',
|
||||
lollipop: 'lollipop',
|
||||
axillary: 'armpit',
|
||||
umbilical: 'navel',
|
||||
};
|
||||
|
||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||
.filter((profile) => !!profile.description)
|
||||
.map((profile) => [profile.descriptionHash, {
|
||||
text: profile.description,
|
||||
entity: profile.entity,
|
||||
}])));
|
||||
|
||||
function getSocialUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (pageContext.env.socials.urls[social.platform]) {
|
||||
return formatTemplate(pageContext.env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const socials = props.actor.socials.map((social) => ({
|
||||
...social,
|
||||
handle: social.url
|
||||
? new URL(social.url).hostname
|
||||
: social.handle,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header-gender {
|
||||
display: inline-flex;
|
||||
@@ -735,19 +766,23 @@ const socials = props.actor.socials.map((social) => ({
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/*
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0 0;
|
||||
overflow: hidden;
|
||||
*/
|
||||
gap: .25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.social {
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .1rem .5rem;
|
||||
justify-content: center;
|
||||
padding: .75rem .75rem;
|
||||
border-radius: .25rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@@ -755,10 +790,6 @@ const socials = props.actor.socials.map((social) => ({
|
||||
font-weight: normal;
|
||||
background: var(--highlight-weak-40);
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.icon-generic {
|
||||
fill: var(--highlight);
|
||||
}
|
||||
@@ -783,6 +814,7 @@ const socials = props.actor.socials.map((social) => ({
|
||||
.link {
|
||||
color: inherit;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,7 +886,7 @@ const socials = props.actor.socials.map((social) => ({
|
||||
display: none;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
bottom: -.25rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="gender"
|
||||
@@ -8,15 +17,6 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
gender: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gender {
|
||||
&.female .icon {
|
||||
|
||||
304
components/actors/merge.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, useId } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
|
||||
const props = defineProps({
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'merged']);
|
||||
|
||||
const targetActor = ref(null);
|
||||
const actorInput = ref(null);
|
||||
const comment = ref(null);
|
||||
const actorQuery = ref('');
|
||||
const actorResults = ref([]);
|
||||
const submitted = ref(false);
|
||||
const merged = ref(false);
|
||||
const searchDropdownId = useId();
|
||||
const conflictTooltipId = useId();
|
||||
|
||||
const sourceTargetConflict = computed(() => props.actors.some((actor) => actor.id === targetActor.value?.id));
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: actorQuery.value, // return partial matches
|
||||
limit: 10,
|
||||
global: true,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
function getActorNames() {
|
||||
if (props.actors.length > 1) {
|
||||
return `${props.actors.length} actors`;
|
||||
}
|
||||
|
||||
if (props.actors[0].entity) {
|
||||
return `${props.actors[0].name} (${props.actors[0].entity.name})`;
|
||||
}
|
||||
|
||||
return props.actors[0].name;
|
||||
}
|
||||
|
||||
async function merge() {
|
||||
submitted.value = true;
|
||||
|
||||
await post(`/actors/${targetActor.value.id}/merge`, {
|
||||
actorIds: props.actors.map((actor) => actor.id),
|
||||
comment: comment.value || null,
|
||||
}, {
|
||||
successFeedback: `Merged ${getActorNames()} into ${targetActor.value.name}`,
|
||||
errorFeedback: `Failed to merge ${getActorNames()} into ${targetActor.value.name}`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
submitted.value = false;
|
||||
merged.value = true;
|
||||
|
||||
emit('merged');
|
||||
|
||||
// emit('close');
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
targetActor.value = actor;
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
actorInput.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:title="`Merge ${actors.length === 1 ? `'${actors[0].name}'` : `${actors.length} actors`}`"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div
|
||||
v-if="merged"
|
||||
class="dialog-body"
|
||||
>
|
||||
Merge successful
|
||||
|
||||
<ul class="options">
|
||||
<li class="option">
|
||||
<a
|
||||
href=""
|
||||
class="link"
|
||||
>Reload page</a>
|
||||
</li>
|
||||
|
||||
<li class="option">
|
||||
<a
|
||||
:href="`/actor/${targetActor?.id}/${targetActor?.slug}`"
|
||||
class="link"
|
||||
>Go to #{{ targetActor?.id }} {{ targetActor?.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="dialog-body"
|
||||
>
|
||||
<ul class="actors nolist">
|
||||
<li
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="actor"
|
||||
>
|
||||
<span class="source">
|
||||
<strong class="source-name">{{ actor.name }}<template v-if="actor.entity"> ({{ actor.entity.name }})</template></strong>
|
||||
<span class="source-id">#{{ actor.id }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<span class="path">merging into</span>
|
||||
|
||||
<div
|
||||
v-if="targetActor"
|
||||
class="target"
|
||||
>
|
||||
<strong class="target-name">
|
||||
<span class="target-id">#{{ targetActor.id }}</span>
|
||||
{{ targetActor.name }}
|
||||
</strong>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
@click="targetActor = null"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<VDropdown
|
||||
:aria-id="searchDropdownId"
|
||||
:triggers="[]"
|
||||
:shown="actorResults.length > 0"
|
||||
:auto-hide="false"
|
||||
placement="auto"
|
||||
>
|
||||
<input
|
||||
ref="actorInput"
|
||||
v-model="actorQuery"
|
||||
class="input"
|
||||
placeholder="Search target actor"
|
||||
@input="searchActors"
|
||||
>
|
||||
|
||||
<template #popper>
|
||||
<ul
|
||||
class="results nolist"
|
||||
>
|
||||
<li
|
||||
v-for="actorResult in actorResults"
|
||||
:key="`actor-result-${actorResult.id}`"
|
||||
v-close-popper
|
||||
class="result-item"
|
||||
@click="selectActor(actorResult)"
|
||||
>
|
||||
<div class="result-label">
|
||||
<span class="result-id">#{{ actorResult.id }}</span> {{ actorResult.name }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
|
||||
<textarea
|
||||
v-model="comment"
|
||||
placeholder="Optional comment"
|
||||
rows="2"
|
||||
class="input comment"
|
||||
/>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<Ellipsis v-if="submitted" />
|
||||
|
||||
<button
|
||||
v-else
|
||||
v-tooltip="sourceTargetConflict && { content: 'Cannot merge actor profile into itself', ariaId: conflictTooltipId }"
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
:disabled="!targetActor || sourceTargetConflict"
|
||||
@click="merge"
|
||||
>Merge</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.actors {
|
||||
max-height: 15rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.actor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
|
||||
&.comment {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.path {
|
||||
color: var(--glass-strong-20);
|
||||
}
|
||||
|
||||
.target {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
padding: .25rem .75rem;
|
||||
fill: var(--glass);
|
||||
|
||||
&:hover {
|
||||
fill: var(--error);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.source-id,
|
||||
.target-id {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.source {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.results {
|
||||
padding: .25rem 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
padding: .25rem .5rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.load-container {
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.options {
|
||||
padding: 0 1.5rem;
|
||||
margin: 0;
|
||||
line-height: 1.75;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,37 @@
|
||||
<script setup>
|
||||
import { ref, useId } from 'vue';
|
||||
|
||||
import { get } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['actor']);
|
||||
const dropdownId = useId();
|
||||
const actors = ref([]);
|
||||
const query = ref(null);
|
||||
const queryInput = ref(null);
|
||||
|
||||
async function search() {
|
||||
const data = await get('/actors', { q: query.value });
|
||||
|
||||
actors.value = data.actors;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
setTimeout(() => {
|
||||
queryInput.value?.focus();
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDropdown
|
||||
:aria-id="dropdownId"
|
||||
:disabled="disabled"
|
||||
class="trigger"
|
||||
@show="focus"
|
||||
@@ -36,38 +68,6 @@
|
||||
</VDropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { get } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const actors = ref([]);
|
||||
const query = ref(null);
|
||||
const queryInput = ref(null);
|
||||
|
||||
defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['actor']);
|
||||
|
||||
async function search() {
|
||||
const data = await get('/actors', { q: query.value });
|
||||
|
||||
actors.value = data.actors;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
setTimeout(() => {
|
||||
queryInput.value?.focus();
|
||||
}, 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.trigger {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
<script setup>
|
||||
import { inject, ref, useId } from 'vue';
|
||||
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
import Gender from './gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
||||
const entityTooltipId = useId();
|
||||
const aliasTooltipId = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tile"
|
||||
@@ -41,7 +67,7 @@
|
||||
|
||||
<span
|
||||
v-if="actor.ageAtDeath"
|
||||
:title="`Passed ${formatDate(actor.ageAtDeath, 'MMMM d, yyyy')}`"
|
||||
:title="`Passed ${formatDate(actor.dateOfDeath, 'MMMM d, yyyy')}`"
|
||||
class="age age-death"
|
||||
>{{ actor.ageAtDeath }}</span>
|
||||
|
||||
@@ -74,42 +100,32 @@
|
||||
</div>
|
||||
|
||||
<span class="label">
|
||||
<span class="name ellipsis">{{ actor.name }}</span>
|
||||
<span
|
||||
class="name ellipsis"
|
||||
:style="{ 'font-size': `${Math.max(0.9 + Math.min((17 - actor.name.length), 0) * 0.06, 0.65)}rem` }"
|
||||
>{{ actor.name }}</span>
|
||||
|
||||
<img
|
||||
v-if="actor.entity"
|
||||
v-tooltip="actor.entity.name"
|
||||
v-tooltip="{ content: actor.entity.name, ariaId: entityTooltipId }"
|
||||
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
||||
class="favicon"
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-if="actor.alias && actor.alias.name !== actor.name"
|
||||
v-tooltip="{
|
||||
content: `Credited as '${actor.alias.name}'`,
|
||||
triggers: ['hover', 'click'],
|
||||
ariaId: aliasTooltipId,
|
||||
}"
|
||||
icon="at-sign"
|
||||
class="alias"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
import Gender from './gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { user, restriction } = pageContext;
|
||||
const pageStash = pageContext.pageProps.stash;
|
||||
const currentStash = pageStash || pageContext.assets?.primaryStash;
|
||||
|
||||
const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id === currentStash?.id));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tile {
|
||||
display: flex;
|
||||
@@ -128,6 +144,10 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
||||
:deep(.bookmarks) .icon:not(.favorited):not(:hover) {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
|
||||
.menu {
|
||||
fill: var(--text-light);
|
||||
}
|
||||
}
|
||||
|
||||
&.unstashed {
|
||||
@@ -135,7 +155,16 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: .5rem .75rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
fill: var(--highlight-strong-20);
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -149,7 +178,7 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
||||
}
|
||||
|
||||
.name {
|
||||
padding: .35rem .25rem .35rem .5rem;
|
||||
padding: 0 .25rem 0 .5rem;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
@@ -246,4 +275,11 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
||||
height: .75rem;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
|
||||
.alias {
|
||||
height: 100%;
|
||||
fill: var(--glass-weak-20);
|
||||
padding: 0 .25rem;
|
||||
cursor: help;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import { post } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
async function initCaches() {
|
||||
await post('/caches', null, {
|
||||
successFeedback: 'Reloaded caches',
|
||||
errorFeedback: 'Failed to reload caches',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<nav class="nav">
|
||||
<ul class="nav-items nolist">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="/admin/actors"
|
||||
class="nav-link nolink"
|
||||
:class="{ active: pageContext.routeParams.section === 'actors' }"
|
||||
>Actors</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="/admin/revisions/scenes"
|
||||
@@ -25,7 +49,23 @@
|
||||
:class="{ active: pageContext.routeParams.section === 'entities' }"
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="pageContext.user.role === 'admin'"
|
||||
class="nav-item"
|
||||
>
|
||||
<a
|
||||
href="/admin/emails"
|
||||
class="nav-link nolink"
|
||||
:class="{ active: pageContext.urlPathname.startsWith('/admin/emails') }"
|
||||
>E-mails</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="initCaches"
|
||||
><Icon icon="database-refresh" />Reload caches</button>
|
||||
</nav>
|
||||
|
||||
<div class="content">
|
||||
@@ -34,12 +74,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
@@ -50,6 +84,7 @@ const pageContext = inject('pageContext');
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1rem .75rem 1rem;
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
margin-bottom: .25rem;
|
||||
|
||||
@@ -1,8 +1,133 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import AlertDialog from '#/components/alerts/create.vue';
|
||||
|
||||
import { del, get } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
const alerts = ref(pageContext.pageProps.alerts);
|
||||
const showAlertDialog = ref(false);
|
||||
const done = ref(true);
|
||||
|
||||
const query = ref('');
|
||||
const filterActors = ref(false);
|
||||
const filterEntities = ref(false);
|
||||
const filterTags = ref(false);
|
||||
const filterExpressions = ref(false);
|
||||
const filterScenes = ref(false);
|
||||
const filterCombined = ref(null);
|
||||
|
||||
const filteredAlerts = computed(() => {
|
||||
const queryRegex = new RegExp(query.value, 'i');
|
||||
|
||||
return alerts.value.filter((alert) => {
|
||||
if (filterActors.value && alert.actors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterEntities.value && alert.entities.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterTags.value && alert.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterExpressions.value && alert.matches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterScenes.value && alert.scenes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCombined.value === false && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches, ...alert.scenes].length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCombined.value === true && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches, ...alert.scenes].length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (queryRegex.test(alert.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.actors.some((actor) => queryRegex.test(actor.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.tags.some((tag) => queryRegex.test(tag.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.entities.some((entity) => queryRegex.test(entity.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.matches.some((match) => queryRegex.test(match.expression))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.scenes.some((scene) => queryRegex.test(scene.title))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
async function reloadAlerts() {
|
||||
alerts.value = await get('/alerts');
|
||||
}
|
||||
|
||||
async function removeAlert(alert) {
|
||||
if (done.value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to remove alert #${alert.id}?`)) { // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
done.value = false;
|
||||
|
||||
const alertLabel = [
|
||||
...alert.actors.map((actor) => actor.name),
|
||||
...alert.tags.map((tag) => tag.name),
|
||||
...alert.entities.map((entity) => entity.name),
|
||||
...alert.matches.map((match) => match.expression),
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
try {
|
||||
await del(`/alerts/${alert.id}`, {
|
||||
undoFeedback: `Removed alert for '${alertLabel}'`,
|
||||
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
await reloadAlerts();
|
||||
}
|
||||
finally {
|
||||
done.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const filterCombinedStates = [null, true, false];
|
||||
|
||||
function toggleFilterCombined() {
|
||||
const index = filterCombinedStates.indexOf(filterCombined.value);
|
||||
|
||||
filterCombined.value = filterCombinedStates[(index + 1) % filterCombinedStates.length];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="profile-section">
|
||||
<div class="section-header">
|
||||
<h3 class="heading">Alerts</h3>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="showAlertDialog = true"
|
||||
@@ -22,37 +147,45 @@
|
||||
|
||||
<div class="filters-section">
|
||||
<Icon
|
||||
v-tooltip="'Only show actor alerts'"
|
||||
icon="star"
|
||||
title="Only show actor alerts"
|
||||
class="noselect"
|
||||
:class="{ active: filterActors }"
|
||||
@click="filterActors = !filterActors"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Only show tag alerts'"
|
||||
icon="price-tags"
|
||||
title="Only show tag alerts"
|
||||
class="noselect"
|
||||
:class="{ active: filterTags }"
|
||||
@click="filterTags = !filterTags"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Only show channel alerts'"
|
||||
icon="device_hub"
|
||||
title="Only show channel alerts"
|
||||
class="noselect"
|
||||
:class="{ active: filterEntities }"
|
||||
@click="filterEntities = !filterEntities"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Only show expression alerts'"
|
||||
icon="regexp"
|
||||
title="Only show expression alerts"
|
||||
class="noselect"
|
||||
:class="{ active: filterExpressions }"
|
||||
@click="filterExpressions = !filterExpressions"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Only show scene release date alerts'"
|
||||
icon="clapboard-play"
|
||||
class="noselect"
|
||||
:class="{ active: filterScenes }"
|
||||
@click="filterScenes = !filterScenes"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="filterCombined === false"
|
||||
icon="target3"
|
||||
@@ -163,50 +296,64 @@
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-for="scene in alert.scenes"
|
||||
:key="`alert-${alert.id}-${scene.id}`"
|
||||
class="alert-scene"
|
||||
>
|
||||
<time
|
||||
:datetime="scene.date.toISOString()"
|
||||
class="alert-date"
|
||||
>{{ format(scene.date, 'yyyy-MM-dd') }}</time>
|
||||
|
||||
<a
|
||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||
class="alert-value alert-title ellipsis link"
|
||||
>{{ scene.title }}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="alert-meta">
|
||||
<div class="alert-triggers">
|
||||
<Icon
|
||||
v-if="alert.notify && alert.isFromPreset"
|
||||
v-tooltip="'Notify in traxxx, added as quick alert'"
|
||||
v-tooltip="{ content: alert.notify ? 'Notify in traxxx, added as quick alert' : undefined, ariaId: `alert-notify-preset-${alert.id}` }"
|
||||
icon="bell-plus"
|
||||
class="trigger"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else-if="alert.notify"
|
||||
v-tooltip="'Notify in traxxx'"
|
||||
v-else
|
||||
v-tooltip="{ content: alert.notify ? 'Notify in traxxx' : undefined, ariaId: `alert-notify-${alert.id}` }"
|
||||
icon="bell2"
|
||||
class="trigger"
|
||||
:class="{ trigger: alert.notify }"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'E-mail me'"
|
||||
icon="envelop5"
|
||||
:class="{ trigger: alert.email }"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-if="alert.stashes.some((stash) => !stash.isPrimary)"
|
||||
v-tooltip="`Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`"
|
||||
v-tooltip="{ content: `Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`, ariaId: `alert-stashes-${alert.id}` }"
|
||||
icon="folder-heart"
|
||||
class="trigger"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else-if="alert.stashes.length > 0"
|
||||
v-tooltip="alert.stashes.length > 0 ? 'Add to Favorites' : undefined"
|
||||
v-else
|
||||
v-tooltip="alert.stashes.length > 0 ? { content: 'Add to Favorites', ariaId: `alert-favorites-${alert.id}` } : undefined"
|
||||
icon="heart7"
|
||||
class="trigger"
|
||||
:class="{ trigger: alert.stashes.length > 0 }"
|
||||
/>
|
||||
|
||||
<!--
|
||||
<Icon
|
||||
icon="envelop5"
|
||||
title="E-mail me"
|
||||
:class="{ trigger: alert.email }"
|
||||
/>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="alert-actions">
|
||||
<span
|
||||
v-tooltip="format(alert.createdAt, 'yyyy-MM-dd hh:mm')"
|
||||
v-tooltip="{ content: format(alert.createdAt, 'yyyy-MM-dd hh:mm'), ariaId: `alert-created-${alert.id}` }"
|
||||
class="alert-id"
|
||||
title="Alert ID"
|
||||
>#{{ alert.id }}</span>
|
||||
@@ -227,124 +374,11 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import AlertDialog from '#/components/alerts/create.vue';
|
||||
|
||||
import { get, del } from '#/src/api.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
const alerts = ref(pageContext.pageProps.alerts);
|
||||
const showAlertDialog = ref(false);
|
||||
const done = ref(true);
|
||||
|
||||
const query = ref('');
|
||||
const filterActors = ref(false);
|
||||
const filterEntities = ref(false);
|
||||
const filterTags = ref(false);
|
||||
const filterExpressions = ref(false);
|
||||
const filterCombined = ref(null);
|
||||
|
||||
const filteredAlerts = computed(() => {
|
||||
const queryRegex = new RegExp(query.value, 'i');
|
||||
|
||||
return alerts.value.filter((alert) => {
|
||||
if (filterActors.value && alert.actors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterEntities.value && alert.entities.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterTags.value && alert.tags.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterExpressions.value && alert.matches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCombined.value === false && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterCombined.value === true && [...alert.actors, ...alert.entities, ...alert.tags, ...alert.matches].length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (queryRegex.test(alert.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.actors.some((actor) => queryRegex.test(actor.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.tags.some((tag) => queryRegex.test(tag.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.entities.some((entity) => queryRegex.test(entity.name))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alert.matches.some((match) => queryRegex.test(match.expression))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
async function reloadAlerts() {
|
||||
alerts.value = await get('/alerts');
|
||||
}
|
||||
|
||||
async function removeAlert(alert) {
|
||||
if (done.value === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to remove alert #${alert.id}?`)) { // eslint-disable-line no-restricted-globals, no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
done.value = false;
|
||||
|
||||
const alertLabel = [
|
||||
...alert.actors.map((actor) => actor.name),
|
||||
...alert.tags.map((tag) => tag.name),
|
||||
...alert.entities.map((entity) => entity.name),
|
||||
...alert.matches.map((match) => match.expression),
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
try {
|
||||
await del(`/alerts/${alert.id}`, {
|
||||
undoFeedback: `Removed alert for '${alertLabel}'`,
|
||||
errorFeedback: `Failed to remove alert for '${alertLabel}'`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
await reloadAlerts();
|
||||
} finally {
|
||||
done.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const filterCombinedStates = [null, true, false];
|
||||
|
||||
function toggleFilterCombined() {
|
||||
const index = filterCombinedStates.indexOf(filterCombined.value);
|
||||
|
||||
filterCombined.value = filterCombinedStates[(index + 1) % filterCombinedStates.length];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-section .section-header {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.alerts {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
@@ -475,6 +509,16 @@ function toggleFilterCombined() {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.alert-scene {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
.alert-date {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alert-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,181 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import { inject, ref, useId } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const props = defineProps({
|
||||
presetActors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
presetEntities: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
presetTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
presetScenes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['alert', 'close']);
|
||||
|
||||
const {
|
||||
assets,
|
||||
user,
|
||||
env,
|
||||
} = inject('pageContext');
|
||||
|
||||
const actors = ref(props.presetActors);
|
||||
const actorResults = ref([]);
|
||||
const actorQuery = ref('');
|
||||
|
||||
const entities = ref(props.presetEntities);
|
||||
const entityResults = ref([]);
|
||||
const entityQuery = ref('');
|
||||
|
||||
const tags = ref(props.presetTags);
|
||||
const tagResults = ref([]);
|
||||
const tagQuery = ref('');
|
||||
|
||||
const scenes = ref(props.presetScenes);
|
||||
|
||||
const matches = ref([]);
|
||||
const matchProperty = ref('title');
|
||||
const matchExpression = ref('');
|
||||
|
||||
const fieldsAnd = ref(true);
|
||||
const actorAnd = ref(true);
|
||||
const tagAnd = ref(true);
|
||||
const matchAnd = ref(true);
|
||||
|
||||
const inputs = {};
|
||||
|
||||
const fieldsLogicTooltipId = useId();
|
||||
const addActorDropdownId = useId();
|
||||
const addTagDropdownId = useId();
|
||||
const addEntityDropdownId = useId();
|
||||
const addMatchDropdownId = useId();
|
||||
const addStashDropdownId = useId();
|
||||
|
||||
const notify = ref(JSON.parse(localStorage.getItem('alert_notify')) ?? true);
|
||||
const email = ref(JSON.parse(localStorage.getItem('alert_email')) ?? false);
|
||||
|
||||
const stashes = ref([]);
|
||||
|
||||
async function createAlert() {
|
||||
const alertLabel = [
|
||||
...actors.value.map((actor) => actor.name),
|
||||
...tags.value.map((tag) => tag.name),
|
||||
...entities.value.map((entity) => entity.name),
|
||||
...matches.value.map((match) => match.expression),
|
||||
...scenes.value.map((scene) => scene.title),
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
const newAlert = await post('/alerts', {
|
||||
all: fieldsAnd.value,
|
||||
allActors: actorAnd.value,
|
||||
allTags: tagAnd.value,
|
||||
allMatches: matchAnd.value,
|
||||
actors: actors.value.map((actor) => actor.id),
|
||||
tagIds: tags.value.map((tag) => tag.id),
|
||||
sceneIds: scenes.value.map((scene) => scene.id),
|
||||
matches: matches.value,
|
||||
entityIds: entities.value.map((entity) => entity.id),
|
||||
notify: notify.value,
|
||||
email: email.value,
|
||||
stashes: stashes.value.map((stash) => stash.id),
|
||||
}, {
|
||||
successFeedback: `Alert for '${alertLabel}' set`,
|
||||
errorFeedback: `Failed to set alert for '${alertLabel}'`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
emit('alert', newAlert);
|
||||
emit('close', true);
|
||||
}
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: `${actorQuery.value}*`, // return partial matches
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
async function searchEntities() {
|
||||
const res = await get('/entities', {
|
||||
query: entityQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
entityResults.value = res;
|
||||
}
|
||||
|
||||
async function searchTags() {
|
||||
const res = await get('/tags', {
|
||||
query: tagQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
tagResults.value = res;
|
||||
}
|
||||
|
||||
function focusInput(target) {
|
||||
setTimeout(() => {
|
||||
inputs[target]?.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
actors.value.push(actor);
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
function selectEntity(entity) {
|
||||
entities.value.push(entity);
|
||||
entityQuery.value = '';
|
||||
entityResults.value = [];
|
||||
}
|
||||
|
||||
function selectTag(tag) {
|
||||
tags.value.push(tag);
|
||||
tagQuery.value = '';
|
||||
tagResults.value = [];
|
||||
}
|
||||
|
||||
function addMatch() {
|
||||
matches.value.push({
|
||||
property: matchProperty.value,
|
||||
expression: matchExpression.value,
|
||||
});
|
||||
|
||||
matchProperty.value = 'title';
|
||||
matchExpression.value = '';
|
||||
}
|
||||
|
||||
function selectStash(selectedStash) {
|
||||
if (!stashes.value.some((stash) => stash.id === selectedStash.id)) {
|
||||
stashes.value.push(selectedStash);
|
||||
}
|
||||
}
|
||||
|
||||
function rememberThen(domain, checked) {
|
||||
localStorage.setItem(`alert_${domain}`, checked);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
title="New alert"
|
||||
@@ -7,14 +185,40 @@
|
||||
class="dialog-body"
|
||||
@submit.prevent="createAlert"
|
||||
>
|
||||
<div class="dialog-section">
|
||||
<div
|
||||
v-if="scenes.length > 0"
|
||||
class="dialog-section"
|
||||
>
|
||||
<div class="section-header">
|
||||
<h3 class="heading">WHEN RELEASED</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="scene in scenes"
|
||||
:key="`alert-scene-${scene.id}`"
|
||||
class="scene"
|
||||
>
|
||||
<span class="scene-title">{{ scene.title }}</span>
|
||||
|
||||
<span class="scene-details">
|
||||
<strong class="scene-date"><time :datetime="scene.date.toISOString()">{{ format(scene.date, 'yyyy-MM-dd') }}</time></strong> on
|
||||
|
||||
<span class="scene-channel">{{ scene.channel?.name || scene.network?.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="dialog-section"
|
||||
>
|
||||
<div class="section-header">
|
||||
<h3 class="heading">IF</h3>
|
||||
</div>
|
||||
|
||||
<div class="field actors">
|
||||
<span
|
||||
v-tooltip="fieldsAnd ? 'The alert is triggered if all fields are matched.' : 'The alert is triggered if any of the fields are matched.'"
|
||||
v-tooltip="{ content: fieldsAnd ? 'The alert is triggered if all fields are matched.' : 'The alert is triggered if any of the fields are matched.', ariaId: fieldsLogicTooltipId }"
|
||||
class="field-logic fields-logic noselect"
|
||||
@click="fieldsAnd = !fieldsAnd"
|
||||
>
|
||||
@@ -56,7 +260,11 @@
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown @show="focusActorInput">
|
||||
<VDropdown
|
||||
:aria-id="addActorDropdownId"
|
||||
placement="auto"
|
||||
@show="focusInput('actor')"
|
||||
>
|
||||
<button
|
||||
class="button"
|
||||
type="button"
|
||||
@@ -64,7 +272,7 @@
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="actorInput"
|
||||
:ref="(el) => inputs.actor = el"
|
||||
v-model="actorQuery"
|
||||
class="input"
|
||||
@input="searchActors"
|
||||
@@ -131,7 +339,11 @@
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<VDropdown
|
||||
placement="auto"
|
||||
:aria-id="addTagDropdownId"
|
||||
@show="focusInput('tag')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@@ -139,7 +351,7 @@
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="tagInput"
|
||||
:ref="(el) => inputs.tag = el"
|
||||
v-model="tagQuery"
|
||||
class="input"
|
||||
@input="searchTags"
|
||||
@@ -177,6 +389,7 @@
|
||||
content: 'Scenes are only associated to one channel, \'AND\' would never match.',
|
||||
triggers: ['click'],
|
||||
autoHide: true,
|
||||
ariaId: `entity-and-tooltip-${entity.id}`,
|
||||
}"
|
||||
class="field-logic"
|
||||
>OR</div>
|
||||
@@ -194,7 +407,11 @@
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<VDropdown
|
||||
placement="auto"
|
||||
:aria-id="addEntityDropdownId"
|
||||
@show="focusInput('entity')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@@ -202,7 +419,7 @@
|
||||
|
||||
<template #popper>
|
||||
<input
|
||||
ref="entityInput"
|
||||
:ref="(el) => inputs.entity = el"
|
||||
v-model="entityQuery"
|
||||
class="input"
|
||||
@input="searchEntities"
|
||||
@@ -256,7 +473,10 @@
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<VDropdown
|
||||
:aria-id="addMatchDropdownId"
|
||||
@show="focusInput('expression')"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
@@ -273,6 +493,7 @@
|
||||
</select>
|
||||
|
||||
<input
|
||||
:ref="(el) => inputs.expression = el"
|
||||
v-model="matchExpression"
|
||||
placeholder="Expression, // for regex"
|
||||
class="input"
|
||||
@@ -292,22 +513,26 @@
|
||||
<span>Notify me in traxxx</span>
|
||||
<Checkbox
|
||||
:checked="notify"
|
||||
@change="(checked) => notify = checked"
|
||||
@change="(checked) => { notify = checked; rememberThen('notify', checked); }"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!--
|
||||
<label class="field email">
|
||||
<label
|
||||
v-if="env.emailEnabled && user.email"
|
||||
class="field email"
|
||||
>
|
||||
<span>E-mail me</span>
|
||||
|
||||
<Checkbox
|
||||
:checked="email"
|
||||
@change="(checked) => email = checked"
|
||||
@change="(checked) => { email = checked; rememberThen('email', checked); }"
|
||||
/>
|
||||
</label>
|
||||
-->
|
||||
|
||||
<div class="stash">
|
||||
<div
|
||||
v-if="scenes.length === 0"
|
||||
class="stash"
|
||||
>
|
||||
<ul class="field-items nolist noselect">
|
||||
<li
|
||||
v-for="stash in stashes"
|
||||
@@ -340,7 +565,7 @@
|
||||
</li>
|
||||
|
||||
<li class="field-add">
|
||||
<VDropdown>
|
||||
<VDropdown :aria-id="addStashDropdownId">
|
||||
<button
|
||||
type="button"
|
||||
class="button field-add"
|
||||
@@ -375,161 +600,6 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
const { assets } = inject('pageContext');
|
||||
|
||||
const props = defineProps({
|
||||
presetActors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
presetEntities: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
presetTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['alert', 'close']);
|
||||
|
||||
const actors = ref(props.presetActors);
|
||||
const actorResults = ref([]);
|
||||
const actorInput = ref(null);
|
||||
const actorQuery = ref('');
|
||||
|
||||
const entities = ref(props.presetEntities);
|
||||
const entityResults = ref([]);
|
||||
const entityInput = ref(null);
|
||||
const entityQuery = ref('');
|
||||
|
||||
const tags = ref(props.presetTags);
|
||||
const tagResults = ref([]);
|
||||
const tagInput = ref(null);
|
||||
const tagQuery = ref('');
|
||||
|
||||
const matches = ref([]);
|
||||
const matchProperty = ref('title');
|
||||
const matchExpression = ref('');
|
||||
|
||||
const fieldsAnd = ref(true);
|
||||
const actorAnd = ref(true);
|
||||
const tagAnd = ref(true);
|
||||
const matchAnd = ref(true);
|
||||
|
||||
const notify = ref(true);
|
||||
const email = ref(false);
|
||||
|
||||
const stashes = ref([]);
|
||||
|
||||
async function createAlert() {
|
||||
const alertLabel = [
|
||||
...actors.value.map((actor) => actor.name),
|
||||
...tags.value.map((tag) => tag.name),
|
||||
...entities.value.map((entity) => entity.name),
|
||||
...matches.value.map((match) => match.expression),
|
||||
].filter(Boolean).join(', ');
|
||||
|
||||
const newAlert = await post('/alerts', {
|
||||
all: fieldsAnd.value,
|
||||
allActors: actorAnd.value,
|
||||
allTags: tagAnd.value,
|
||||
allMatches: matchAnd.value,
|
||||
actors: actors.value.map((actor) => actor.id),
|
||||
tagIds: tags.value.map((tag) => tag.id),
|
||||
matches: matches.value,
|
||||
entityIds: entities.value.map((entity) => entity.id),
|
||||
notify: notify.value,
|
||||
email: email.value,
|
||||
stashes: stashes.value.map((stash) => stash.id),
|
||||
}, {
|
||||
successFeedback: `Alert for '${alertLabel}' set`,
|
||||
errorFeedback: `Failed to set alert for '${alertLabel}'`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
emit('alert', newAlert);
|
||||
emit('close', true);
|
||||
}
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: `${actorQuery.value}*`, // return partial matches
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
async function searchEntities() {
|
||||
const res = await get('/entities', {
|
||||
query: entityQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
entityResults.value = res;
|
||||
}
|
||||
|
||||
async function searchTags() {
|
||||
const res = await get('/tags', {
|
||||
query: tagQuery.value,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
tagResults.value = res;
|
||||
}
|
||||
|
||||
function focusActorInput() {
|
||||
setTimeout(() => {
|
||||
actorInput.value.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
actors.value.push(actor);
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
function selectEntity(entity) {
|
||||
entities.value.push(entity);
|
||||
entityQuery.value = '';
|
||||
entityResults.value = [];
|
||||
}
|
||||
|
||||
function selectTag(tag) {
|
||||
tags.value.push(tag);
|
||||
tagQuery.value = '';
|
||||
tagResults.value = [];
|
||||
}
|
||||
|
||||
function addMatch() {
|
||||
matches.value.push({
|
||||
property: matchProperty.value,
|
||||
expression: matchExpression.value,
|
||||
});
|
||||
|
||||
matchProperty.value = 'title';
|
||||
matchExpression.value = '';
|
||||
}
|
||||
|
||||
function selectStash(selectedStash) {
|
||||
if (!stashes.value.some((stash) => stash.id === selectedStash.id)) {
|
||||
stashes.value.push(selectedStash);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 30rem;
|
||||
@@ -561,6 +631,20 @@ function selectStash(selectedStash) {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.scene {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.scene-title {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.scene-date {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,3 +1,37 @@
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
campaign: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
backdrop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const pageContext = inject('pageContext');
|
||||
const { restriction } = pageContext;
|
||||
|
||||
// console.log(props.campaign);
|
||||
|
||||
const bannerSrc = (() => {
|
||||
if (props.campaign.banner) {
|
||||
// if (props.campaign.banner.entity.type === 'network' || props.campaign.banner.entity.isIndependent || !props.campaign.banner.entity.parent) {
|
||||
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
||||
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||
}
|
||||
|
||||
if (props.campaign.banner.entity.type === 'channel' && props.campaign.banner.entity.parent?.type === 'network') {
|
||||
return `/banners/${props.campaign.banner.entity.parent.slug}/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="restriction"
|
||||
@@ -12,7 +46,6 @@
|
||||
|
||||
<iframe
|
||||
v-else-if="campaign?.banner?.type === 'html'"
|
||||
ref="iframe"
|
||||
:width="campaign.banner.width"
|
||||
:height="campaign.banner.height"
|
||||
:src="bannerSrc"
|
||||
@@ -45,40 +78,6 @@
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { restriction } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
campaign: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
backdrop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log(props.campaign);
|
||||
|
||||
const bannerSrc = (() => {
|
||||
if (props.campaign.banner) {
|
||||
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
|
||||
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||
}
|
||||
|
||||
if (props.campaign.banner.entity.type === 'channel' && props.campaign.banner.entity.parent?.type === 'network') {
|
||||
return `/banners/${props.campaign.banner.entity.parent.slug}/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.campaign {
|
||||
height: 100%;
|
||||
@@ -100,18 +99,8 @@ const bannerSrc = (() => {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: 4px; /* margin for drop shadow */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||
}
|
||||
|
||||
.dark .campaign-overlay {
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||
}
|
||||
|
||||
.campaign.backdrop .campaign-overlay {
|
||||
box-shadow: inset 0 0 5px var(--shadow-weak-20);
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
@@ -119,7 +108,21 @@ const bannerSrc = (() => {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak-20));
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
.campaign-overlay {
|
||||
padding: 4px; /* margin for drop shadow */
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(125%);
|
||||
}
|
||||
|
||||
.dark .campaign-overlay {
|
||||
backdrop-filter: blur(1rem) saturate(70%) brightness(75%);
|
||||
}
|
||||
|
||||
.campaign-banner {
|
||||
filter: drop-shadow(0 0 2px var(--shadow-weak-20));
|
||||
}
|
||||
}
|
||||
|
||||
.restricted {
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
confirmClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['open', 'close', 'event']);
|
||||
|
||||
function close() {
|
||||
if (!props.confirmClose || confirm('You have unchanged changes, are you sure you want to close the dialog?')) { // eslint-disable-line no-alert
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => emit('open'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="#container">
|
||||
<div
|
||||
@@ -6,6 +33,7 @@
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
v-bind="$attrs"
|
||||
@click.stop
|
||||
>
|
||||
<div class="dialog-header">
|
||||
@@ -24,31 +52,6 @@
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
confirmClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['open', 'close', 'event']);
|
||||
|
||||
function close() {
|
||||
if (!props.confirmClose || confirm('You have unchanged changes, are you sure you want to close the dialog?')) { // eslint-disable-line no-restricted-globals, no-alert
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => emit('open'));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dialog-body {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
domains: {
|
||||
type: Array,
|
||||
default: () => ['scenes', 'actors', 'movies'],
|
||||
},
|
||||
domain: {
|
||||
type: String,
|
||||
default: 'scenes',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="domains">
|
||||
<Link
|
||||
@@ -32,27 +53,6 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
stash: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
domains: {
|
||||
type: Array,
|
||||
default: () => ['scenes', 'actors', 'movies'],
|
||||
},
|
||||
domain: {
|
||||
type: String,
|
||||
default: 'scenes',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.domains {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import ActorSearch from '#/components/actors/search.vue';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['actors']);
|
||||
|
||||
const newActors = ref([]);
|
||||
|
||||
function addActor(actor) {
|
||||
newActors.value = newActors.value.concat(actor);
|
||||
|
||||
emit('actors', props.edits.actors.concat(actor.id));
|
||||
}
|
||||
|
||||
watch(() => props.scene, () => {
|
||||
newActors.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
class="actors nolist"
|
||||
@@ -40,43 +79,6 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import ActorSearch from '#/components/actors/search.vue';
|
||||
|
||||
const newActors = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['actors']);
|
||||
|
||||
function addActor(actor) {
|
||||
newActors.value = newActors.value.concat(actor);
|
||||
|
||||
emit('actors', props.edits.actors.concat(actor.id));
|
||||
}
|
||||
|
||||
watch(() => props.scene, () => { newActors.value = []; });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actors {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['augmentation']);
|
||||
const augmentation = reactive(props.edits.augmentation);
|
||||
|
||||
watch(augmentation, () => emit('augmentation', augmentation));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="figure augmentation"
|
||||
@@ -208,23 +228,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['augmentation']);
|
||||
const augmentation = reactive(props.edits.augmentation);
|
||||
|
||||
watch(augmentation, () => emit('augmentation', augmentation));
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
defineProps({
|
||||
avatar: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="avatar noshrink">
|
||||
<img
|
||||
@@ -19,10 +31,16 @@
|
||||
>{{ avatar.sharpness.toFixed(2) }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
:title="`Added ${format(avatar.createdAt, 'yyyy-MM-dd')}, may not reflect photo age`"
|
||||
class="avatar-date"
|
||||
>{{ format(avatar.createdAt, '\'\'yy-MM') }}</span>
|
||||
|
||||
<a
|
||||
:href="getPath(avatar)"
|
||||
target="_blank"
|
||||
class="avatar-zoom"
|
||||
@click.stop
|
||||
>
|
||||
<Icon
|
||||
icon="search"
|
||||
@@ -31,17 +49,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
defineProps({
|
||||
avatar: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
@@ -89,7 +96,8 @@ defineProps({
|
||||
}
|
||||
|
||||
.avatar-meta,
|
||||
.avatar-credit {
|
||||
.avatar-credit,
|
||||
.avatar-date {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
box-sizing: border-box;
|
||||
@@ -112,4 +120,10 @@ defineProps({
|
||||
bottom: .75rem;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.avatar-date {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,104 +1,3 @@
|
||||
<template>
|
||||
<div class="figure">
|
||||
<div class="value-section">
|
||||
<span class="value-label">Units</span>
|
||||
|
||||
<select
|
||||
:value="units"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
@change="emit('units', $event.target.value)"
|
||||
>
|
||||
<option value="us">USA</option>
|
||||
<option value="uk">UK</option>
|
||||
<option value="eu">Europe/Asia</option>
|
||||
<option value="jp">Japan</option>
|
||||
<option value="au">Australia</option>
|
||||
<option value="it">Italy</option>
|
||||
<option value="fr">France</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<span class="figure-bust">
|
||||
<div class="value-section">
|
||||
<span class="value-label">Bust</span>
|
||||
|
||||
<select
|
||||
v-model="figure.bust"
|
||||
class="select input"
|
||||
placeholder="Bust"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
<option
|
||||
:key="`figure-bust-unknown`"
|
||||
:value="null"
|
||||
/>
|
||||
|
||||
<option
|
||||
v-for="bust in bustSizes[units]"
|
||||
:key="`figure-bust-${bust}`"
|
||||
:value="Array.isArray(bust) ? bust[0] : bust"
|
||||
>{{ Array.isArray(bust) ? bust.join('/') : bust }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Cup</span>
|
||||
|
||||
<select
|
||||
v-model="figure.cup"
|
||||
class="select input"
|
||||
placeholder="Cup"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
<option
|
||||
:key="`figure-cup-unknown`"
|
||||
:value="null"
|
||||
/>
|
||||
|
||||
<option
|
||||
v-for="cup in cupSizes[units]"
|
||||
:key="`figure-cup-${cup}`"
|
||||
:value="Array.isArray(cup) ? cup[0] : cup"
|
||||
>{{ Array.isArray(cup) ? cup.join('/') : cup }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Waist</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
v-model="figure.waist"
|
||||
type="number"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
|
||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||
<template v-else> cm</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Hip</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
v-model="figure.hip"
|
||||
type="number"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
|
||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||
<template v-else> cm</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
@@ -145,3 +44,104 @@ const bustSizes = {
|
||||
bustSizes.uk = bustSizes.us;
|
||||
bustSizes.jp = bustSizes.eu;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="figure">
|
||||
<div class="value-section">
|
||||
<span class="value-label">Units</span>
|
||||
|
||||
<select
|
||||
:value="units"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
@change="emit('units', $event.target.value)"
|
||||
>
|
||||
<option value="us">USA</option>
|
||||
<option value="uk">UK</option>
|
||||
<option value="eu">Europe/Asia</option>
|
||||
<option value="jp">Japan</option>
|
||||
<option value="au">Australia</option>
|
||||
<option value="it">Italy</option>
|
||||
<option value="fr">France</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<span class="figure-bust">
|
||||
<div class="value-section">
|
||||
<span class="value-label">Bust</span>
|
||||
|
||||
<select
|
||||
v-model="figure.bust"
|
||||
class="select input"
|
||||
placeholder="Bust"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
<option
|
||||
key="figure-bust-unknown"
|
||||
:value="null"
|
||||
/>
|
||||
|
||||
<option
|
||||
v-for="bust in bustSizes[units]"
|
||||
:key="`figure-bust-${bust}`"
|
||||
:value="Array.isArray(bust) ? bust[0] : bust"
|
||||
>{{ Array.isArray(bust) ? bust.join('/') : bust }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Cup</span>
|
||||
|
||||
<select
|
||||
v-model="figure.cup"
|
||||
class="select input"
|
||||
placeholder="Cup"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
<option
|
||||
key="figure-cup-unknown"
|
||||
:value="null"
|
||||
/>
|
||||
|
||||
<option
|
||||
v-for="cup in cupSizes[units]"
|
||||
:key="`figure-cup-${cup}`"
|
||||
:value="Array.isArray(cup) ? cup[0] : cup"
|
||||
>{{ Array.isArray(cup) ? cup.join('/') : cup }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Waist</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
v-model="figure.waist"
|
||||
type="number"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
|
||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||
<template v-else> cm</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="value-section">
|
||||
<span class="value-label">Hip</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
v-model="figure.hip"
|
||||
type="number"
|
||||
class="input"
|
||||
:disabled="!editing.has('figure')"
|
||||
>
|
||||
|
||||
<template v-if="['us', 'uk'].includes(units)"> inch</template>
|
||||
<template v-else> cm</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,46 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import MovieSearch from '#/components/movies/search.vue';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['movies']);
|
||||
|
||||
const newMovies = ref([]);
|
||||
|
||||
function addMovie(movie) {
|
||||
newMovies.value = newMovies.value.concat(movie);
|
||||
|
||||
console.log(movie);
|
||||
|
||||
emit('movies', props.edits.movies.concat(movie.id));
|
||||
|
||||
console.log(props.edits);
|
||||
}
|
||||
|
||||
watch(() => props.scene, () => {
|
||||
newMovies.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
class="movies nolist"
|
||||
@@ -40,47 +83,6 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import MovieSearch from '#/components/movies/search.vue';
|
||||
|
||||
const newMovies = ref([]);
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
scene: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['movies']);
|
||||
|
||||
function addMovie(movie) {
|
||||
newMovies.value = newMovies.value.concat(movie);
|
||||
|
||||
console.log(movie);
|
||||
|
||||
emit('movies', props.edits.movies.concat(movie.id));
|
||||
|
||||
console.log(props.edits);
|
||||
}
|
||||
|
||||
watch(() => props.scene, () => { newMovies.value = []; });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.movies {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['penis', 'units']);
|
||||
const penis = reactive(props.edits.penis);
|
||||
|
||||
watch(penis, () => emit('penis', penis));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="figure penis"
|
||||
@@ -83,27 +107,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['penis', 'units']);
|
||||
const penis = reactive(props.edits.penis);
|
||||
|
||||
watch(penis, () => emit('penis', penis));
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
<script setup>
|
||||
import { inject, reactive, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['place']);
|
||||
const pageContext = inject('pageContext');
|
||||
const countries = pageContext.pageProps.countries;
|
||||
|
||||
const place = reactive(props.edits[props.item.key]);
|
||||
|
||||
watch(place, () => emit('place', place));
|
||||
|
||||
const topCountries = [
|
||||
'AU',
|
||||
'BR',
|
||||
'CZ',
|
||||
'DE',
|
||||
'JP',
|
||||
'RU',
|
||||
'GB',
|
||||
'US',
|
||||
];
|
||||
|
||||
const sortedCountries = countries.toSorted((countryA, countryB) => topCountries.indexOf(countryB.alpha2) - topCountries.indexOf(countryA.alpha2));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="place"
|
||||
@@ -35,43 +75,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch, inject } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const countries = pageContext.pageProps.countries;
|
||||
|
||||
const emit = defineEmits(['place']);
|
||||
const place = reactive(props.edits[props.item.key]);
|
||||
|
||||
watch(place, () => emit('place', place));
|
||||
|
||||
const topCountries = [
|
||||
'AU',
|
||||
'BR',
|
||||
'CZ',
|
||||
'DE',
|
||||
'JP',
|
||||
'RU',
|
||||
'GB',
|
||||
'US',
|
||||
];
|
||||
|
||||
const sortedCountries = countries.toSorted((countryA, countryB) => topCountries.indexOf(countryB.alpha2) - topCountries.indexOf(countryA.alpha2));
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,42 @@
|
||||
<script setup>
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
import { inject } from 'vue';
|
||||
|
||||
defineProps({
|
||||
rev: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
socials: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const pageContext = inject('pageContext');
|
||||
const { env } = pageContext;
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
function getUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (env.socials.urls[social.platform]) {
|
||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="socials nolist">
|
||||
<li
|
||||
@@ -38,46 +77,6 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { env } = pageContext;
|
||||
|
||||
defineProps({
|
||||
rev: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
socials: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
function getUrl(social) {
|
||||
if (social.url) {
|
||||
return social.url;
|
||||
}
|
||||
|
||||
if (env.socials.urls[social.platform]) {
|
||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.socials {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,210 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import Avatar from '#/components/edit/avatar.vue';
|
||||
import Socials from '#/components/edit/revision-socials.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
import { formatDuration } from '#/utils/format.js';
|
||||
|
||||
defineProps({
|
||||
context: {
|
||||
type: String,
|
||||
default: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const revisions = ref(pageContext.pageProps.revisions);
|
||||
const domain = pageContext.routeParams.domain;
|
||||
|
||||
const actors = ref(pageContext.pageProps.actors);
|
||||
const tags = ref(pageContext.pageProps.tags);
|
||||
const movies = ref(pageContext.pageProps.movies);
|
||||
const avatars = ref(pageContext.pageProps.avatars);
|
||||
|
||||
const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor])));
|
||||
const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag])));
|
||||
const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie])));
|
||||
const avatarsById = computed(() => Object.fromEntries(avatars.value.map((avatar) => [avatar.id, avatar])));
|
||||
|
||||
const feedbacks = ref({});
|
||||
const showReviewed = ref(false);
|
||||
const reviewedRevisions = ref(new Set());
|
||||
const expanded = ref(new Set());
|
||||
|
||||
const mappedKeys = {
|
||||
actors: actorsById,
|
||||
// tags: tagsById,
|
||||
movies: moviesById,
|
||||
};
|
||||
|
||||
const dateKeys = [
|
||||
'date',
|
||||
'dateOfBirth',
|
||||
'dateOfDeath',
|
||||
'productionDate',
|
||||
'createdAt',
|
||||
];
|
||||
|
||||
const curatedKeys = {
|
||||
duration: (duration) => formatDuration(duration),
|
||||
};
|
||||
|
||||
const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
const curatedBase = Object.fromEntries(Object.entries(revision.base).map(([key, value]) => {
|
||||
if (Array.isArray(value) && mappedKeys[key]) {
|
||||
return [key, value.map((itemId) => ({
|
||||
id: itemId,
|
||||
name: mappedKeys[key].value[itemId]?.name || mappedKeys[key].value[itemId]?.title,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItemId) => deltaItemId === itemId)),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (key === 'tags') {
|
||||
return [key, value.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.actorId
|
||||
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||
: tagsById.value[tag.id]?.name,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaTag) => deltaTag.id === tag.id && (!Object.hasOwn(tag, 'actorId') || deltaTag.actorId === tag.actorId))),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return [key, value.map((item) => ({
|
||||
...item,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItem) => deltaItem.url === item.url || `${deltaItem.platform}:${deltaItem.handle}` === `${item.platform}:${item.handle}`)),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (dateKeys.includes(key) && value) {
|
||||
return [key, new Date(value)];
|
||||
}
|
||||
|
||||
if (curatedKeys[key]) {
|
||||
return [key, curatedKeys[key](value)];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}));
|
||||
|
||||
const curatedDeltas = revision.deltas.map((delta) => {
|
||||
if (Array.isArray(delta.value) && mappedKeys[delta.key]) {
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((itemId) => ({
|
||||
id: itemId,
|
||||
name: mappedKeys[delta.key].value[itemId]?.name || mappedKeys[delta.key].value[itemId]?.title,
|
||||
modified: !revision.base[delta.key].includes(itemId),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'tags') {
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.actorId
|
||||
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
|
||||
: tagsById.value[tag.id]?.name,
|
||||
modified: !revision.base[delta.key].some((baseTag) => baseTag.id === tag.id && (!Object.hasOwn(baseTag, 'actorId') || baseTag.actorId === tag.actorId)),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((social) => ({
|
||||
...social,
|
||||
modified: !revision.base[delta.key].some((baseItem) => baseItem.url === social.url || `${baseItem.platform}:${baseItem.handle}` === `${social.platform}:${social.handle}`),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'mergeTarget' || delta.key === 'mergeSource') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return {
|
||||
...delta,
|
||||
value: `#${delta.actorId} ${delta.actorName} (${delta.profileIds.length} profiles${delta.profileIds.length > 0 ? `: ${delta.profileIds.join()}` : ''})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (dateKeys.includes(delta.key) && delta.value) {
|
||||
return {
|
||||
...delta,
|
||||
value: new Date(delta.value),
|
||||
};
|
||||
}
|
||||
|
||||
if (curatedKeys[delta.key]) {
|
||||
return {
|
||||
...delta,
|
||||
value: curatedKeys[delta.key](delta.value),
|
||||
};
|
||||
}
|
||||
|
||||
return delta;
|
||||
});
|
||||
|
||||
return {
|
||||
...revision,
|
||||
base: curatedBase,
|
||||
deltas: curatedDeltas,
|
||||
};
|
||||
}));
|
||||
|
||||
async function reloadRevisions() {
|
||||
const updatedRevisions = await get(`/revisions/${domain}`, {
|
||||
isFinalized: showReviewed.value ? undefined : false,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
actors.value = updatedRevisions.actors;
|
||||
tags.value = updatedRevisions.tags;
|
||||
movies.value = updatedRevisions.movies;
|
||||
avatars.value = updatedRevisions.avatars;
|
||||
revisions.value = updatedRevisions.revisions;
|
||||
}
|
||||
|
||||
async function reviewRevision(revision, isApproved) {
|
||||
reviewedRevisions.value.add(revision.id);
|
||||
|
||||
try {
|
||||
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||
isApproved,
|
||||
feedback: feedbacks.value[revision.id],
|
||||
}, {
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||
revisionId: revision.id,
|
||||
});
|
||||
|
||||
revisions.value = revisions.value.map((rev) => (rev.id === updatedRevision.revision.id ? updatedRevision.revision : rev));
|
||||
}
|
||||
catch (error) {
|
||||
reviewedRevisions.value.delete(revision.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function banEditor(revision) {
|
||||
await post('/bans', {
|
||||
userId: revision.user.id,
|
||||
banIp: true,
|
||||
});
|
||||
|
||||
await reviewRevision(revision, false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<div
|
||||
@@ -21,7 +228,7 @@
|
||||
class="rev"
|
||||
:class="{
|
||||
reviewed: reviewedRevisions.has(rev.id),
|
||||
expanded: context === 'admin' || expanded.has(rev.id)
|
||||
expanded: context === 'admin' || expanded.has(rev.id),
|
||||
}"
|
||||
>
|
||||
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
||||
@@ -65,14 +272,14 @@
|
||||
|
||||
<template v-else-if="context === 'admin'">
|
||||
<Icon
|
||||
v-tooltip="`Ban user from submitting revisions`"
|
||||
v-tooltip="{ content: 'Ban user from submitting revisions', ariaId: `rev-ban-${rev.id}` }"
|
||||
icon="user-block"
|
||||
class="review-reject review-ban"
|
||||
@click="banEditor(rev)"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="`Reject revision`"
|
||||
v-tooltip="{ content: 'Reject revision', ariaId: `rev-reject-${rev.id}` }"
|
||||
icon="blocked"
|
||||
class="review-reject"
|
||||
@click="reviewRevision(rev, false)"
|
||||
@@ -85,7 +292,7 @@
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-tooltip="`Approve and apply revision`"
|
||||
v-tooltip="{ content: 'Approve and apply revision', ariaId: `rev-approve-${rev.id}` }"
|
||||
icon="checkmark"
|
||||
class="review-approve"
|
||||
@click="reviewRevision(rev, true)"
|
||||
@@ -209,7 +416,7 @@
|
||||
|
||||
<a
|
||||
v-if="rev.user && context !== 'user'"
|
||||
v-tooltip="rev.user.username"
|
||||
v-tooltip="{ content: rev.user.username, ariaId: `rev-user-${rev.id}` }"
|
||||
:href="`/user/${rev.user.username}`"
|
||||
target="_blank"
|
||||
class="rev-avatar nolink ellipsis"
|
||||
@@ -227,179 +434,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import Avatar from '#/components/edit/avatar.vue';
|
||||
import Socials from '#/components/edit/revision-socials.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
import { formatDuration } from '#/utils/format.js';
|
||||
|
||||
defineProps({
|
||||
context: {
|
||||
type: String,
|
||||
default: 'admin',
|
||||
},
|
||||
});
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const revisions = ref(pageContext.pageProps.revisions);
|
||||
const domain = pageContext.routeParams.domain;
|
||||
|
||||
const actors = ref(pageContext.pageProps.actors);
|
||||
const tags = ref(pageContext.pageProps.tags);
|
||||
const movies = ref(pageContext.pageProps.movies);
|
||||
const avatars = ref(pageContext.pageProps.avatars);
|
||||
|
||||
const actorsById = computed(() => Object.fromEntries(actors.value.map((actor) => [actor.id, actor])));
|
||||
const tagsById = computed(() => Object.fromEntries(tags.value.map((tag) => [tag.id, tag])));
|
||||
const moviesById = computed(() => Object.fromEntries(movies.value.map((movie) => [movie.id, movie])));
|
||||
const avatarsById = computed(() => Object.fromEntries(avatars.value.map((avatar) => [avatar.id, avatar])));
|
||||
|
||||
const feedbacks = ref({});
|
||||
const showReviewed = ref(false);
|
||||
const reviewedRevisions = ref(new Set());
|
||||
const expanded = ref(new Set());
|
||||
|
||||
const mappedKeys = {
|
||||
actors: actorsById,
|
||||
tags: tagsById,
|
||||
movies: moviesById,
|
||||
};
|
||||
|
||||
const dateKeys = [
|
||||
'date',
|
||||
'dateOfBirth',
|
||||
'dateOfDeath',
|
||||
'productionDate',
|
||||
'createdAt',
|
||||
];
|
||||
|
||||
const curatedKeys = {
|
||||
duration: (duration) => formatDuration(duration),
|
||||
};
|
||||
|
||||
const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||
const curatedBase = Object.fromEntries(Object.entries(revision.base).map(([key, value]) => {
|
||||
if (Array.isArray(value) && mappedKeys[key]) {
|
||||
return [key, value.map((itemId) => ({
|
||||
id: itemId,
|
||||
name: mappedKeys[key].value[itemId]?.name || mappedKeys[key].value[itemId]?.title,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItemId) => deltaItemId === itemId)),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return [key, value.map((item) => ({
|
||||
...item,
|
||||
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaItem) => deltaItem.url === item.url || `${deltaItem.platform}:${deltaItem.handle}` === `${item.platform}:${item.handle}`)),
|
||||
}))];
|
||||
}
|
||||
|
||||
if (dateKeys.includes(key) && value) {
|
||||
return [key, new Date(value)];
|
||||
}
|
||||
|
||||
if (curatedKeys[key]) {
|
||||
return [key, curatedKeys[key](value)];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
}));
|
||||
|
||||
const curatedDeltas = revision.deltas.map((delta) => {
|
||||
if (Array.isArray(delta.value) && mappedKeys[delta.key]) {
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((itemId) => ({
|
||||
id: itemId,
|
||||
name: mappedKeys[delta.key].value[itemId]?.name || mappedKeys[delta.key].value[itemId]?.title,
|
||||
modified: !revision.base[delta.key].includes(itemId),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (delta.key === 'socials') {
|
||||
// new socials don't have IDs yet, so we need to compare the values
|
||||
return {
|
||||
...delta,
|
||||
value: delta.value.map((social) => ({
|
||||
...social,
|
||||
modified: !revision.base[delta.key].some((baseItem) => baseItem.url === social.url || `${baseItem.platform}:${baseItem.handle}` === `${social.platform}:${social.handle}`),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (dateKeys.includes(delta.key) && delta.value) {
|
||||
return {
|
||||
...delta,
|
||||
value: new Date(delta.value),
|
||||
};
|
||||
}
|
||||
|
||||
if (curatedKeys[delta.key]) {
|
||||
return {
|
||||
...delta,
|
||||
value: curatedKeys[delta.key](delta.value),
|
||||
};
|
||||
}
|
||||
|
||||
return delta;
|
||||
});
|
||||
|
||||
return {
|
||||
...revision,
|
||||
base: curatedBase,
|
||||
deltas: curatedDeltas,
|
||||
};
|
||||
}));
|
||||
|
||||
async function reloadRevisions() {
|
||||
const updatedRevisions = await get(`/revisions/${domain}`, {
|
||||
isFinalized: showReviewed.value ? undefined : false,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
actors.value = updatedRevisions.actors;
|
||||
tags.value = updatedRevisions.tags;
|
||||
movies.value = updatedRevisions.movies;
|
||||
avatars.value = updatedRevisions.avatars;
|
||||
revisions.value = updatedRevisions.revisions;
|
||||
}
|
||||
|
||||
async function reviewRevision(revision, isApproved) {
|
||||
reviewedRevisions.value.add(revision.id);
|
||||
|
||||
try {
|
||||
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||
isApproved,
|
||||
feedback: feedbacks.value[revision.id],
|
||||
});
|
||||
|
||||
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||
revisionId: revision.id,
|
||||
});
|
||||
|
||||
revisions.value = revisions.value.map((rev) => (rev.id === updatedRevision.revision.id ? updatedRevision.revision : rev));
|
||||
} catch (error) {
|
||||
reviewedRevisions.value.delete(revision.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function banEditor(revision) {
|
||||
await post('/bans', {
|
||||
userId: revision.user.id,
|
||||
banIp: true,
|
||||
});
|
||||
|
||||
await reviewRevision(revision, false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,75 @@
|
||||
<script setup>
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
import {
|
||||
inject,
|
||||
ref,
|
||||
useId,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['socials']);
|
||||
const pageContext = inject('pageContext');
|
||||
const { env } = pageContext;
|
||||
|
||||
const socials = ref(props.edits.socials);
|
||||
|
||||
const platform = ref('');
|
||||
const handle = ref('');
|
||||
const url = ref('');
|
||||
const addSocialDropdownId = useId();
|
||||
|
||||
watch(socials, () => emit('socials', socials.value));
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
function addSocial() {
|
||||
if (!handle.value && !url.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handle.value && !platform.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
socials.value = socials.value.concat({
|
||||
platform: platform.value || null,
|
||||
handle: handle.value || null,
|
||||
url: url.value || null,
|
||||
});
|
||||
|
||||
emit('socials', socials.value);
|
||||
|
||||
platform.value = '';
|
||||
handle.value = '';
|
||||
url.value = '';
|
||||
}
|
||||
|
||||
function getUrl(social) {
|
||||
if (social.url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (env.socials.urls[social.platform]) {
|
||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
class="list nolist"
|
||||
@@ -54,7 +126,7 @@
|
||||
</li>
|
||||
|
||||
<li class="list-new">
|
||||
<VDropdown>
|
||||
<VDropdown :aria-id="addSocialDropdownId">
|
||||
<Icon
|
||||
icon="plus2"
|
||||
class="add noselect"
|
||||
@@ -110,77 +182,6 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
inject,
|
||||
} from 'vue';
|
||||
|
||||
import formatTemplate from 'template-format';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { env } = pageContext;
|
||||
|
||||
const props = defineProps({
|
||||
edits: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
editing: {
|
||||
type: Set,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['socials']);
|
||||
const socials = ref(props.edits.socials);
|
||||
|
||||
const platform = ref('');
|
||||
const handle = ref('');
|
||||
const url = ref('');
|
||||
|
||||
watch(socials, () => emit('socials', socials));
|
||||
|
||||
const iconMap = {
|
||||
twitter: 'twitter-x',
|
||||
};
|
||||
|
||||
function addSocial() {
|
||||
if (!handle.value && !url.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (handle.value && !platform.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
socials.value = socials.value.concat({
|
||||
platform: platform.value || null,
|
||||
handle: handle.value || null,
|
||||
url: url.value || null,
|
||||
});
|
||||
|
||||
emit('socials', socials.value);
|
||||
|
||||
platform.value = '';
|
||||
handle.value = '';
|
||||
url.value = '';
|
||||
}
|
||||
|
||||
function getUrl(social) {
|
||||
if (social.url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (env.socials.urls[social.platform]) {
|
||||
return formatTemplate(env.socials.urls[social.platform], { handle: social.handle });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list {
|
||||
display: flex;
|
||||
|
||||
@@ -1,53 +1,9 @@
|
||||
<template>
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:class="{ disabled: !editing.has(item.key) }"
|
||||
>
|
||||
<li
|
||||
v-for="tag in [...item.value, ...newTags]"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="tag"
|
||||
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
|
||||
>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
|
||||
<Icon
|
||||
v-if="edits.tags && !edits.tags.some((tagId) => tagId === tag.id)"
|
||||
icon="checkmark"
|
||||
class="add"
|
||||
@click="emit('tags', edits.tags.concat(tag.id))"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
icon="cross2"
|
||||
class="remove"
|
||||
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="new">
|
||||
<TagSearch
|
||||
:disabled="!editing.has(item.key)"
|
||||
@tag="addTag"
|
||||
>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</TagSearch>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import events from '#/src/events.js';
|
||||
|
||||
import TagSearch from '#/components/tags/search.vue';
|
||||
|
||||
const newTags = ref([]);
|
||||
import events from '#/src/events.js';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -70,8 +26,25 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['tags']);
|
||||
|
||||
function addTag(tag) {
|
||||
if (props.edits.tags.some((tagId) => tagId === tag.id)) {
|
||||
const newTags = ref([]);
|
||||
|
||||
const tags = [
|
||||
{
|
||||
tags: props.item.value.filter((tag) => tag.actorId === null),
|
||||
actor: null,
|
||||
actorId: null,
|
||||
},
|
||||
...props.scene.actors.map((actor) => ({
|
||||
tags: props.item.value.filter((tag) => tag.actorId === actor.id),
|
||||
actor,
|
||||
actorId: actor?.id || null,
|
||||
})),
|
||||
];
|
||||
|
||||
function addTag(newTag, actor) {
|
||||
const actorId = actor?.id || null;
|
||||
|
||||
if (props.edits.tags.some((sceneTag) => sceneTag.id === newTag.id && sceneTag.actorId === actorId)) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: 'Tag already added',
|
||||
@@ -80,14 +53,75 @@ function addTag(tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
newTags.value = newTags.value.concat(tag);
|
||||
const newTagWithActorId = {
|
||||
...newTag,
|
||||
actorId,
|
||||
};
|
||||
|
||||
emit('tags', props.edits.tags.concat(tag.id));
|
||||
newTags.value = newTags.value.concat(newTagWithActorId);
|
||||
emit('tags', props.edits.tags.concat(newTagWithActorId));
|
||||
}
|
||||
|
||||
watch(() => props.scene, () => { newTags.value = []; });
|
||||
watch(() => props.scene, () => {
|
||||
newTags.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-sections">
|
||||
<div
|
||||
v-for="actorTags in tags"
|
||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
||||
class="tags-section"
|
||||
>
|
||||
<ul
|
||||
class="tags nolist"
|
||||
:class="{ disabled: !editing.has(item.key) }"
|
||||
>
|
||||
<li
|
||||
v-if="actorTags.actor"
|
||||
class="tags-actor"
|
||||
>{{ actorTags.actor.name }}:</li>
|
||||
|
||||
<li
|
||||
v-for="tag in [...actorTags.tags, ...newTags.filter((newTag) => newTag.actorId === actorTags.actorId)]"
|
||||
:key="`tag-${tag.id}`"
|
||||
class="tag"
|
||||
:class="{ deleted: edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId) }"
|
||||
>
|
||||
<span class="tag-name">{{ tag.name }}</span>
|
||||
|
||||
<Icon
|
||||
v-if="edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)"
|
||||
icon="checkmark"
|
||||
class="add"
|
||||
@click="emit('tags', edits.tags.concat(tag))"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
icon="cross2"
|
||||
class="remove"
|
||||
@click="emit('tags', edits.tags.filter((sceneTag) => !(sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)))"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li class="new">
|
||||
<TagSearch
|
||||
:disabled="!editing.has(item.key)"
|
||||
@tag="(tag) => addTag(tag, actorTags.actor)"
|
||||
>
|
||||
<Icon
|
||||
icon="plus3"
|
||||
class="add"
|
||||
/>
|
||||
</TagSearch>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tags {
|
||||
display: flex;
|
||||
@@ -116,7 +150,7 @@ watch(() => props.scene, () => { newTags.value = []; });
|
||||
align-items: center;
|
||||
margin-left: .25rem;
|
||||
|
||||
&:hover {
|
||||
&:hover .icon {
|
||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||
}
|
||||
|
||||
@@ -129,6 +163,18 @@ watch(() => props.scene, () => { newTags.value = []; });
|
||||
}
|
||||
}
|
||||
|
||||
.tags-sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .75rem;
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
.tags-actor {
|
||||
margin-right: .5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -145,9 +191,11 @@ watch(() => props.scene, () => { newTags.value = []; });
|
||||
.tag,
|
||||
.new {
|
||||
.remove,
|
||||
.add {
|
||||
.add,
|
||||
.actor {
|
||||
height: auto;
|
||||
padding: .25rem .3rem;
|
||||
margin-left: .25rem;
|
||||
border-radius: .25rem;
|
||||
fill: var(--highlight-strong-10);
|
||||
|
||||
|
||||
286
components/emails/alert.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Column,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Link,
|
||||
Preview,
|
||||
Row,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
defineProps({
|
||||
triggers: {
|
||||
type: Array,
|
||||
default: () => mockTriggers,
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
|
||||
const mockTriggers = [
|
||||
{
|
||||
scene: {
|
||||
id: 684061,
|
||||
title: 'Thick & Bootylicious: Vanessa Blake Takes A BBC Up Her Ass',
|
||||
slug: 'thick-bootylicious-vanessa-blake-takes-a-bbc-up',
|
||||
date: '2026-02-17T00:00:00.000Z',
|
||||
description: 'Vanessa Blake’s curves are impossible to ignore—especially her thick, round, bootylicious ass. The scene opens with the camera following Vanessa through the house as she struts confidently in a slow, teasing booty walk, giving viewers a full view of her luscious curves. She pauses at the stairs, turning back to flaunt her massive ass before letting her hands wander over her body. Feeling the heat build, Vanessa begins to pleasure herself, showcasing her insatiable appetite for big black cock. Moving to the couch, she grabs a dildo and slowly works it into her ass, preparing herself for what’s coming next. Sitting back and watching the show is Lexington Steele, enjoying every second as she stretches and teases in anticipation. Lexington finally steps in, and Vanessa eagerly drops to her knees, wrapping her lips around his massive BBC and giving him a deep, hungry blowjob. The action quickly escalates to the couch, where they fuck in multiple intense positions, Vanessa’s thick body bouncing with every thrust. When she’s fully ready, Vanessa has Lex slide his BBC deep into her ass, and the two go at it hard in a variety of explosive anal positions. The scene builds to a powerful climax as Lexington pulls out and finishes with a messy facial, leaving Vanessa satisfied and dripping after an unforgettable session.',
|
||||
actors: [
|
||||
{
|
||||
id: 919,
|
||||
name: 'Vanessa Blake',
|
||||
},
|
||||
{
|
||||
id: 1374,
|
||||
name: 'Lexington Steele',
|
||||
},
|
||||
],
|
||||
actorIds: [
|
||||
919,
|
||||
1374,
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: 'MILF',
|
||||
slug: 'milf',
|
||||
},
|
||||
{
|
||||
name: 'facial',
|
||||
slug: 'facial',
|
||||
},
|
||||
],
|
||||
tagIds: [
|
||||
4,
|
||||
12,
|
||||
19,
|
||||
30,
|
||||
29,
|
||||
37,
|
||||
41,
|
||||
68,
|
||||
82,
|
||||
100,
|
||||
112,
|
||||
119,
|
||||
null,
|
||||
130,
|
||||
],
|
||||
entityId: 522,
|
||||
parentEntityId: 47,
|
||||
},
|
||||
posterUrl: 'http://localhost:5100/media/posters/lazy/cd/b5/99375c7998a20acffef2217a8a21c3546924312cec65.jpeg',
|
||||
alertId: 123,
|
||||
alertActors: [],
|
||||
alertTags: [],
|
||||
alertEntities: [
|
||||
{
|
||||
id: 522,
|
||||
name: 'Jules Jordan',
|
||||
},
|
||||
],
|
||||
alertMatches: [],
|
||||
alertSceneIds: [],
|
||||
},
|
||||
{
|
||||
scene: {
|
||||
id: 684060,
|
||||
title: 'Elizabeth Skylar’s Big ASSets Drive The Milfomaniac Manuel Ferrara Wild',
|
||||
slug: 'elizabeth-skylar-s-big-assets-drive-the-manuel',
|
||||
date: '2026-02-21T00:00:00.000Z',
|
||||
description: 'Elizabeth Skylar is ready to indulge her cravings, and there’s only one man she wants—the Milfomaniac, Manuel Ferrara. The scene opens with Elizabeth stepping out onto the balcony in sexy lingerie, confidently showing off her busty curves as the camera captures every seductive angle. She teases the viewer with slow, deliberate poses before heading inside to the kitchen, where she perches on a stool with her ass on full display. Manuel joins her, grabbing and admiring her curves, letting the camera in on just how irresistible she is. The action moves to the living room, where Elizabeth stretches out on the couch and begins to pleasure herself, putting on a sultry show for Manuel. Unable to resist, she drops to her knees, her tits spilling free as she takes him deep into her mouth with an eager, hungry intensity. From there, the two tear into each other on the couch, fucking in multiple positions as Elizabeth moans for more. The scene builds to a explosive finish, ending with Manuel delivering a memorable facial that leaves Elizabeth completely satisfied.',
|
||||
actors: [
|
||||
{
|
||||
id: 44001,
|
||||
name: 'Elizabeth Skylar',
|
||||
},
|
||||
{
|
||||
id: 512,
|
||||
name: 'Manuel Ferrara',
|
||||
},
|
||||
],
|
||||
actorIds: [
|
||||
512,
|
||||
44001,
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: 'anal',
|
||||
slug: 'anal',
|
||||
},
|
||||
{
|
||||
name: 'ass to mouth',
|
||||
tag: 'atm',
|
||||
},
|
||||
],
|
||||
tagIds: [
|
||||
4,
|
||||
30,
|
||||
29,
|
||||
31,
|
||||
37,
|
||||
41,
|
||||
68,
|
||||
82,
|
||||
119,
|
||||
125,
|
||||
],
|
||||
entityId: 522,
|
||||
parentEntityId: 47,
|
||||
},
|
||||
posterUrl: 'http://localhost:5100/media/posters/lazy/3c/84/f53e772e4710ca71fb74bdb6896af3d180725d17710b.jpeg',
|
||||
alertId: 123,
|
||||
alertActors: [],
|
||||
alertTags: [],
|
||||
alertEntities: [
|
||||
{
|
||||
id: 522,
|
||||
name: 'Jules Jordan',
|
||||
},
|
||||
],
|
||||
alertMatches: [],
|
||||
alertSceneIds: [],
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>{{ triggers.length }} new scene{{ triggers.length === 1 ? '' : 's' }} matching your alerts on traxxx</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">Scenes for which you set e-mail alerts have been added or released:</Text>
|
||||
|
||||
<div
|
||||
v-for="(trigger, index) in triggers"
|
||||
:key="trigger.scene.id"
|
||||
style="margin-bottom: 15px; font-size: 14px;"
|
||||
:style="{
|
||||
'padding-bottom': index < triggers.length - 1 ? '15px' : 0,
|
||||
'border-bottom': index < triggers.length - 1 ? 'solid 1px #ddd' : 'none',
|
||||
}"
|
||||
>
|
||||
<Row style="margin-bottom: 8px;">
|
||||
<Column valign="top">
|
||||
{{ format(trigger.scene.date, 'yyyy-MM-dd') }} |
|
||||
<span v-if="trigger.alertSceneIds.length > 0">Scene is released</span>
|
||||
|
||||
<span v-else>
|
||||
New
|
||||
|
||||
<template
|
||||
v-if="trigger.alertTags.length > 0"
|
||||
>{{ trigger.alertTags.map((tag) => tag.name).join(', ') }}</template>
|
||||
|
||||
scene
|
||||
|
||||
<template
|
||||
v-if="trigger.alertActors.length > 0"
|
||||
>with {{ trigger.alertActors.map((actor) => actor.name).join(', ') }} </template>
|
||||
|
||||
<template
|
||||
v-if="trigger.alertEntities.length > 0"
|
||||
>for {{ trigger.alertEntities[0].name }} </template>
|
||||
|
||||
<template
|
||||
v-if="trigger.alertMatches.length > 0"
|
||||
>matching {{ trigger.alertMatches.map((match) => match.expression).join(', ') }}</template>
|
||||
</span>
|
||||
</Column>
|
||||
|
||||
<Column
|
||||
width="50"
|
||||
valign="top"
|
||||
style="text-align: right;"
|
||||
>
|
||||
<span style="font-size: 14px; color: #aaa;">#{{ trigger.alertId }}</span>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<div>
|
||||
<Row>
|
||||
<Column
|
||||
width="175"
|
||||
valign="top"
|
||||
>
|
||||
<Link :href="`${address}/scene/${trigger.scene.id}/${trigger.scene.slug}`">
|
||||
<Img
|
||||
v-if="trigger.posterUrl"
|
||||
:src="trigger.posterUrl"
|
||||
width="160"
|
||||
:alt="trigger.scene.title"
|
||||
style="margin-top: 2px; border-radius: 8px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
style="width: 144px; height: 74px; background: #eee; color: #aaa; text-align: center; border-radius: 8px; padding: 8px;"
|
||||
>No thumb</div>
|
||||
</Link>
|
||||
</Column>
|
||||
|
||||
<Column valign="top">
|
||||
<div style="margin-bottom: 5px;">
|
||||
<Link
|
||||
:href="`${address}/scene/${trigger.scene.id}/${trigger.scene.slug}`"
|
||||
style="font-weight: bold; line-height: 1.5; color: inherit;"
|
||||
>{{ trigger.scene.title }}</Link>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span style="font-size: 14px;">{{ trigger.scene.actors.slice(0, 3).map((actor) => actor.name).join(', ') }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span style="font-size: 14px; color: #666;">{{ trigger.scene.tags.slice(0, 5).map((tag) => tag.name).join(', ') }}</span>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="margin-top: 30px;"
|
||||
>Enjoy!</Heading>
|
||||
|
||||
<span style="font-size: 14px; color: #aaa;">Delivered by the Automated Notification and Alert Link</span>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
106
components/emails/password.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
ip: {
|
||||
type: String,
|
||||
default: '127.0.0.1',
|
||||
},
|
||||
country: {
|
||||
type: String,
|
||||
default: 'Unknown',
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: () => new Date(),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Your traxxx password has been changed</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="margin-top: 30px;"
|
||||
>Your traxxx password was changed.</Heading>
|
||||
|
||||
<Text>Device used to change your password:</Text>
|
||||
|
||||
<table
|
||||
align="center"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="font-size: 14px;"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 4px 8px 4px 0;">IP:</td>
|
||||
<td style="padding: 4px 0;">{{ ip }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 8px 4px 0;">Country:</td>
|
||||
<td style="padding: 4px 0;">{{ country }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 8px 4px 0;">Timestamp:</td>
|
||||
<td style="padding: 4px 0;">{{ format(timestamp, 'yyyy-MM-dd hh:mm:ss') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">If this change was made by you, you can ignore this e-mail. If this was not you, please <a
|
||||
href="mailto:account@traxxx.me"
|
||||
style="color: #f65596; text-decoration: none;"
|
||||
>contact us</a> immediately.</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
79
components/emails/recovery.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import config from 'config';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
verifyUrl: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100/recovery?userId=0&token=00000000000000000000000000000000',
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Reset your traxxx password</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Text>
|
||||
<strong>You requested to reset the password for your traxxx account.</strong>
|
||||
<br>
|
||||
Please set a new password by clicking the button below:
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
:href="verifyUrl"
|
||||
target="_blank"
|
||||
style="background: #f65596; color: #fff; font-size: 16px; padding: 1rem 1.5rem; border-radius: .5rem; margin: 1rem 0;"
|
||||
><strong>Reset password</strong></Button>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">
|
||||
The link will expire in {{ config.auth.passwordTokenExpiry }} minutes, or when you request another password reset.
|
||||
If you did not request a password reset, you can safely ignore this e-mail.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
80
components/emails/signup.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import config from 'config';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
verifyUrl: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100/verify?userId=0&token=00000000000000000000000000000000',
|
||||
},
|
||||
trigger: {
|
||||
type: String,
|
||||
default: 'change',
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Sign up for traxxx</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi,</Heading>
|
||||
|
||||
<Text style="font-size: 16px;">
|
||||
<strong>Thank you for signing up for traxxx!</strong>
|
||||
<br>
|
||||
Please complete your registration by clicking the button below:
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
:href="verifyUrl"
|
||||
target="_blank"
|
||||
style="background: #f65596; color: #fff; font-size: 16px; padding: 1rem 1.5rem; border-radius: .5rem; margin: 1rem 0;"
|
||||
><strong>Create my account</strong></Button>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">The link will expire in {{ config.auth.signupTokenExpiry }} minutes. If you did not sign up, you can safely ignore this e-mail.</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
62
components/emails/unavailable.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Someone tried to use this e-mail address for traxxx</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="margin-top: 30px;"
|
||||
>Someone tried to assign this e-mail address to a traxxx account.</Heading>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">If this was you with this account ({{ user.username }}), your e-mail address is already set correctly. If this was you with another account, you will need to use another e-mail address. If this was not you, you can safely ignore this e-mail.</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
81
components/emails/verify.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Text,
|
||||
} from '@vue-email/components';
|
||||
|
||||
import config from 'config';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: () => mockUser,
|
||||
},
|
||||
verifyUrl: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100/verify?userId=0&token=00000000000000000000000000000000',
|
||||
},
|
||||
trigger: {
|
||||
type: String,
|
||||
default: 'change',
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
default: 'http://localhost:5100',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// defineProps' default factory (above, in <script setup>) references this. It must live in a plain
|
||||
// <script> block: Vue's compiler forbids defineProps from referencing variables declared inside
|
||||
// <script setup> itself, since that block gets hoisted out of the setup() function.
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'DebuggingLibrarian',
|
||||
email: 'debug@alerts.traxxx.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Html>
|
||||
<Body style="font-family: sans-serif;">
|
||||
<Preview>Please verify your traxxx e-mail address</Preview>
|
||||
|
||||
<Container>
|
||||
<Img
|
||||
:src="`${address}/img/logo.png`"
|
||||
width="150"
|
||||
alt="traxxx"
|
||||
style="margin: 15px auto 25px auto;"
|
||||
/>
|
||||
|
||||
<Heading
|
||||
as="h3"
|
||||
style="color: #f65596; margin: 0 auto 0 auto;"
|
||||
>Hi, {{ user.username }}</Heading>
|
||||
|
||||
<Text>
|
||||
<strong>You requested to {{ trigger === 'verify' ? 'verify' : 'change' }} the e-mail address for your traxxx account.</strong>
|
||||
<br>
|
||||
Please confirm your e-mail address by clicking the button below:
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
:href="verifyUrl"
|
||||
target="_blank"
|
||||
style="background: #f65596; color: #fff; font-size: 16px; padding: 1rem 1.5rem; border-radius: .5rem; margin: 1rem 0;"
|
||||
><strong>Verify e-mail address</strong></Button>
|
||||
|
||||
<Text style="font-size: 16px; margin: 20px 0;">
|
||||
The link will expire in {{ config.auth.emailTokenExpiry }} minutes, or when you request an(other) e-mail change or verification. If you did not request this {{ trigger === 'verify' ? 'verification' : 'change' }}, you can safely ignore this e-mail.</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
</template>
|
||||
@@ -1,3 +1,16 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
showNetworkSymbol: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
:href="`/${entity.type}/${entity.slug}`"
|
||||
@@ -22,19 +35,6 @@
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
entity: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
showNetworkSymbol: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.entity {
|
||||
width: 15rem;
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
<script setup>
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'available',
|
||||
},
|
||||
toggleActor: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['actor']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
:key="`filter-actor-${actor.id}`"
|
||||
@@ -44,37 +75,6 @@
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'available',
|
||||
},
|
||||
toggleActor: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['actor']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-name {
|
||||
align-items: stretch;
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
<script setup>
|
||||
import { UseVirtualList } from '@vueuse/components';
|
||||
import { computed, inject, ref, useId } from 'vue';
|
||||
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
import Actor from '#/components/filters/actor.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const selectedGender = ref(null);
|
||||
const order = ref('count');
|
||||
const genderTooltipId = useId();
|
||||
const sortAlphaTooltipId = useId();
|
||||
const sortCountTooltipId = useId();
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const pageProps = pageContext.pageProps;
|
||||
const { actor: pageActor } = pageProps;
|
||||
|
||||
const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean));
|
||||
|
||||
const availableActors = computed(() => props.actors
|
||||
.filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id)
|
||||
&& actor.id !== pageActor?.id
|
||||
&& searchRegexp.value.test(actor.name)
|
||||
&& (!selectedGender.value || actor.gender === selectedGender.value))
|
||||
.sort((actorA, actorB) => {
|
||||
if (order.value === 'count') {
|
||||
return actorB.count - actorA.count;
|
||||
}
|
||||
|
||||
return actorA.name.localeCompare(actorB.name);
|
||||
}));
|
||||
|
||||
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
|
||||
const isAggActorsLimited = computed(() => props.actors.length >= pageContext.env.maxAggregateSize);
|
||||
|
||||
function toggleActor(actor, combine) {
|
||||
if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) {
|
||||
emit('update', 'actors', props.filters.actors.filter((filterActor) => filterActor.id !== actor.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
emit('update', 'actors', props.filters.actors.concat(actor));
|
||||
}
|
||||
else {
|
||||
emit('update', 'actors', [actor]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectGender() {
|
||||
const genderIndex = genders.value.indexOf(selectedGender.value);
|
||||
|
||||
if (genderIndex >= genders.value.length - 1) {
|
||||
selectedGender.value = genders.value[0];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedGender.value = genders.value[genderIndex + 1];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter actors-container">
|
||||
<div
|
||||
@@ -18,6 +94,7 @@
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-tooltip="{ content: selectedGender ? `${selectedGender.slice(0, 1).toUpperCase()}${selectedGender.slice(1)} performers` : `All genders`, ariaId: genderTooltipId }"
|
||||
class="filter-sort noselect"
|
||||
@click="selectGender"
|
||||
>
|
||||
@@ -35,6 +112,7 @@
|
||||
|
||||
<div
|
||||
v-show="order === 'name'"
|
||||
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
@@ -45,6 +123,7 @@
|
||||
|
||||
<div
|
||||
v-show="order === 'count'"
|
||||
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'name'"
|
||||
>
|
||||
@@ -96,78 +175,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { UseVirtualList } from '@vueuse/components';
|
||||
|
||||
import Actor from '#/components/filters/actor.vue';
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const selectedGender = ref(null);
|
||||
const order = ref('count');
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const pageProps = pageContext.pageProps;
|
||||
const { actor: pageActor } = pageProps;
|
||||
|
||||
const selectedActors = computed(() => props.filters.actors.map((filterActor) => props.actors.find((actor) => actor.id === filterActor.id)).filter(Boolean));
|
||||
|
||||
const availableActors = computed(() => props.actors
|
||||
.filter((actor) => !props.filters.actors.some((filterActor) => filterActor.id === actor.id)
|
||||
&& actor.id !== pageActor?.id
|
||||
&& searchRegexp.value.test(actor.name)
|
||||
&& (!selectedGender.value || actor.gender === selectedGender.value))
|
||||
.sort((actorA, actorB) => {
|
||||
if (order.value === 'count') {
|
||||
return actorB.count - actorA.count;
|
||||
}
|
||||
|
||||
return actorA.name.localeCompare(actorB.name);
|
||||
}));
|
||||
|
||||
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
|
||||
const isAggActorsLimited = computed(() => props.actors.length >= pageContext.env.maxAggregateSize);
|
||||
|
||||
function toggleActor(actor, combine) {
|
||||
if (props.filters.actors.some((filterActor) => filterActor.id === actor.id)) {
|
||||
emit('update', 'actors', props.filters.actors.filter((filterActor) => filterActor.id !== actor.id));
|
||||
return;
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
emit('update', 'actors', props.filters.actors.concat(actor));
|
||||
} else {
|
||||
emit('update', 'actors', [actor]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectGender() {
|
||||
const genderIndex = genders.value.indexOf(selectedGender.value);
|
||||
|
||||
if (genderIndex >= genders.value.length - 1) {
|
||||
selectedGender.value = genders.value[0];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedGender.value = genders.value[genderIndex + 1];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter {
|
||||
padding: 0;
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
<script setup>
|
||||
import { format, subYears } from 'date-fns';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// <select> doesn't work well with :value for some reason
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const maxDob = format(subYears(new Date(), 18), 'yyyy-MM-dd');
|
||||
|
||||
const dobType = ref(props.filters.dobType);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<div class="filter-section">
|
||||
@@ -53,26 +75,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { format, subYears } from 'date-fns';
|
||||
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
|
||||
const maxDob = format(subYears(new Date(), 18), 'yyyy-MM-dd');
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const dobType = ref(props.filters.dobType); // <select> doesn't work well with :value for some reason
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
// cup range should probably not be used, because it becomes impossible to filter for larger values than the current results
|
||||
cupRange: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
function cupRangeToCups(cupA, cupB) {
|
||||
const smallestCode = cupA.charCodeAt(0);
|
||||
const biggestCode = cupB.charCodeAt(0);
|
||||
|
||||
return Array.from({ length: biggestCode - smallestCode + 1 }, (value, index) => String.fromCharCode(smallestCode + index));
|
||||
}
|
||||
|
||||
const naturalBoobsValues = [true, undefined, false];
|
||||
const cups = computed(() => (props.cupRange ? cupRangeToCups(props.cupRange[0], props.cupRange[1]) : 'ABCDEFGHIJKZ'.split(''))); // add Z to allow for 'bigger than K' without wasting fidelity
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<RangeFilter
|
||||
@@ -42,33 +72,3 @@
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
// cup range should probably not be used, because it becomes impossible to filter for larger values than the current results
|
||||
cupRange: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
function cupRangeToCups(cupA, cupB) {
|
||||
const smallestCode = cupA.charCodeAt(0);
|
||||
const biggestCode = cupB.charCodeAt(0);
|
||||
|
||||
return Array.from({ length: biggestCode - smallestCode + 1 }, (value, index) => String.fromCharCode(smallestCode + index));
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const naturalBoobsValues = [true, undefined, false];
|
||||
const cups = computed(() => (props.cupRange ? cupRangeToCups(props.cupRange[0], props.cupRange[1]) : 'ABCDEFGHIJKZ'.split(''))); // add Z to allow for 'bigger than K' without wasting fidelity
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,80 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref, useId } from 'vue';
|
||||
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
channels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const order = ref('name');
|
||||
const sortAlphaTooltipId = useId();
|
||||
const sortCountTooltipId = useId();
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { channel: pageChannel } = pageProps;
|
||||
|
||||
function sort(channelA, channelB) {
|
||||
if (order.value === 'count') {
|
||||
return channelB.count - channelA.count;
|
||||
}
|
||||
|
||||
return channelA.name.localeCompare(channelB.name);
|
||||
}
|
||||
|
||||
const entities = computed(() => {
|
||||
const filteredChannels = props.channels.filter((channel) => channel.id !== pageChannel?.id
|
||||
&& (searchRegexp.value.test(channel.name)
|
||||
|| searchRegexp.value.test(channel.slug)
|
||||
|| (channel.parent && searchRegexp.value.test(channel.parent.name))
|
||||
|| (channel.parent && searchRegexp.value.test(channel.parent.slug))));
|
||||
|
||||
const networks = Object.values(filteredChannels.reduce((acc, channel) => {
|
||||
if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child
|
||||
acc[channel.id] = {
|
||||
...channel,
|
||||
children: [],
|
||||
};
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[channel.id] && channel.parent && !acc[channel.parent.id] && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||
acc[channel.parent.id] = {
|
||||
...channel.parent,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (!acc[channel.id] && channel.parent && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||
acc[channel.parent.id].children.push(channel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}))
|
||||
.map((network) => ({
|
||||
...network,
|
||||
children: network.children?.sort(sort),
|
||||
count: network.count || network.children?.reduce((acc, channel) => acc + channel.count, 0),
|
||||
}))
|
||||
.sort(sort)
|
||||
.flatMap((network) => [network, ...(network.children || [])]);
|
||||
|
||||
return networks;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter channels-container">
|
||||
<div class="filters-sort">
|
||||
@@ -14,6 +91,7 @@
|
||||
|
||||
<div
|
||||
v-show="order === 'name'"
|
||||
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
@@ -22,6 +100,7 @@
|
||||
|
||||
<div
|
||||
v-show="order === 'count'"
|
||||
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'name'"
|
||||
>
|
||||
@@ -75,7 +154,7 @@
|
||||
v-if="filters.entity?.id === entity.id"
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
@click.native.stop="emit('update', 'entity', null)"
|
||||
@click.stop="emit('update', 'entity', null)"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
@@ -85,81 +164,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
channels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const order = ref('name');
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { channel: pageChannel } = pageProps;
|
||||
|
||||
function sort(channelA, channelB) {
|
||||
if (order.value === 'count') {
|
||||
return channelB.count - channelA.count;
|
||||
}
|
||||
|
||||
return channelA.name.localeCompare(channelB.name);
|
||||
}
|
||||
|
||||
const entities = computed(() => {
|
||||
const filteredChannels = props.channels.filter((channel) => channel.id !== pageChannel?.id
|
||||
&& (searchRegexp.value.test(channel.name)
|
||||
|| searchRegexp.value.test(channel.slug)
|
||||
|| (channel.parent && searchRegexp.value.test(channel.parent.name))
|
||||
|| (channel.parent && searchRegexp.value.test(channel.parent.slug))));
|
||||
|
||||
const networks = Object.values(filteredChannels.reduce((acc, channel) => {
|
||||
if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child
|
||||
acc[channel.id] = {
|
||||
...channel,
|
||||
children: [],
|
||||
};
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[channel.id] && channel.parent && !acc[channel.parent.id] && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||
acc[channel.parent.id] = {
|
||||
...channel.parent,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (!acc[channel.id] && channel.parent && (channel.type === 'channel' || channel.type === 'studio')) {
|
||||
acc[channel.parent.id].children.push(channel);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}))
|
||||
.map((network) => ({
|
||||
...network,
|
||||
children: network.children?.sort(sort),
|
||||
count: network.count || network.children?.reduce((acc, channel) => acc + channel.count, 0),
|
||||
}))
|
||||
.sort(sort)
|
||||
.flatMap((network) => [network, ...(network.children || [])]);
|
||||
|
||||
return networks;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-items {
|
||||
max-height: 15rem;
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
countries: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedCountry: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['country']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="countries nolist">
|
||||
<li
|
||||
@@ -19,27 +34,12 @@
|
||||
v-if="selectedCountry === country.alpha2"
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
@click.native.stop="emit('country', null)"
|
||||
@click.stop="emit('country', null)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
countries: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedCountry: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['country']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.countries:not(:last-child) {
|
||||
border-bottom: solid 1px var(--glass-weak-30);
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import Countries from './countries.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
countries: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps } = pageContext;
|
||||
|
||||
const allCountries = computed(() => props.countries || pageProps.countries);
|
||||
const countryQuery = ref('');
|
||||
|
||||
const topCountryAlpha2s = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
||||
const topCountries = computed(() => topCountryAlpha2s.map((alpha2) => allCountries.value.find((country) => country.alpha2 === alpha2)).filter(Boolean));
|
||||
const filteredCountries = computed(() => allCountries.value.filter((country) => new RegExp(countryQuery.value, 'i').test(country.name)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="countries-container"
|
||||
@@ -43,35 +72,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
|
||||
import Countries from './countries.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
countries: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps } = pageContext;
|
||||
|
||||
const allCountries = computed(() => props.countries || pageProps.countries);
|
||||
const countryQuery = ref('');
|
||||
|
||||
const topCountryAlpha2s = ['AU', 'BR', 'CZ', 'DE', 'JP', 'RU', 'GB', 'US'];
|
||||
const topCountries = computed(() => topCountryAlpha2s.map((alpha2) => allCountries.value.find((country) => country.alpha2 === alpha2)).filter(Boolean));
|
||||
const filteredCountries = computed(() => allCountries.value.filter((country) => new RegExp(countryQuery.value, 'i').test(country.name)));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.countries-container {
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps({
|
||||
results: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// desktop defaults to open, compact defaults to closed
|
||||
// we can't measure viewframe in SSR, so use separate toggles
|
||||
const showFilters = ref(true);
|
||||
const showFiltersCompact = ref(false);
|
||||
|
||||
function toggleFilters(state) {
|
||||
showFilters.value = state ?? !showFilters.value;
|
||||
showFiltersCompact.value = state ?? !showFiltersCompact.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filters-frame">
|
||||
<div
|
||||
@@ -47,31 +72,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps({
|
||||
results: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// desktop defaults to open, compact defaults to closed
|
||||
// we can't measure viewframe in SSR, so use separate toggles
|
||||
const showFilters = ref(true);
|
||||
const showFiltersCompact = ref(false);
|
||||
|
||||
function toggleFilters(state) {
|
||||
showFilters.value = state ?? !showFilters.value;
|
||||
showFiltersCompact.value = state ?? !showFiltersCompact.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter {
|
||||
padding: .5rem;
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<script setup>
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="filter genders nolist">
|
||||
<li>
|
||||
@@ -47,19 +60,6 @@
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.genders {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<script setup>
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
|
||||
defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<RangeFilter
|
||||
@@ -31,16 +44,3 @@
|
||||
</RangeFilter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RangeFilter from '#/components/filters/range.vue';
|
||||
|
||||
defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
<template>
|
||||
<div class="filter-section">
|
||||
<label class="filter-label noselect">
|
||||
<span class="label">
|
||||
<Checkbox
|
||||
:checked="!disabled"
|
||||
class="checkbox"
|
||||
@change="(checked) => $emit('enable', checked)"
|
||||
/>{{ label }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!disabled"
|
||||
class="label-values"
|
||||
>{{ value[0] }} - {{ value[1] }}<template v-if="unit"> {{ unit }}</template></span>
|
||||
</label>
|
||||
|
||||
<span class="filter-split">
|
||||
<Range
|
||||
:min="min"
|
||||
:max="max"
|
||||
:value="value"
|
||||
:values="values"
|
||||
:disabled="disabled"
|
||||
:allow-enable="allowEnable"
|
||||
@enable="emit('enable', true)"
|
||||
@input="(range) => emit('input', range)"
|
||||
@change="(range) => emit('change', range)"
|
||||
>
|
||||
<template #start><slot name="start" /></template>
|
||||
<template #end><slot name="end" /></template>
|
||||
</Range>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Checkbox from '../form/checkbox.vue';
|
||||
import Range from '../form/range.vue';
|
||||
@@ -75,3 +39,39 @@ defineProps({
|
||||
|
||||
const emit = defineEmits(['change', 'input', 'enable']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter-section">
|
||||
<label class="filter-label noselect">
|
||||
<span class="label">
|
||||
<Checkbox
|
||||
:checked="!disabled"
|
||||
class="checkbox"
|
||||
@change="(checked) => $emit('enable', checked)"
|
||||
/>{{ label }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="!disabled"
|
||||
class="label-values"
|
||||
>{{ value[0] }} - {{ value[1] }}<template v-if="unit"> {{ unit }}</template></span>
|
||||
</label>
|
||||
|
||||
<span class="filter-split">
|
||||
<Range
|
||||
:min="min"
|
||||
:max="max"
|
||||
:value="value"
|
||||
:values="values"
|
||||
:disabled="disabled"
|
||||
:allow-enable="allowEnable"
|
||||
@enable="emit('enable', true)"
|
||||
@input="(range) => emit('input', range)"
|
||||
@change="(range) => emit('change', range)"
|
||||
>
|
||||
<template #start><slot name="start" /></template>
|
||||
<template #end><slot name="end" /></template>
|
||||
</Range>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,129 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref, useId } from 'vue';
|
||||
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
actorTags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
|
||||
const {
|
||||
tag: pageTag,
|
||||
actor: pageActor,
|
||||
} = pageProps;
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const order = ref('priority');
|
||||
const showActorTags = ref(!!pageActor);
|
||||
const actorTagsTooltipId = useId();
|
||||
const allTagsTooltipId = useId();
|
||||
const sortPriorityTooltipId = useId();
|
||||
const sortCountTooltipId = useId();
|
||||
const sortAlphaTooltipId = useId();
|
||||
|
||||
const priorityTags = [
|
||||
'anal',
|
||||
'dp',
|
||||
'threesome',
|
||||
'gangbang',
|
||||
'blowbang',
|
||||
'orgy',
|
||||
'airtight',
|
||||
'dap',
|
||||
'dvp',
|
||||
'triple-penetration',
|
||||
'tap',
|
||||
'tvp',
|
||||
'transsexual',
|
||||
'spitroast',
|
||||
'mfm',
|
||||
'fmf',
|
||||
'bdsm',
|
||||
'deepthroat',
|
||||
'blowjob',
|
||||
'lesbian',
|
||||
];
|
||||
|
||||
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor));
|
||||
|
||||
const groupedTags = computed(() => {
|
||||
// can't show actor tags inside stash, because both require a join, and manticore currently only supports one
|
||||
const tags = showActorTags.value && isActorTagsAvailable.value
|
||||
? props.actorTags
|
||||
: props.tags;
|
||||
|
||||
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
||||
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
||||
&& tag.id !== pageTag?.id
|
||||
&& searchRegexp.value.test(tag.name));
|
||||
|
||||
if (order.value === 'count') {
|
||||
const available = filtered.sort((tagA, tagB) => {
|
||||
if (order.value === 'count') {
|
||||
return tagB.count - tagA.count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available,
|
||||
};
|
||||
}
|
||||
|
||||
if (order.value === 'priority') {
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available: [
|
||||
...priorityTags.map((priorityTag) => filtered.find((tag) => tag.slug === priorityTag)).filter(Boolean),
|
||||
...filtered.filter((tag) => !priorityTags.includes(tag.slug)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available: filtered,
|
||||
};
|
||||
});
|
||||
|
||||
function toggleTag(tag, combine) {
|
||||
emit('update', 'onlyActorTags', showActorTags.value, false);
|
||||
|
||||
if (props.filters.tags.includes(tag.slug)) {
|
||||
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
||||
return;
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
emit('update', 'tags', props.filters.tags.concat(tag.slug));
|
||||
}
|
||||
else {
|
||||
emit('update', 'tags', [tag.slug]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter tags-container">
|
||||
<div class="filters-sort">
|
||||
@@ -12,18 +138,38 @@
|
||||
<Icon icon="search" />
|
||||
</label>
|
||||
|
||||
<template v-if="isActorTagsAvailable">
|
||||
<div
|
||||
v-show="showActorTags"
|
||||
v-tooltip="{ content: 'Tags relevant to the selected actors', ariaId: actorTagsTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="showActorTags = false"
|
||||
>
|
||||
<Icon icon="user-tags" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="!showActorTags"
|
||||
v-tooltip="{ content: 'All tags', ariaId: allTagsTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="showActorTags = true"
|
||||
>
|
||||
<Icon icon="price-tags" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-show="order === 'priority'"
|
||||
v-tooltip="{ content: 'Sorted by tag priority', ariaId: sortPriorityTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
<Icon
|
||||
icon="star"
|
||||
/>
|
||||
<Icon icon="star" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="order === 'count'"
|
||||
v-tooltip="{ content: 'Sorted by number of scenes', ariaId: sortCountTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'name'"
|
||||
>
|
||||
@@ -34,6 +180,7 @@
|
||||
|
||||
<div
|
||||
v-show="order === 'name'"
|
||||
v-tooltip="{ content: 'Sorted alphanumerically', ariaId: sortAlphaTooltipId }"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'priority'"
|
||||
>
|
||||
@@ -101,106 +248,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
|
||||
import abbreviateNumber from '#/src/utils/abbreviate-number.js';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const order = ref('priority');
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { tag: pageTag } = pageProps;
|
||||
|
||||
const priorityTags = [
|
||||
'anal',
|
||||
'gangbang',
|
||||
'blowbang',
|
||||
'transsexual',
|
||||
'airtight',
|
||||
'dp',
|
||||
'dap',
|
||||
'dvp',
|
||||
'triple-penetration',
|
||||
'tap',
|
||||
'tvp',
|
||||
'mfm',
|
||||
'fmf',
|
||||
'threesome',
|
||||
'bdsm',
|
||||
'deepthroat',
|
||||
'blowjob',
|
||||
'lesbian',
|
||||
];
|
||||
|
||||
const groupedTags = computed(() => {
|
||||
const selected = props.tags.filter((tag) => props.filters.tags.includes(tag.slug));
|
||||
const filtered = props.tags.filter((tag) => !props.filters.tags.includes(tag.slug)
|
||||
&& tag.id !== pageTag?.id
|
||||
&& searchRegexp.value.test(tag.name));
|
||||
|
||||
if (order.value === 'count') {
|
||||
const available = filtered.sort((tagA, tagB) => {
|
||||
if (order.value === 'count') {
|
||||
return tagB.count - tagA.count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available,
|
||||
};
|
||||
}
|
||||
|
||||
if (order.value === 'priority') {
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available: [
|
||||
...priorityTags.map((priorityTag) => filtered.find((tag) => tag.slug === priorityTag)).filter(Boolean),
|
||||
...filtered.filter((tag) => !priorityTags.includes(tag.slug)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// page: [pageTag],
|
||||
selected,
|
||||
available: filtered,
|
||||
};
|
||||
});
|
||||
|
||||
function toggleTag(tag, combine) {
|
||||
if (props.filters.tags.includes(tag.slug)) {
|
||||
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
||||
return;
|
||||
}
|
||||
|
||||
if (combine) {
|
||||
emit('update', 'tags', props.filters.tags.concat(tag.slug));
|
||||
} else {
|
||||
emit('update', 'tags', [tag.slug]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-items {
|
||||
max-height: 15rem;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<script setup>
|
||||
const emit = defineEmits(['toggle']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="filters-toggle open"
|
||||
@@ -5,10 +9,6 @@
|
||||
><Icon icon="filter" /></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const emit = defineEmits(['toggle']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filters-toggle {
|
||||
min-width: 2.75rem;
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
<template>
|
||||
<div class="years-container">
|
||||
<select
|
||||
v-model="selected"
|
||||
class="years nobar"
|
||||
multiple
|
||||
@change="updateYears"
|
||||
>
|
||||
<option
|
||||
v-for="year in years"
|
||||
:key="`year-${year.year}`"
|
||||
class="year"
|
||||
:value="year.year"
|
||||
>{{ year.year }}</option>
|
||||
</select>
|
||||
|
||||
<Icon
|
||||
v-show="selected.length > 0"
|
||||
icon="cross2"
|
||||
class="clear"
|
||||
@click="clearYears"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -50,6 +25,31 @@ function clearYears() {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="years-container">
|
||||
<select
|
||||
v-model="selected"
|
||||
class="years nobar"
|
||||
multiple
|
||||
@change="updateYears"
|
||||
>
|
||||
<option
|
||||
v-for="year in years"
|
||||
:key="`year-${year.year}`"
|
||||
class="year"
|
||||
:value="year.year"
|
||||
>{{ year.year }}</option>
|
||||
</select>
|
||||
|
||||
<Icon
|
||||
v-show="selected.length > 0"
|
||||
icon="cross2"
|
||||
class="clear"
|
||||
@click="clearYears"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.years-container {
|
||||
display: flex;
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<span class="footer-segment">© traxxx</span>
|
||||
|
||||
<a
|
||||
v-if="env.links.discord"
|
||||
:href="env.links.discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="footer-segment footer-link nolink discord"
|
||||
><Icon icon="discord-full" /></a>
|
||||
|
||||
<a
|
||||
v-if="env.links.content"
|
||||
:href="env.links.content"
|
||||
target="_blank"
|
||||
class="footer-segment footer-link"
|
||||
>Content Removal / DMCA</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
@@ -26,17 +5,63 @@ const pageContext = inject('pageContext');
|
||||
const { env } = pageContext;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<div class="footer-row">
|
||||
<a
|
||||
v-if="env.links.matrix"
|
||||
:href="env.links.matrix"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="footer-segment footer-link nolink matrix"
|
||||
><Icon icon="matrix-full" /></a>
|
||||
|
||||
<a
|
||||
v-if="env.links.discord"
|
||||
:href="env.links.discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="footer-segment footer-link nolink discord"
|
||||
><Icon icon="discord-full" /></a>
|
||||
|
||||
<a
|
||||
v-if="env.links.content"
|
||||
:href="env.links.content"
|
||||
target="_blank"
|
||||
class="footer-segment footer-link"
|
||||
>Content Removal (DMCA)</a>
|
||||
</div>
|
||||
|
||||
<div class="footer-row">
|
||||
<span class="footer-segment">© traxxx v{{ env.version }}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
background: var(--background-base-20);
|
||||
box-shadow: inset 0 0 3px var(--shadow-weak-30);
|
||||
font-size: .8rem;
|
||||
font-size: .75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
|
||||
&:first-child .footer-segment {
|
||||
padding-top: .6rem;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: solid 1px var(--glass-weak-30);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-segment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -56,7 +81,8 @@ const { env } = pageContext;
|
||||
}
|
||||
}
|
||||
|
||||
.discord .icon {
|
||||
.discord .icon,
|
||||
.matrix .icon {
|
||||
height: 1.25rem;
|
||||
width: 3.5rem;
|
||||
fill: var(--glass-strong-10);
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
const emit = defineEmits(['sidebar']);
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="nav-list noselect">
|
||||
<a
|
||||
@@ -37,15 +46,6 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
const emit = defineEmits(['sidebar']);
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nav-list {
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,26 @@
|
||||
<script setup>
|
||||
import { useId } from 'vue';
|
||||
|
||||
defineProps({
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['change']);
|
||||
|
||||
const uid = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="check-container noselect">
|
||||
<span
|
||||
@@ -22,27 +45,6 @@
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const uid = String(Math.random()).slice(2);
|
||||
|
||||
defineProps({
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['change']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.check-container {
|
||||
display: flex;
|
||||
|
||||
92
components/form/password.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
password: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Password',
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
default: 'current-password',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change', 'input']);
|
||||
|
||||
const currentPassword = ref(props.password);
|
||||
const showPassword = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="password-container">
|
||||
<input
|
||||
v-model="currentPassword"
|
||||
:type="showPassword ? 'input' : 'password'"
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocomplete"
|
||||
minlength="3"
|
||||
class="password input"
|
||||
@change="emit('change', currentPassword)"
|
||||
@input="emit('input', currentPassword)"
|
||||
>
|
||||
|
||||
<div
|
||||
class="password-show"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
<Icon
|
||||
v-show="!showPassword"
|
||||
icon="eye"
|
||||
class="password-show"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-show="showPassword"
|
||||
icon="eye-blocked"
|
||||
class="password-show"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input {
|
||||
background: var(--background);
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.password-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.password-show {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding: 0 1rem 0 .5rem;
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +1,8 @@
|
||||
<template>
|
||||
<div class="range-container">
|
||||
<div
|
||||
class="label label-start"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
|
||||
>
|
||||
<slot name="start" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="range"
|
||||
:class="{ disabled }"
|
||||
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
|
||||
@click="setNearest"
|
||||
>
|
||||
<input
|
||||
v-model.number="range.a"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.a"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
<input
|
||||
v-model.number="range.b"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.b"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="label label-end"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
|
||||
>
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
nextTick,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -138,6 +85,59 @@ async function setRange(prop, value) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="range-container">
|
||||
<div
|
||||
class="label label-start"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.a === minValue ? 'a' : 'b', min)"
|
||||
>
|
||||
<slot name="start" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="range"
|
||||
:class="{ disabled }"
|
||||
:style="{ background: `linear-gradient(90deg, var(--slider-track) ${minPercentage}%, var(--slider-range) ${minPercentage}%, var(--slider-range) ${maxPercentage}%, var(--slider-track) ${maxPercentage}%)` }"
|
||||
@click="setNearest"
|
||||
>
|
||||
<input
|
||||
v-model.number="range.a"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.a"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
|
||||
<input
|
||||
v-model.number="range.b"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:data-value="range.b"
|
||||
:disabled="disabled"
|
||||
type="range"
|
||||
class="slider"
|
||||
@input="set('input')"
|
||||
@change="set('change')"
|
||||
@click.stop
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="label label-end"
|
||||
:class="{ disabled }"
|
||||
@click="setRange(range.b === maxValue ? 'b' : 'a', max)"
|
||||
>
|
||||
<slot name="end" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.dark .range-container .range {
|
||||
--slider-range: var(--lighten-weak-10);
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
<template>
|
||||
<label
|
||||
class="toggle-container noselect"
|
||||
:class="{ light: $store.state.ui.theme === 'dark' }"
|
||||
>
|
||||
<input
|
||||
:id="`toggle-${id}`"
|
||||
:checked="checked"
|
||||
:true-value="trueValue"
|
||||
:false-value="falseValue"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
class="toggle-input"
|
||||
@change="$emit('change', $event.target.checked)"
|
||||
>
|
||||
|
||||
<label
|
||||
:for="`toggle-${id}`"
|
||||
class="toggle"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
@@ -44,12 +21,35 @@ export default {
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
id: Math.floor(new Date().getTime() * Math.random()),
|
||||
id: Math.floor(Date.now() * Math.random()),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
class="toggle-container noselect"
|
||||
:class="{ light: $store.state.ui.theme === 'dark' }"
|
||||
>
|
||||
<input
|
||||
:id="`toggle-${id}`"
|
||||
:checked="checked"
|
||||
:true-value="trueValue"
|
||||
:false-value="falseValue"
|
||||
:disabled="disabled"
|
||||
type="checkbox"
|
||||
class="toggle-input"
|
||||
@change="$emit('change', $event.target.checked)"
|
||||
>
|
||||
|
||||
<label
|
||||
:for="`toggle-${id}`"
|
||||
class="toggle"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import 'breakpoints';
|
||||
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
<script setup>
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
ref,
|
||||
useId,
|
||||
} from 'vue';
|
||||
|
||||
import AlertDialog from '#/components/alerts/create.vue';
|
||||
import Notifications from '#/components/header/notifications.vue';
|
||||
import Settings from '#/components/settings/settings.vue';
|
||||
// import getPath from '#/src/get-path.js';
|
||||
|
||||
import { del } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
import logo from '../../assets/img/logo.svg?raw';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const theme = ref(pageContext.env.theme);
|
||||
|
||||
const user = pageContext.user;
|
||||
const primaryStash = pageContext.assets?.primaryStash;
|
||||
const unseen = ref(pageContext.assets?.unseenNotifications);
|
||||
const query = ref(pageContext.urlParsed.search.q || '');
|
||||
const allowLogin = pageContext.env.allowLogin;
|
||||
const searchFocused = ref(false);
|
||||
const showSettings = ref(false);
|
||||
const showAlertDialog = ref(false);
|
||||
const notifsDropdownId = useId();
|
||||
const menuDropdownId = useId();
|
||||
|
||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||
const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`;
|
||||
|
||||
function search() {
|
||||
navigate('/search', { q: query.value }, { redirect: true });
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme;
|
||||
|
||||
Cookies.set('theme', newTheme);
|
||||
events.emit('theme', newTheme);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await del('/session');
|
||||
navigate('/login?consent', null, { redirect: true }); // pass consent variable to reinstate in new session
|
||||
}
|
||||
|
||||
function blurSearch(event) {
|
||||
if (!event.relatedTarget || !Object.hasOwn(event.relatedTarget?.dataset, 'search')) {
|
||||
searchFocused.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<nav class="nav">
|
||||
@@ -92,6 +153,7 @@
|
||||
>
|
||||
<VDropdown
|
||||
v-if="user"
|
||||
:aria-id="notifsDropdownId"
|
||||
:triggers="['click']"
|
||||
:prevent-overflow="true"
|
||||
class="notifs-trigger"
|
||||
@@ -121,6 +183,7 @@
|
||||
</VDropdown>
|
||||
|
||||
<VDropdown
|
||||
:aria-id="menuDropdownId"
|
||||
:triggers="['click']"
|
||||
:prevent-overflow="true"
|
||||
class="menu-trigger"
|
||||
@@ -187,14 +250,59 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="user"
|
||||
class="menu-item"
|
||||
>
|
||||
<a
|
||||
:href="`/user/${user.username}/stashes`"
|
||||
class="menu-button nolink stashes"
|
||||
>
|
||||
<Icon icon="folder-heart" />
|
||||
Stashes
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="user"
|
||||
class="menu-item menu-div"
|
||||
>
|
||||
<a
|
||||
:href="`/user/${user.username}/alerts`"
|
||||
class="menu-button nolink alerts"
|
||||
>
|
||||
<Icon icon="alarm" />
|
||||
Alerts
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="showSettings = true"
|
||||
@touchstart="showSettings = true"
|
||||
>
|
||||
<Icon icon="equalizer" />
|
||||
Settings
|
||||
<Icon icon="filter" />
|
||||
Filters
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="theme === 'dark'"
|
||||
v-close-popper
|
||||
class="menu-item menu-button menu-div"
|
||||
@click="setTheme('light')"
|
||||
>
|
||||
<Icon icon="sun3" />
|
||||
Light theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-else
|
||||
v-close-popper
|
||||
class="menu-item menu-button menu-div"
|
||||
@click="setTheme('dark')"
|
||||
>
|
||||
<Icon icon="moon" />
|
||||
Dark theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
@@ -211,28 +319,6 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="theme === 'dark'"
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="setTheme('light')"
|
||||
@touchstart="setTheme('light')"
|
||||
>
|
||||
<Icon icon="sun3" />
|
||||
Light theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-else
|
||||
v-close-popper
|
||||
class="menu-item menu-button"
|
||||
@click="setTheme('dark')"
|
||||
@touchstart="setTheme('dark')"
|
||||
>
|
||||
<Icon icon="moon" />
|
||||
Dark theme
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="user"
|
||||
class="menu-button menu-item logout"
|
||||
@@ -257,64 +343,6 @@
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import { del } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
// import getPath from '#/src/get-path.js';
|
||||
|
||||
import Notifications from '#/components/header/notifications.vue';
|
||||
import Settings from '#/components/settings/settings.vue';
|
||||
import AlertDialog from '#/components/alerts/create.vue';
|
||||
|
||||
import logo from '../../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const theme = ref(pageContext.env.theme);
|
||||
|
||||
const user = pageContext.user;
|
||||
const primaryStash = pageContext.assets?.primaryStash;
|
||||
const unseen = ref(pageContext.assets?.unseenNotifications);
|
||||
const query = ref(pageContext.urlParsed.search.q || '');
|
||||
const allowLogin = pageContext.env.allowLogin;
|
||||
const searchFocused = ref(false);
|
||||
const showSettings = ref(false);
|
||||
const showAlertDialog = ref(false);
|
||||
|
||||
const activePage = computed(() => pageContext.urlParsed.pathname.split('/')[1]);
|
||||
const currentPath = `${pageContext.urlParsed.pathnameOriginal}${pageContext.urlParsed.searchOriginal || ''}`;
|
||||
|
||||
function search() {
|
||||
navigate('/search', { q: query.value }, { redirect: true });
|
||||
}
|
||||
|
||||
function setTheme(newTheme) {
|
||||
theme.value = newTheme;
|
||||
|
||||
Cookies.set('theme', newTheme);
|
||||
events.emit('theme', newTheme);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await del('/session');
|
||||
navigate('/login?consent', null, { redirect: true }); // pass consent variable to reinstate in new session
|
||||
}
|
||||
|
||||
function blurSearch(event) {
|
||||
if (!event.relatedTarget || !Object.hasOwn(event.relatedTarget?.dataset, 'search')) {
|
||||
searchFocused.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
@@ -540,6 +568,7 @@ function blurSearch(event) {
|
||||
align-items: center;
|
||||
padding: .75rem .5rem .75rem .75rem;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--glass);
|
||||
@@ -549,7 +578,6 @@ function blurSearch(event) {
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.logout) .icon {
|
||||
fill: var(--primary);
|
||||
@@ -561,6 +589,10 @@ function blurSearch(event) {
|
||||
}
|
||||
}
|
||||
|
||||
.menu-div {
|
||||
border-bottom: solid 1px var(--glass-weak-30);
|
||||
}
|
||||
|
||||
/*
|
||||
.favorites .icon {
|
||||
fill: var(--primary);
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
<script setup>
|
||||
import {
|
||||
defineEmits,
|
||||
inject,
|
||||
onMounted,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
import { get, patch } from '#/src/api.js';
|
||||
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const emit = defineEmits(['unseen', 'addAlert']);
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
|
||||
const notifications = ref([]);
|
||||
const done = ref(true);
|
||||
|
||||
async function fetchNotifications() {
|
||||
done.value = false;
|
||||
|
||||
const res = await get(`/users/${user?.id}/notifications`);
|
||||
notifications.value = res.notifications;
|
||||
|
||||
done.value = true;
|
||||
|
||||
emit('unseen', res.unseen);
|
||||
}
|
||||
|
||||
async function markSeen(notif) {
|
||||
if (notif.isSeen || !done.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
done.value = false;
|
||||
|
||||
await patch(`/users/${user?.id}/notifications/${notif.id}`, {
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await fetchNotifications();
|
||||
|
||||
done.value = true;
|
||||
}
|
||||
|
||||
async function markAllSeen() {
|
||||
done.value = false;
|
||||
|
||||
await patch(`/users/${user?.id}/notifications`, {
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await fetchNotifications();
|
||||
|
||||
done.value = true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchNotifications();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu">
|
||||
<div class="notifs-header">
|
||||
@@ -44,7 +108,15 @@
|
||||
class="notif-body notif-link nolink"
|
||||
>
|
||||
<span class="notif-trigger">
|
||||
<span class="notif-details">
|
||||
<span
|
||||
v-if="notif.matchedSceneIds.length > 0"
|
||||
class="notif-details"
|
||||
>Scene is released</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="notif-details"
|
||||
>
|
||||
New
|
||||
|
||||
<template
|
||||
@@ -88,71 +160,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
defineEmits,
|
||||
onMounted,
|
||||
inject,
|
||||
} from 'vue';
|
||||
|
||||
import { get, patch } from '#/src/api.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
import Ellipsis from '#/components/loading/ellipsis.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
|
||||
const notifications = ref([]);
|
||||
const done = ref(true);
|
||||
|
||||
const emit = defineEmits(['unseen', 'addAlert']);
|
||||
|
||||
async function fetchNotifications() {
|
||||
done.value = false;
|
||||
|
||||
const res = await get(`/users/${user?.id}/notifications`);
|
||||
notifications.value = res.notifications;
|
||||
|
||||
done.value = true;
|
||||
|
||||
emit('unseen', res.unseen);
|
||||
}
|
||||
|
||||
async function markSeen(notif) {
|
||||
if (notif.isSeen || !done.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
done.value = false;
|
||||
|
||||
await patch(`/users/${user?.id}/notifications/${notif.id}`, {
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await fetchNotifications();
|
||||
|
||||
done.value = true;
|
||||
}
|
||||
|
||||
async function markAllSeen() {
|
||||
done.value = false;
|
||||
|
||||
await patch(`/users/${user?.id}/notifications`, {
|
||||
seen: true,
|
||||
});
|
||||
|
||||
await fetchNotifications();
|
||||
|
||||
done.value = true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchNotifications();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notifs {
|
||||
width: 30rem;
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ active }"
|
||||
:title="title"
|
||||
class="icon"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const svg = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
@@ -27,11 +16,22 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const svg = ref(null);
|
||||
|
||||
import(`../../assets/img/icons/${props.icon}.svg?raw`).then((raw) => {
|
||||
svg.value = raw.default;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{ active }"
|
||||
:title="title"
|
||||
class="icon"
|
||||
v-html="svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
fill: var(--text);
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
<template>
|
||||
<a
|
||||
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
|
||||
rel="noopener"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
@@ -19,3 +10,12 @@ defineProps({
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
:class="{ active: pageContext.urlPathname === $attrs.href || active }"
|
||||
rel="noopener"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
<script setup>
|
||||
import { computed, ref, useId } from 'vue';
|
||||
|
||||
import Player from '#/components/video/player.vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
const props = defineProps({
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const playing = ref(false);
|
||||
const paused = ref(false);
|
||||
|
||||
const poster = computed(() => {
|
||||
if (props.release.poster) {
|
||||
return getPath(props.release.poster, 'thumbnail');
|
||||
}
|
||||
|
||||
if (props.release.covers?.length > 0) {
|
||||
return getPath(props.release.covers[0], 'thumbnail');
|
||||
}
|
||||
|
||||
if (props.release.photos?.length > 0) {
|
||||
return getPath(props.release.photos[0], 'thumbnail');
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
||||
const restrictedTooltipId = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="banner-container"
|
||||
@@ -49,7 +86,7 @@
|
||||
|
||||
<Icon
|
||||
v-if="(release.trailer || release.teaser).isRestricted"
|
||||
v-tooltip="'Restricted video'"
|
||||
v-tooltip="{ content: 'Restricted video', ariaId: restrictedTooltipId }"
|
||||
icon="blocked"
|
||||
class="restricted"
|
||||
/>
|
||||
@@ -102,42 +139,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
|
||||
import Player from '#/components/video/player.vue';
|
||||
|
||||
const props = defineProps({
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const playing = ref(false);
|
||||
const paused = ref(false);
|
||||
|
||||
const poster = computed(() => {
|
||||
if (props.release.poster) {
|
||||
return getPath(props.release.poster, 'thumbnail');
|
||||
}
|
||||
|
||||
if (props.release.covers?.length > 0) {
|
||||
return getPath(props.release.covers[0], 'thumbnail');
|
||||
}
|
||||
|
||||
if (props.release.photos?.length > 0) {
|
||||
return getPath(props.release.photos[0], 'thumbnail');
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const coversInAlbum = props.release.covers?.length > 0 && props.release.trailer;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.banner-container {
|
||||
background-position: center;
|
||||
|
||||