Added socials.

This commit is contained in:
DebaucheryLibrarian 2024-11-04 02:36:30 +01:00
parent 208f1dfde4
commit de60b67cb9
20 changed files with 945 additions and 142 deletions

View File

@ -43,3 +43,19 @@ body {
.icon.icon-fansly {
fill: #2699f6;
}
.icon.icon-linktree {
fill: #43e660;
}
.icon.icon-pornhub {
fill: #ff9000;
}
.icon.icon-cashapp {
fill: #00c853;
}
.icon.icon-loyalfans {
fill: #d90a16;
}

37
assets/img/icons/cashapp.svg Executable file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
class="app-icon"
viewBox="0 0 64 64"
version="1.1"
id="svg2"
sodipodi:docname="cashapp.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="4.2922526"
inkscape:cx="-6.4068923"
inkscape:cy="41.004111"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
id="path1"
style="fill-rule:nonzero"
d="M 22.300781 0 C 15.900781 0 12.699219 -0.000390625 9.1992188 1.0996094 A 13.6 13.6 0 0 0 1.0996094 9.1992188 C -0.000390625 12.659219 0 15.880781 0 22.300781 L 0 41.689453 C 0 48.099453 -0.000390625 51.300781 1.0996094 54.800781 A 13.6 13.6 0 0 0 9.1992188 62.900391 C 12.659219 64.000391 15.880781 64 22.300781 64 L 41.699219 64 C 48.099219 64 51.300781 64.000859 54.800781 62.880859 A 13.6 13.6 0 0 0 62.900391 54.779297 C 64.000391 51.319297 64 48.099688 64 41.679688 L 64 22.310547 C 64 15.900547 64.000391 12.699219 62.900391 9.1992188 A 13.6 13.6 0 0 0 54.800781 1.0996094 C 51.300781 -0.000390625 48.099219 0 41.699219 0 L 22.300781 0 z M 34.660156 10.009766 L 39.5 10.009766 C 40.33 10.009766 40.949297 10.789141 40.779297 11.619141 L 39.990234 15.419922 A 19.73 19.73 0 0 1 46.710938 19.259766 C 47.270938 19.799766 47.299531 20.699219 46.769531 21.199219 L 44.269531 23.800781 C 43.799531 24.300781 42.970703 24.300781 42.470703 23.800781 L 42.490234 23.820312 C 40.380234 21.920312 37.149062 20.529297 33.789062 20.529297 C 31.149062 20.529297 28.519531 21.490156 28.519531 23.910156 C 28.519531 26.400156 31.339609 27.229453 34.599609 28.439453 C 40.299609 30.409453 45 32.830312 45 38.570312 C 45 44.800312 40.27925 49.069844 32.53125 49.589844 L 31.832031 52.980469 A 1.32 1.32 0 0 1 30.53125 54.039062 L 25.681641 54 C 24.851641 53.99 24.242109 53.220625 24.412109 52.390625 L 25.152344 48.820312 C 22.120344 47.990313 19.459375 46.489922 17.359375 44.419922 A 1.36 1.36 0 0 1 17.359375 42.5 L 20.060547 39.800781 A 1.3 1.3 0 0 1 21.900391 39.800781 C 24.500391 42.410781 27.860547 43.480469 31.060547 43.480469 C 34.580547 43.480469 36.960938 42.019297 36.960938 39.529297 C 36.960937 37.109297 34.570547 36.47 30.060547 34.75 C 25.290547 33.04 20.779297 30.55 20.779297 24.75 C 20.779297 18.05 26.239687 14.779219 32.679688 14.449219 L 33.380859 11.070312 A 1.32 1.32 0 0 1 34.660156 10.009766 z " />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,16 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 494.1 100" style="enable-background:new 0 0 494.1 100;" xml:space="preserve">
<path d="M0,10.7h14.2v74.5h39.3v13.1H0V10.7z M67.5,10.7c4.8,0,8.9,3.7,8.9,8.6c0,4.9-4,8.8-8.9,8.8c-4.9,0-8.9-3.9-8.9-8.8
C58.6,14.5,62.5,10.7,67.5,10.7z M60.5,35.2h13.6v63.2H60.5V35.2z M82.2,35.2h13.6v8.7c4-6.7,10.9-10.4,20.1-10.4
c14.8,0,24,11.5,24,29.7v35.1h-13.6V64.5c0-11.8-5.2-18.5-14.5-18.5c-10.3,0-15.9,7-15.9,19.6v32.7H82.2L82.2,35.2L82.2,35.2z
M147.1,10.7h13.6v55.4l25.4-30.9h17.1l-27.1,31.6l27.1,31.5h-17.1l-25.4-30.8v30.8h-13.6V10.7z M208.6,19.1h13.9v16.1h16.2v11.3
h-16.2v32.5c0,4.1,2.5,6.7,6.5,6.7h9.1v12.7h-10.9c-11.8,0-18.5-7-18.5-19.4L208.6,19.1L208.6,19.1z M245.6,35.2h12.6V43
c3.4-6,9-9.5,15.9-9.5c2.1,0,3.2,0.1,4.8,0.6v12.6c-0.9-0.2-2.3-0.5-5.1-0.5c-10,0-15.5,8.4-15.5,22.8v29.2h-13.6V35.2H245.6z
M310.8,33.5c15,0,31.3,9,31.3,34.7V70h-48.8c1.1,11.3,7.6,17.5,18.6,17.5c7.9,0,14.5-4.2,16-10.1h13.9
C340.3,90,327.1,100,311.8,100c-19.6,0-32-12.7-32-33.3C279.8,48.4,291.7,33.5,310.8,33.5z M327.5,58.8c-1.9-7.8-8.1-12.7-16.7-12.7
c-8.3,0-14.2,5-16.5,12.7H327.5z M379.1,33.5c15,0,31.3,9,31.3,34.7V70h-48.8c1.1,11.3,7.6,17.5,18.6,17.5c7.9,0,14.5-4.2,16-10.1
H410C408.6,90,395.4,100,380.1,100c-19.6,0-32-12.7-32-33.3C348.1,48.4,360,33.5,379.1,33.5z M395.8,58.8
c-1.9-7.8-8.1-12.7-16.8-12.7c-8.3,0-14.2,5-16.5,12.7H395.8z M413.7,33.3H438l-17.3-16.4l9.5-9.7L446.7,24V0h14.3v24l16.5-16.8
l9.5,9.7l-17.3,16.4h24.3v13.6h-24.5L487,63.7l-9.5,9.5l-23.7-23.7l-23.7,23.7l-9.5-9.5L438,46.8h-24.5V33.3H413.7z M446.8,66.2
h14.3v32.2h-14.3V66.2z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

39
assets/img/icons/linktree.svg Executable file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 98 98.000003"
xml:space="preserve"
sodipodi:docname="linktree.svg"
width="98"
height="98"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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="3.7971541"
inkscape:cx="-1.1850981"
inkscape:cy="63.205231"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
d="M 9.2,33.2 H 33.4 L 16.1,16.8 25.6,7.2 42,23.9 V 0.1 H 56.2 V 23.9 L 72.6,7.2 82.1,16.8 64.8,33.1 H 89 V 46.6 H 64.7 L 82,63.3 72.5,72.7 49,49.2 25.5,72.8 16,63.3 33.3,46.6 H 9 V 33.2 Z m 32.9,32.7 h 14.2 v 32 H 42.1 Z"
id="path1">
</path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

40
assets/img/icons/loyalfans.svg Executable file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 213.99999 214"
xml:space="preserve"
sodipodi:docname="loyalfans.svg"
width="214"
height="214"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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="1.5023135"
inkscape:cx="-47.593262"
inkscape:cy="179.72281"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
d="m 71.2,29.9 c 8.5,-8.5 22.5,-8.5 31,0 8.5,8.5 8.5,22.5 0,31 -8.5,8.5 -22.5,8.5 -31,0 -8.5,-8.5 -8.7,-22.5 0,-31 z M 212,0.4 133.9,97.3 c -1.2,1.4 -2.4,3 -3.6,4.4 -5.9,7.7 -11.1,16 -15.2,24.9 -8.3,17.6 -13.1,37.4 -13.1,58.3 v 28.7 H 71 v -30.1 c 0,-2.6 0,-5.1 -0.2,-7.7 -1,-18 -5.5,-34.8 -12.7,-50.2 -1.8,-3.8 -3.8,-7.5 -5.9,-11.3 L 51.4,112.9 2,30.5 l 84.8,72 v 0 0 z"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="logo-default" data-testid="logo-default-icon" viewBox="0 -0.04 154.28 24.03">
<path d="m110.215 23.07 7.873-17.456h-3.862l-4.534 9.913-4.557-9.913h-3.858l7.89 17.465zm13.452-17.506h-3.537V23.07h3.537zm3.33.05v17.434h1.417v.031h3.51a8.76 8.76 0 0 0 6.154-2.567 7.963 7.963 0 0 0 1.868-2.761 8.624 8.624 0 0 0 0-6.8 8.348 8.348 0 0 0-1.868-2.793 8.586 8.586 0 0 0-6.154-2.54zm3.483 13.978V9.101h1.444a5.245 5.245 0 0 1 0 10.49zm17.339-3.885.077.027c1.818.623 2.892 1.493 2.892 2.441a1.919 1.919 0 0 1-1.918 1.769 6.034 6.034 0 0 1-4.68-1.965l-2.54 2.391a9.681 9.681 0 0 0 4.155 2.639 10.322 10.322 0 0 0 3.068.424 5.418 5.418 0 0 0 5.405-5.247 5.053 5.053 0 0 0-1.367-3.415 9.316 9.316 0 0 0-3.858-2.319c-.149-.05-.3-.1-.5-.149-1.6-.4-2.22-.9-2.242-1.791a1.225 1.225 0 0 1 .3-1 2.664 2.664 0 0 1 1.543-.65 5.379 5.379 0 0 1 3.366 1.2l.618.4 1.818-2.964-.573-.374a8.663 8.663 0 0 0-5.234-1.742 6.071 6.071 0 0 0-4.061 1.715 4.67 4.67 0 0 0-1.272 3.56 4.815 4.815 0 0 0 2.238 3.939 8.459 8.459 0 0 0 2.639 1.1zM95.543 5.61l-3.56 6.574-3.587-6.574h-3.858l5.7 11.059v6.4h3.483v-6.4L99.405 5.61zm-17.484 0v9.764L68.422 5.61h-1.1v17.461h3.488V12.882l9.809 10.188h.92V5.609zm-21.991 0-8.893 17.451h3.912l1.642-3.24h7.224l1.521 3.244h3.858l-8.244-17.46zm.42 6.849 1.818 3.889h-3.785zM43.21 5.605l-7.273 6.619L28.645 5.6h-1.2v17.461h3.488V12.377l5 4.557 5.008-4.557v10.684h3.488V5.6H43.21zM10.63 19.118a1.876 1.876 0 0 0-.041-.217c-.239-.911-.465-1.823-.726-2.725a.569.569 0 0 1 .176-.677 1.3 1.3 0 0 0 .221-1.543 1.418 1.418 0 0 0-1.313-.772 1.376 1.376 0 0 0-1.232.938 1.3 1.3 0 0 0 .415 1.507.358.358 0 0 1 .117.438c-.257.916-.492 1.836-.735 2.757a2.781 2.781 0 0 0-.041.289h3.158m-1.6-7.9a5.417 5.417 0 0 1 1.642-2.134 4.357 4.357 0 0 1 5.577.352 5.785 5.785 0 0 1 1.187 6.736 13.445 13.445 0 0 1-2.725 3.61 28.84 28.84 0 0 1-5.491 4.16.424.424 0 0 1-.352.027 27.5 27.5 0 0 1-6.4-5.094 9.672 9.672 0 0 1-2.279-3.84 5.677 5.677 0 0 1 1.8-5.766 4.324 4.324 0 0 1 6.6 1.169c.149.239.28.492.438.781M5.503 2.722c.4.379.79.781 1.214 1.132.352.293.537.262.772-.126.329-.546.618-1.11.9-1.683.036-.077-.036-.244-.108-.329A1.015 1.015 0 0 1 8.387.222a1.006 1.006 0 0 1 1.363 1.48.31.31 0 0 0-.072.438c.3.532.573 1.087.893 1.611.221.361.37.37.722.126a3.189 3.189 0 0 0 .447-.37c.266-.266.519-.541.776-.808-.492-.672-.4-1.358.2-1.678a1.006 1.006 0 0 1 1.354.406.985.985 0 0 1-.424 1.367.619.619 0 0 0-.388.492c-.23.947-.483 1.886-.717 2.833a.486.486 0 0 1-.532.415q-3-.007-5.992 0a.48.48 0 0 1-.532-.411c-.244-.966-.505-1.931-.74-2.9a.513.513 0 0 0-.334-.411 1 1 0 0 1-.564-1.132 1.026 1.026 0 0 1 .862-.767.982.982 0 0 1 1.074.659c.14.361.063.672-.284 1.142"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
data-name="logo-default"
data-testid="logo-default-icon"
viewBox="0 -0.04 25 24.999999"
version="1.1"
id="svg1"
width="25"
height="25"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="m 14.114527,19.614142 c -0.0094,-0.07308 -0.02308,-0.145535 -0.041,-0.217 -0.239,-0.911 -0.465,-1.823 -0.726,-2.725 -0.103352,-0.239339 -0.03083,-0.5183 0.176,-0.677 0.408042,-0.408238 0.498047,-1.036638 0.221,-1.543 -0.250584,-0.490502 -0.762542,-0.791516 -1.313,-0.772 -0.56432,0.03001 -1.052913,0.40201 -1.232,0.938 -0.212834,0.538692 -0.04361,1.15321 0.415,1.507 0.138253,0.09956 0.18719,0.28276 0.117,0.438 -0.257,0.916 -0.492,1.836 -0.735,2.757 -0.01872,0.09555 -0.0324,0.192015 -0.041,0.289 h 3.158 m -1.6,-7.9 c 0.359002,-0.838194 0.923783,-1.572203 1.642,-2.1340003 1.699386,-1.2472131 4.047914,-1.0989825 5.577,0.352 1.822799,1.7459463 2.303203,4.4721513 1.187,6.7360003 -0.697958,1.348581 -1.619326,2.569183 -2.725,3.61 -1.657551,1.601073 -3.501032,2.997701 -5.491,4.16 -0.107829,0.05974 -0.236321,0.0696 -0.352,0.027 -2.3753486,-1.369455 -4.5325397,-3.086444 -6.3999999,-5.094 -1.048787,-1.084717 -1.8292518,-2.39976 -2.2790001,-3.84 -0.5571739,-2.108189 0.1424143,-4.349203 1.8000001,-5.7660003 2.0738766,-1.7880035 5.2664089,-1.2225383 6.5999999,1.1690003 0.149,0.239 0.28,0.492 0.438,0.781 M 8.9875271,3.2181417 c 0.4,0.379 0.79,0.781 1.2139999,1.132 0.352,0.293 0.537,0.262 0.772,-0.126 0.329,-0.546 0.618,-1.11 0.9,-1.683 0.036,-0.077 -0.036,-0.244 -0.108,-0.329 -0.410242,-0.4325033 -0.361198,-1.1237515 0.106,-1.49399998 0.986666,-0.90866627 2.349666,0.57133368 1.363,1.47999998 -0.142839,0.099814 -0.17537,0.2977135 -0.072,0.438 0.3,0.532 0.573,1.087 0.893,1.611 0.221,0.361 0.37,0.37 0.722,0.126 0.159934,-0.109493 0.309554,-0.2333389 0.447,-0.37 0.266,-0.266 0.519,-0.541 0.776,-0.808 -0.492,-0.672 -0.4,-1.358 0.2,-1.678 0.486391,-0.2579836 1.089809,-0.077047 1.354,0.406 0.277315,0.4928925 0.08357,1.1175522 -0.424,1.367 -0.208917,0.081596 -0.357352,0.2698181 -0.388,0.492 -0.23,0.947 -0.483,1.886 -0.717,2.833 -0.03678,0.2589486 -0.271887,0.4423538 -0.532,0.415 -2,-0.00467 -3.997333,-0.00467 -5.9919999,0 -0.2596827,0.030788 -0.4962279,-0.1519569 -0.532,-0.411 -0.244,-0.966 -0.505,-1.931 -0.74,-2.9 -0.026543,-0.1883196 -0.15509,-0.3465019 -0.334,-0.411 -0.4330367,-0.1954461 -0.6687764,-0.6685975 -0.564,-1.132 0.1027353,-0.4083194 0.4445066,-0.7124244 0.862,-0.767 0.4703187,-0.069255 0.9227059,0.2083271 1.074,0.659 0.14,0.361 0.063,0.672 -0.284,1.142"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

45
assets/img/icons/pornhub.svg Executable file
View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.2"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 79.5 75"
overflow="visible"
xml:space="preserve"
sodipodi:docname="pornhub.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
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="10.906667"
inkscape:cx="39.746333"
inkscape:cy="37.5"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<path
id="path1"
d="M 64.099609 46 C 62.799609 46 61.599219 46.499609 60.699219 47.599609 C 59.799219 48.699609 59.300781 50.3 59.300781 52.5 C 59.300781 54.8 59.700391 56.4 60.400391 57.5 C 61.400391 59 62.700391 59.800781 64.400391 59.800781 C 65.600391 59.800781 66.699609 59.299219 67.599609 58.199219 C 68.499609 57.199219 68.9 55.4 69 53 C 69 50.5 68.499609 48.699609 67.599609 47.599609 C 66.699609 46.499609 65.499609 46 64.099609 46 z " /><path
id="path4"
d="M 3.6992188 25.300781 C 1.4992187 25.600781 0.2 26.900391 0 29.400391 L 0 70.800781 C 0.2 73.300781 1.3992187 74.700391 3.6992188 74.900391 L 75.900391 74.900391 C 78.100391 74.600391 79.399609 73.300781 79.599609 70.800781 L 79.599609 29.400391 C 79.299609 26.900391 78.100781 25.500781 75.800781 25.300781 L 3.6992188 25.300781 z M 7.0996094 33.900391 L 12.800781 33.900391 L 12.800781 43.699219 C 14.300781 42.299219 17.400781 41.599219 19.800781 41.699219 L 19.900391 41.699219 C 21.100391 41.699219 22.099609 42.000391 23.099609 42.400391 C 24.199609 42.900391 25 43.500781 25.5 44.300781 C 26 45.100781 26.400781 45.900391 26.800781 46.900391 C 27.000781 47.800391 27.099609 49.199219 27.099609 51.199219 L 27.099609 63.800781 L 21.300781 63.800781 L 21.300781 52.300781 C 21.300781 50.100781 21.2 48.6 21 48 C 20.7 47.4 20.400781 46.9 19.800781 46.5 C 19.200781 46.2 18.499219 46 17.699219 46 C 16.699219 46 15.899609 46.199219 15.099609 46.699219 C 14.299609 47.199219 13.700391 47.900781 13.400391 48.800781 C 13.100391 49.800781 12.900391 51.2 12.900391 53 L 12.900391 63.900391 L 7.0996094 63.900391 L 7.0996094 33.900391 z M 53.400391 33.900391 L 59.199219 33.900391 L 58.900391 43.599609 C 60.300391 42.399609 63.299219 41.499219 65.699219 41.699219 C 66.399219 41.799219 67 41.9 67.5 42 C 69.3 42.4 70.999219 43.199609 72.199219 44.599609 C 73.899219 46.499609 74.799609 49.199219 74.599609 52.699219 C 74.599609 56.399219 73.700391 59.300781 71.900391 61.300781 C 70.200391 63.300781 68 64.300781 65.5 64.300781 L 65.300781 64.300781 L 65 64.300781 C 62 64.300781 60.000781 63.199219 58.800781 62.199219 L 58.800781 63.900391 L 53.400391 63.900391 L 53.400391 33.900391 z M 30.300781 42.199219 L 36.099609 42.199219 L 36.099609 52.199219 C 36.099609 55.199219 36.200391 57.100781 36.400391 57.800781 C 36.700391 58.400781 37.099609 59.000391 37.599609 59.400391 C 38.099609 59.800391 38.799219 60 39.699219 60 C 40.599219 60 41.500781 59.699219 42.300781 59.199219 C 43.100781 58.699219 43.600391 57.999219 43.900391 57.199219 C 44.200391 56.399219 44.300781 54.400781 44.300781 51.300781 L 44.199219 42.199219 L 50 42.199219 L 50 64 L 44.699219 64 L 44.699219 61.599609 C 43.799219 62.699609 41.899219 64.4 38.199219 64.5 L 37.5 64.5 C 36.1 64.5 34.799609 64.1 33.599609 63.5 C 32.499609 62.9 31.599609 62.000781 31.099609 60.800781 C 30.599609 59.700781 30.300781 58 30.300781 56 L 30.300781 42.199219 z " />
<path
d="M74.6,9.9c-0.2-0.7-0.5-1.3-0.9-1.9c-0.4-0.6-1-1-1.9-1.4c-0.8-0.4-1.7-0.5-2.7-0.5c-1.8-0.1-4.7,0.6-5.6,2V6.5h-4.2v17h4.5 v-7.7c0-1.9,0.1-3.2,0.3-3.9c0.2-0.7,0.7-1.3,1.3-1.7c0.6-0.4,1.3-0.6,2.1-0.6c0.6,0,1.1,0.2,1.6,0.4c0.4,0.3,0.7,0.7,0.9,1.3 c0.2,0.5,0.3,1.7,0.3,3.6v8.7h4.5V12.9C74.8,11.6,74.7,10.6,74.6,9.9 M53.1,6.2c-1.3,0.2-2.6,1.2-3.2,1.9V6.5h-4.2v17h4.5v-5.3 c0-2.9,0.1-4.8,0.4-5.7c0.3-0.9,0.6-1.5,1-1.9c0.4-0.3,1-0.5,1.6-0.5c0.7,0,1.4,0.2,2.1,0.7l1.4-3.9c-1-0.6-1.9-0.8-3-0.8 C53.6,6.1,53.3,6.1,53.1,6.2 M34.8,6.1c-1.7,0-3.2,0.4-4.5,1.1c-1.4,0.7-2.4,1.8-3.1,3.2c-0.7,1.4-1.1,2.8-1.1,4.3 c0,2,0.4,3.6,1.1,5c0.7,1.4,1.8,2.4,3.2,3.1c1.4,0.7,2.9,1.1,4.5,1.1c2.5,0,4.6-0.8,6.3-2.5c1.7-1.7,2.5-3.8,2.5-6.4 c0-2.6-0.8-4.7-2.5-6.3C39.4,6.9,37.3,6.1,34.8,6.1 M37.7,18.9c-0.8,0.9-1.8,1.3-3,1.3c-1.2,0-2.2-0.4-3-1.3 C31,18,30.6,16.7,30.6,15s0.4-3,1.2-3.9c0.8-0.9,1.8-1.3,3-1.3c1.2,0,2.2,0.4,3,1.3c0.8,0.9,1.2,2.2,1.2,3.8 C38.9,16.7,38.5,18,37.7,18.9 M20.3,0.4C19.4,0.1,17.5,0,14.6,0H7v23.5h4.7v-8.9h3.1c2.1,0,3.8-0.1,4.9-0.3c0.8-0.2,1.7-0.6,2.5-1.1 s1.5-1.3,2-2.3c0.5-1,0.8-2.2,0.8-3.6c0-1.9-0.5-3.4-1.4-4.6C22.8,1.5,21.6,0.7,20.3,0.4 M19.6,9.1c-0.4,0.5-0.9,0.9-1.5,1.2 s-1.9,0.4-3.7,0.4h-2.6V4h2.3c1.7,0,2.8,0.1,3.4,0.2c0.8,0.1,1.4,0.5,1.9,1c0.5,0.6,0.8,1.3,0.8,2.1C20.1,8,20,8.6,19.6,9.1"
id="path3" />
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

3
assets/img/icons/twitter-x.svg Executable file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

14
assets/img/icons/x.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300.25"
height="300.25"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 178.695,127.15 290.395,0 h -26.46 L 166.905,110.38 89.465,0 H 0.125 L 117.255,166.93 0.125,300.25 h 26.46 l 102.4,-116.59 81.8,116.59 h 89.34 M 36.135,19.54 h 40.65 l 187.13,262.13 h -40.66"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -294,6 +294,44 @@
>{{ actor.agency }}</span>
</li>
<div class="bio-item bio-socials hideable">
<ul class="socials">
<a
v-for="social in socials"
:key="`social-${social.id}`"
:href="getSocialUrl(social)"
target="_blank"
rel="noopener"
:title="social.platform || social.url"
class="social ellipsis"
>
<Icon
v-if="social.platform && env.socials.urls[social.platform]"
:icon="iconMap[social.platform] || social.platform"
:title="social.platform"
:class="`icon-social icon-${social.platform}`"
/>
<Icon
v-else-if="social.platform"
icon="bubbles10"
:title="social.platform"
:class="`icon-social icon-${social.platform} icon-generic`"
/>
<Icon
v-else-if="social.url"
icon="sphere"
:title="social.platform"
: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>
<li class="bio-item updated">
<span
class="ellipsis"
@ -372,6 +410,7 @@
<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';
@ -379,7 +418,7 @@ import { formatDate } from '#/utils/format.js';
const expanded = ref(false);
const pageContext = inject('pageContext');
const user = pageContext.user;
const { user, env } = pageContext;
const props = defineProps({
actor: {
@ -388,6 +427,10 @@ const props = defineProps({
},
});
const iconMap = {
twitter: 'twitter-x',
};
// if the profile is empty, the expand button overlaps the header
const showExpand = [
'age',
@ -429,6 +472,25 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
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>
@ -653,6 +715,49 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
}
}
.bio-socials {
display: block;
}
.socials {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
grid-gap: 0 0;
overflow: hidden;
gap: .25rem;
padding: 0;
}
.social {
display: flex;
height: 2rem;
align-items: center;
padding: .1rem .5rem;
border-radius: .25rem;
color: inherit;
text-decoration: none;
font-size: .9rem;
font-weight: normal;
background: var(--highlight-weak-40);
.icon {
margin-right: .5rem;
}
.icon-generic {
fill: var(--highlight);
}
&:hover {
color: var(--primary);
cursor: pointer;
.icon {
fill: var(--primary);
}
}
}
.actor-actions {
display: flex;
flex-shrink: 0;

View File

@ -0,0 +1,109 @@
<template>
<ul class="socials nolist">
<li
v-for="social in socials"
:key="`social-${rev.id}-${index}-${social.id}`"
class="delta-item"
:class="{ modified: social.modified }"
>
<a
:href="getUrl(social)"
target="_blank"
class="link"
>
<Icon
v-if="social.platform && env.socials.urls[social.platform]"
:icon="iconMap[social.platform] || social.platform"
:title="social.platform"
:class="`icon-social icon-${social.platform}`"
/>
<Icon
v-else-if="social.platform"
icon="bubbles10"
:title="social.platform"
:class="`icon-social icon-${social.platform} icon-generic`"
/>
<Icon
v-else-if="social.url"
icon="sphere"
:title="social.platform"
:class="`icon-social icon-${social.platform} icon-generic`"
/>
{{ social.handle || social.url }}
</a>
</li>&nbsp;
</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;
flex-wrap: wrap;
gap: .5rem;
}
.delta-item .link {
display: inline-flex;
align-items: center;
padding: .25rem .5rem;
border-radius: .25rem;
box-shadow: 0 0 3px var(--shadow-weak-20);
color: inherit;
.icon {
margin-right: .5rem;
height: auto;
}
.icon-generic {
fill: var(--glass-strong-10);
}
}
.delta-item.modified {
font-weight: bold;
}
</style>

View File

@ -104,8 +104,15 @@
<div class="delta-deltas">
<span class="delta-from delta-value">
<Socials
v-if="delta.key === 'socials'"
:rev="rev"
:index="index"
:socials="rev.base[delta.key]"
/>
<ul
v-if="Array.isArray(rev.base[delta.key])"
v-else-if="Array.isArray(rev.base[delta.key])"
class="nolist"
>[
<li
@ -129,8 +136,15 @@
<span class="delta-arrow"></span>
<span class="delta-to delta-value">
<Socials
v-if="delta.key === 'socials'"
:rev="rev"
:index="index"
:socials="delta.value"
/>
<ul
v-if="Array.isArray(delta.value)"
v-else-if="Array.isArray(delta.value)"
class="nolist"
>[
<li
@ -218,6 +232,7 @@ 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';
@ -277,6 +292,14 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
}))];
}
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)) {
return [key, new Date(value)];
}
@ -300,6 +323,17 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
};
}
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)) {
return {
...delta,

289
components/edit/socials.vue Normal file
View File

@ -0,0 +1,289 @@
<template>
<ul
class="list nolist"
:class="{ disabled: !editing.has('socials') }"
>
<li
v-for="(social, index) in socials"
:key="`socials-${social}`"
class="list-item"
:class="{ deleted: !socials.some((listItem) => listItem.social === social.social || listItem.url === social.url) }"
>
<a
:href="getUrl(social)"
target="_blank"
class="link"
>
<Icon
v-if="social.platform && env.socials.urls[social.platform]"
:icon="iconMap[social.platform] || social.platform"
:title="social.platform"
:class="`icon-social icon-${social.platform}`"
/>
<Icon
v-else-if="social.platform"
icon="bubbles10"
:title="social.platform"
:class="`icon-social icon-${social.platform} icon-generic`"
/>
<Icon
v-else-if="social.url"
icon="sphere"
:title="social.platform"
:class="`icon-social icon-${social.platform} icon-generic`"
/>
{{ social.handle || social.url }}
</a>
<Icon
v-if="!socials.some((listItem) => listItem.social === social.social || listItem.url === social.url)"
icon="checkmark"
class="add noselect"
@click="socials = socials.concat(social)"
/>
<Icon
v-else
icon="cross2"
class="remove noselect"
@click="socials = socials.filter((listItem, listIndex) => listIndex !== index)"
/>
</li>
<li class="list-new">
<VDropdown>
<Icon
icon="plus2"
class="add noselect"
/>
<template #popper>
<form
class="new"
@submit.prevent="addSocial"
>
<div class="new-section">
<input
v-model="platform"
list="platforms"
class="input"
placeholder="Platform"
pattern="[a-z]+"
>
<datalist id="platforms">
<option value="onlyfans">OnlyFans</option>
<option value="twitter">Twitter/X</option>
<option value="instagram">Instagram</option>
<option value="pornhub">PornHub</option>
<option value="linktree">Linktree</option>
<option value="fansly">Fansly</option>
<option value="loyalfans">LoyalFans</option>
<option value="manyvids">ManyVids</option>
<option value="cashapp">Cash App</option>
</datalist>
<input
v-model="handle"
class="input"
placeholder="Handle"
pattern="[\w-]+"
:disabled="!!url"
>
</div>
<div class="new-section">
OR<input
v-model="url"
class="input"
placeholder="Website URL"
:disabled="!!handle"
>
<button
class="button"
>Add</button>
</div>
</form>
</template>
</VDropdown>
</li>
</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 = 'onlyfans';
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;
gap: .5rem;
&.disabled {
opacity: .5;
}
}
.list-item {
.icon-social {
margin-right: .5rem;
}
.icon-generic {
fill: var(--glass-strong-20);
}
&.deleted {
color: var(--glass);
text-decoration: line-through;
.icon.icon-social {
fill: var(--glass-weak-10);
}
}
}
.list-item,
.list-new {
display: inline-flex;
align-items: stretch;
border-radius: .25rem;
box-shadow: 0 0 3px var(--shadow-weak-30);
background: var(--background);
.link {
padding: .25rem 0 .25rem .5rem;
display: inline-flex;
align-items: center;
color: inherit;
}
.icon {
height: auto;
}
.add,
.remove {
padding: 0 .3rem;
margin-left: .5rem;
border-radius: .25rem;
&:hover {
fill: var(--text-light);
cursor: pointer;
}
}
.add {
fill: var(--success);
&:hover {
background: var(--success);
}
}
.remove {
fill: var(--error);
&:hover {
background: var(--error);
}
}
}
.list-new .add {
padding: .25rem .5rem;
background: var(--background);
margin: 0;
}
.new {
padding: .25rem;
}
.new-section {
display: flex;
align-items: center;
gap: .5rem;
padding: .25rem;
.input {
flex-grow: 1;
&:disabled {
opacity: .5;
}
}
}
</style>

View File

@ -66,6 +66,23 @@ module.exports = {
bans: {
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
},
socials: {
urls: {
cashapp: 'https://cash.app/${handle}', // eslint-disable-line no-template-curly-in-string
fansly: 'https://fansly.com/{handle}',
linktree: 'https://linktr.ee/{handle}',
loyalfans: 'https://www.loyalfans.com/{handle}',
manyvids: 'https://www.manyvids.com/Profile/{handle}/slug/Store/Videos',
onlyfans: 'https://onlyfans.com/{handle}',
pornhub: 'https://www.pornhub.com/model/{handle}',
twitter: 'https://x.com/{handle}',
},
prefix: {
default: '@',
cashapp: '$',
reddit: 'u/',
},
},
apiAccess: {
graphqlEnabled: true,
keySize: 24, // bytes

11
package-lock.json generated
View File

@ -54,6 +54,7 @@
"redis": "^4.6.12",
"sharp": "^0.32.6",
"sirv": "^2.0.3",
"template-format": "^1.2.5",
"unprint": "^0.14.1",
"video.js": "^8.10.0",
"vike": "^0.4.150",
@ -10316,6 +10317,11 @@
"node": ">=8.0.0"
}
},
"node_modules/template-format": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/template-format/-/template-format-1.2.5.tgz",
"integrity": "sha512-ZZqSfqYBMfPjouADYSRN9iaYlLr2PPVFYgULcV8cGMrJbifNXKvP7qx5PBFQjXg5mh1Gwkk+LTgdsZ8bmSvBdw=="
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@ -18962,6 +18968,11 @@
"resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz",
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="
},
"template-format": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/template-format/-/template-format-1.2.5.tgz",
"integrity": "sha512-ZZqSfqYBMfPjouADYSRN9iaYlLr2PPVFYgULcV8cGMrJbifNXKvP7qx5PBFQjXg5mh1Gwkk+LTgdsZ8bmSvBdw=="
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",

View File

@ -54,6 +54,7 @@
"redis": "^4.6.12",
"sharp": "^0.32.6",
"sirv": "^2.0.3",
"template-format": "^1.2.5",
"unprint": "^0.14.1",
"video.js": "^8.10.0",
"vike": "^0.4.150",

View File

@ -217,47 +217,12 @@
</span>
</div>
<ul
v-if="item.type === 'list'"
class="list nolist"
:class="{ disabled: !editing.has(item.key) }"
>
<li
v-for="(value, index) in item.value"
:key="`${item.type}-${value}`"
class="list-item"
:class="{ deleted: !edits[item.key].some((listItem) => listItem.value === value.value || listItem.url === value.url) }"
>
<Icon
v-if="value.icon"
:icon="value.icon"
:class="`icon-social icon-${value.icon}`"
/>
<a
v-if="value.url"
:href="value.url"
target="_blank"
class="link"
>{{ value.url }}</a>
<template v-else>{{ value.value || value }}</template>
<Icon
v-if="!edits[item.key].some((listItem) => listItem.value === value.value || listItem.url === value.url)"
icon="checkmark"
class="add noselect"
@click="edits[item.key] = edits[item.key].concat(value)"
/>
<Icon
v-else
icon="cross2"
class="remove noselect"
@click="edits[item.key] = edits[item.key].filter((listItem, listIndex) => listIndex !== index)"
/>
</li>
</ul>
<EditSocials
v-if="item.type === 'socials'"
:edits="edits"
:editing="editing"
@socials="(socials) => edits.socials = socials"
/>
<EditPlace
v-if="item.type === 'place'"
@ -373,6 +338,7 @@
import { ref, computed, inject } from 'vue';
import { format } from 'date-fns';
import EditSocials from '#/components/edit/socials.vue';
import EditPlace from '#/components/edit/place.vue';
import EditFigure from '#/components/edit/figure.vue';
import EditAugmentation from '#/components/edit/augmentation.vue';
@ -423,16 +389,11 @@ const fields = computed(() => [
: null,
inline: true,
},
/*
{
key: 'socials',
type: 'list',
value: actor.value.socials.map((social) => ({
url: social.url,
icon: social.platform,
})),
type: 'socials',
value: actor.value.socials,
},
*/
{
key: 'origin',
type: 'place',
@ -647,7 +608,7 @@ async function submit() {
actorId: actor.value.id,
edits: {
...Object.fromEntries(Array.from(editing.value).flatMap((key) => {
if (edits.value[key] && typeof edits.value[key] === 'object') {
if (edits.value[key] && typeof edits.value[key] === 'object' && !Array.isArray(edits.value[key])) {
return Object.entries(edits.value[key]).map(([valueKey, value]) => [keyMap[key]?.[valueKey] || valueKey, value]);
}
@ -844,61 +805,6 @@ async function submit() {
}
}
.list {
&.disabled {
opacity: .5;
}
}
.list-item {
display: flex;
align-items: center;
border-radius: .25rem;
background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak-30);
.icon-social {
margin: 0 .5rem;
}
&.deleted {
color: var(--glass);
text-decoration: line-through;
.icon.icon-social {
fill: var(--glass-weak-10);
}
}
.add,
.remove {
padding: .25rem .3rem;
margin-left: .5rem;
border-radius: .25rem;
&:hover {
fill: var(--text-light);
cursor: pointer;
}
}
.add {
fill: var(--success);
&:hover {
background: var(--success);
}
}
.remove {
fill: var(--error);
&:hover {
background: var(--error);
}
}
}
.avatars {
width: 100%;
display: flex;

View File

@ -55,6 +55,8 @@ const keyMap = {
isCircumcised: 'circumcised',
};
const socialsOrder = ['onlyfans', 'twitter'];
export function curateActor(actor, context = {}) {
return {
id: actor.id,
@ -115,9 +117,11 @@ export function curateActor(actor, context = {}) {
agency: actor.agency,
avatar: curateMedia(actor.avatar),
socials: context.socials?.map((social) => ({
id: social.id,
url: social.url,
platform: social.platform,
})),
handle: social.handle,
})).toSorted((socialA, socialB) => socialsOrder.indexOf(socialB.platform) - socialsOrder.indexOf(socialA.platform)),
profiles: context.profiles?.map((profile) => ({
id: profile.id,
description: profile.description,
@ -216,7 +220,7 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
.leftJoin('media', 'media.id', 'actors_avatars.media_id')
.groupBy('media.id', 'actors_avatars.actor_id')
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
knex('actors_social')
knex('actors_socials')
.whereIn('actor_id', actorIds),
reqUser
? knex('stashes_actors')
@ -527,14 +531,14 @@ export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
}
async function applyActorValueDelta(profileId, delta, trx) {
return knex('actors_profiles')
await knex('actors_profiles')
.where('id', profileId)
.update(keyMap[delta.key] || delta.key, delta.value)
.transacting(trx);
}
async function applyActorDirectDelta(actorId, delta, trx) {
return knex('actors')
await knex('actors')
.where('id', actorId)
.update(keyMap[delta.key] || delta.key, delta.value)
.modify((builder) => {
@ -545,6 +549,22 @@ async function applyActorDirectDelta(actorId, delta, trx) {
.transacting(trx);
}
async function applyActorSocialsDelta(actorId, delta, trx) {
await knex('actors_socials')
.where('actor_id', actorId)
.delete()
.transacting(trx);
await knex('actors_socials')
.insert(delta.value.map((social) => ({
actor_id: actorId,
platform: social.platform,
handle: social.handle,
url: social.url,
})))
.transacting(trx);
}
async function fetchMainProfile(actorId, wasCreated = false) {
const profileEntry = await knex('actors_profiles')
.where('actor_id', actorId)
@ -623,6 +643,10 @@ async function applyActorRevision(revisionIds, reqUser) {
return applyActorValueDelta(mainProfile.id, delta, trx);
}
if (delta.key === 'socials') {
return applyActorSocialsDelta(revision.actor_id, delta, trx);
}
if (delta.key === 'name' && reqUser.role === 'admin') {
return applyActorDirectDelta(revision.actor_id, delta, trx);
}
@ -767,35 +791,59 @@ function convertWeight(weight, units) {
return Number(weight) || null;
}
export async function createActorRevision(actorId, {
edits,
comment,
apply,
...options
}, reqUser) {
const [
[actor],
openRevisions,
] = await Promise.all([
fetchActorsById([actorId], {
reqUser,
includeAssets: true,
includePartOf: true,
}),
knex('actors_revisions')
.where('user_id', reqUser.id)
.whereNull('approved'),
]);
const platformsByHostname = Object.fromEntries(Object.entries(config.socials.urls).map(([platform, url]) => {
const { hostname, pathname } = new URL(url);
if (!actor) {
throw new HttpError(`No actor with ID ${actorId} found to update`, 404);
}
return [hostname, {
platform,
pathname: decodeURIComponent(pathname),
url,
}];
}));
if (openRevisions.length >= config.revisions.unapprovedLimit && reqUser.role !== 'admin') {
throw new HttpError(`You have ${config.revisions.unapprovedLimit} unapproved revisions, please wait for approval before submitting another revision.`, 429);
}
function curateSocials(socials) {
return socials.map((social) => {
if (!social.handle && !social.url) {
throw new Error('No social handle or website URL specified');
}
const baseActor = Object.fromEntries(Object.entries(actor).map(([key, values]) => {
if (social.handle && !social.platform) {
throw new Error('No platform specified for social handle');
}
if (social.handle && social.platform && /[\w-]+/.test(social.handle) && /[a-z]+/i.test(social.platform)) {
return {
platform: social.platform.toLowerCase(),
handle: social.handle,
};
}
if (social.url) {
const { hostname, pathname } = new URL(social.url);
const platform = platformsByHostname[hostname];
if (platform) {
const handle = pathname.match(new RegExp(platform.pathname.replace('{handle}', '([\\w-]+)')))?.[1];
if (handle) {
return {
platform: platform.platform,
handle,
};
}
}
return {
url: social.url,
};
}
throw new Error('Invalid social');
}).filter(Boolean);
}
function getBaseActor(actor) {
return Object.fromEntries(Object.entries(actor).map(([key, values]) => {
if ([
'scenes',
'likes',
@ -805,11 +853,11 @@ export async function createActorRevision(actorId, {
return null;
}
/* avatar should return id
if (values?.hash) {
return [key, values.hash];
if ([
'socials',
].includes(key)) {
return [key, values];
}
*/
if (values?.id) {
return [key, values.id];
@ -825,8 +873,10 @@ export async function createActorRevision(actorId, {
return [key, values];
}).filter(Boolean));
}
const deltas = await Promise.all(Object.entries(edits).map(async ([key, value]) => {
function getDeltas(edits, baseActor, options) {
return Promise.all(Object.entries(edits).map(async ([key, value]) => {
if (baseActor[key] === value || typeof value === 'undefined') {
return null;
}
@ -890,6 +940,24 @@ export async function createActorRevision(actorId, {
];
}
if (key === 'socials') {
const convertedSocials = curateSocials(value);
const convertedUrls = value
.filter((social) => social.url && !convertedSocials.some((convertedSocial) => convertedSocial.url === social.url))
.map((social) => social.url);
const conversionComment = convertedUrls.length > 0
? `curated URLs ${convertedUrls.join(', ')} as social handles`
: null;
return {
key,
value: convertedSocials,
comment: conversionComment,
};
}
if (['cup', 'bust', 'waist', 'hip'].includes(key)) {
const convertedValue = convertFigure(key, value, options.figureUnits);
@ -967,6 +1035,38 @@ export async function createActorRevision(actorId, {
return { key, value };
})).then((rawDeltas) => rawDeltas.flat().filter(Boolean));
}
export async function createActorRevision(actorId, {
edits,
comment,
apply,
...options
}, reqUser) {
const [
[actor],
openRevisions,
] = await Promise.all([
fetchActorsById([actorId], {
reqUser,
includeAssets: true,
includePartOf: true,
}),
knex('actors_revisions')
.where('user_id', reqUser.id)
.whereNull('approved'),
]);
if (!actor) {
throw new HttpError(`No actor with ID ${actorId} found to update`, 404);
}
if (openRevisions.length >= config.revisions.unapprovedLimit && reqUser.role !== 'admin') {
throw new HttpError(`You have ${config.revisions.unapprovedLimit} unapproved revisions, please wait for approval before submitting another revision.`, 429);
}
const baseActor = getBaseActor(actor);
const deltas = await getDeltas(edits, baseActor, options);
const deltaComments = deltas.map((delta) => delta.comment);
const curatedComment = [comment, ...deltaComments].filter(Boolean).join(' | ');

View File

@ -42,6 +42,7 @@ export default async function mainHandler(req, res, next) {
media: config.media,
psa: config.psa,
links: config.links,
socials: config.socials,
},
meta: {
now: new Date().toISOString(),