Merge branch 'main' into feature/157-update-main-navigation
|
@ -1,3 +1,5 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 16H0V0H2V16ZM4 5V3H16V5H4ZM4 7V9H16V7H4ZM4 13V11H16V13H4Z" fill="#898C94"/>
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M2 16H0V0H2V16ZM4 5V3H16V5H4ZM4 7V9H16V7H4ZM4 13V11H16V13H4Z"
|
||||||
|
fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 254 B |
|
@ -1,3 +1,5 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.77779V0H16.0001V1.77779H0ZM10.9092 9.4546H5.09094V2.90911H10.9092V9.4546ZM12.3637 3.63638H16.0001V5.09094H12.3637V3.63638ZM0 10.9092H16.0001V12.3637H0V10.9092ZM3.63638 3.63638H0V5.09094H3.63638V3.63638ZM12.3637 8.72732H16.0001V7.27277H12.3637V8.72732ZM3.63638 8.72732H0V7.27277H3.63638V8.72732ZM0 16H16.0001V14.2222H0V16Z" fill="#898C94"/>
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M0 1.77779V0H16.0001V1.77779H0ZM10.9092 9.4546H5.09094V2.90911H10.9092V9.4546ZM12.3637 3.63638H16.0001V5.09094H12.3637V3.63638ZM0 10.9092H16.0001V12.3637H0V10.9092ZM3.63638 3.63638H0V5.09094H3.63638V3.63638ZM12.3637 8.72732H16.0001V7.27277H12.3637V8.72732ZM3.63638 8.72732H0V7.27277H3.63638V8.72732ZM0 16H16.0001V14.2222H0V16Z"
|
||||||
|
fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 520 B |
5
public/icons/hide-table-of-contents-2.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect fill="#fff" x="0.999512" y="27" width="26" height="26" transform="rotate(-90 0.999512 27)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M6 9.24437C6 8.55582 6.57935 8 7.29706 8C8.01477 8 8.59412 8.55582 8.59412 9.24437C8.59412 9.93293 8.01477 10.4887 7.29706 10.4887C6.57935 10.4887 6 9.93293 6 9.24437ZM6 14.222C6 13.5334 6.57935 12.9776 7.29706 12.9776C8.01477 12.9776 8.59412 13.5334 8.59412 14.222C8.59412 14.9105 8.01477 15.4663 7.29706 15.4663C6.57935 15.4663 6 14.9105 6 14.222ZM7.29706 17.9548C6.57935 17.9548 6 18.5189 6 19.1991C6 19.8794 6.588 20.4435 7.29706 20.4435C8.00612 20.4435 8.59412 19.8794 8.59412 19.1991C8.59412 18.5189 8.01477 17.9548 7.29706 17.9548ZM22.0024 20.0283H9.89648V18.3691H22.0024V20.0283ZM9.89648 15.0517H22.0024V13.3926H9.89648V15.0517ZM9.89648 10.0742V8.41504H22.0024V10.0742H9.89648Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 953 B |
|
@ -1,3 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M22 24V21H12V19H22V16L26 20L22 24Z" fill="black"/>
|
<rect fill="#fff" x="0.999512" y="27" width="26" height="26" transform="rotate(-90 0.999512 27)" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M10.9995 18V15H19.9995V13H10.9995V10L6.99951 14L10.9995 18Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 163 B After Width: | Height: | Size: 333 B |
|
@ -1,4 +1,4 @@
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="40" height="40" fill="#F7F7F7"/>
|
<rect x="1" y="1" width="26" height="26" stroke="currentColor" stroke-width="2" fill="#fff"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 15.2444C12 14.5558 12.5794 14 13.2971 14C14.0148 14 14.5941 14.5558 14.5941 15.2444C14.5941 15.9329 14.0148 16.4887 13.2971 16.4887C12.5794 16.4887 12 15.9329 12 15.2444ZM12 20.222C12 19.5334 12.5794 18.9776 13.2971 18.9776C14.0148 18.9776 14.5941 19.5334 14.5941 20.222C14.5941 20.9105 14.0148 21.4663 13.2971 21.4663C12.5794 21.4663 12 20.9105 12 20.222ZM13.2971 23.9548C12.5794 23.9548 12 24.5189 12 25.1991C12 25.8794 12.588 26.4435 13.2971 26.4435C14.0061 26.4435 14.5941 25.8794 14.5941 25.1991C14.5941 24.5189 14.0148 23.9548 13.2971 23.9548ZM28.0015 26.0284H15.8956V24.3692H28.0015V26.0284ZM15.8956 21.0517H28.0015V19.3925H15.8956V21.0517ZM15.8956 16.0741V14.4149H28.0015V16.0741H15.8956Z" fill="black"/>
|
<path d="M6 9.24437C6 8.55582 6.57935 8 7.29706 8C8.01477 8 8.59412 8.55582 8.59412 9.24437C8.59412 9.93293 8.01477 10.4887 7.29706 10.4887C6.57935 10.4887 6 9.93293 6 9.24437ZM6 14.222C6 13.5334 6.57935 12.9776 7.29706 12.9776C8.01477 12.9776 8.59412 13.5334 8.59412 14.222C8.59412 14.9105 8.01477 15.4663 7.29706 15.4663C6.57935 15.4663 6 14.9105 6 14.222ZM7.29706 17.9548C6.57935 17.9548 6 18.5189 6 19.1991C6 19.8794 6.588 20.4435 7.29706 20.4435C8.00612 20.4435 8.59412 19.8794 8.59412 19.1991C8.59412 18.5189 8.01477 17.9548 7.29706 17.9548ZM22.0015 20.0284H9.89562V18.3692H22.0015V20.0284ZM9.89562 15.0517H22.0015V13.3925H9.89562V15.0517ZM9.89562 10.0741V8.41491H22.0015V10.0741H9.89562Z" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 916 B |
6
public/icons/user-link-behance.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.2995 19.5H7.70052C6.84635 19.5 6.04167 19.1667 5.4375 18.5625C4.83333 17.9583 4.5 17.1536 4.5 16.2969V7.70313C4.5 6.84635 4.83333 6.04167 5.4375 5.4375C6.04167 4.83333 6.84635 4.5 7.70052 4.5H16.2995C17.1536 4.5 17.9583 4.83333 18.5625 5.4375C19.1667 6.04167 19.5 6.84635 19.5 7.70313V16.2995C19.5 17.1536 19.1667 17.9583 18.5625 18.5625C17.9583 19.1667 17.1536 19.5 16.2995 19.5ZM7.70052 6C7.2474 6 6.82031 6.17708 6.4974 6.5C6.17708 6.82031 6 7.2474 6 7.70313V16.2995C6 16.7526 6.17708 17.1797 6.4974 17.5026C6.82031 17.8229 7.2474 18 7.70052 18H16.2995C16.7526 18 17.1797 17.8229 17.5026 17.5026C17.8229 17.1797 18 16.7526 18 16.2969V7.70313C18 7.2474 17.8229 6.82031 17.5026 6.5C17.1797 6.17708 16.7526 6 16.2995 6H7.70052Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3074 9.375H14.0574C13.849 9.375 13.6824 9.20573 13.6824 9C13.6824 8.79167 13.849 8.625 14.0574 8.625H16.3074C16.5157 8.625 16.6824 8.79167 16.6824 9C16.6824 9.20573 16.5157 9.375 16.3074 9.375Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.797 11.7552C11.1537 11.8776 11.8074 12.224 11.8074 13.2838C11.8074 14.9297 10.0235 15 9.78654 15H6.93237V9H9.66675C9.96623 9 11.4324 8.95052 11.4324 10.5C11.4324 11.3229 11.0339 11.6328 10.797 11.7552ZM8.43237 10.125V11.25H9.41414C9.54956 11.25 9.99487 11.1641 9.99487 10.6615C9.99487 10.1615 9.41414 10.125 9.3256 10.125H8.43237ZM9.53133 13.8672C9.62768 13.8672 10.297 13.8307 10.297 13.1484C10.297 12.4661 9.77091 12.375 9.53133 12.375H8.43237V13.8672H9.53133Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.1772 13.5H17.3725C17.1589 14.2005 16.3985 15 15.2449 15C14.4845 15 12.9324 14.5781 12.9324 12.5625C12.9324 10.3854 14.6121 10.125 15.1824 10.125C17.198 10.125 17.4792 11.8125 17.4324 13.125H14.4324C14.4324 13.125 14.6251 13.8672 15.3751 13.8672C15.7475 13.8672 16.0079 13.6849 16.1772 13.5ZM15.2162 11.099C14.7188 11.0911 14.1563 11.5312 14.1694 12H16.198C16.1667 11.7083 16.0704 11.4766 15.9115 11.3333C15.7527 11.1875 15.5626 11.1068 15.2162 11.099Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0"?>
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<svg width="12px" height="12px" viewBox="0 0 12 12"
|
<path
|
||||||
enable-background="new 0 0 12 12" version="1.1"
|
d="M15.4476 3.67969C14.2002 3.67969 12.9502 4.15625 11.9997 5.10677L9.87988 7.22656C7.97624 9.1276 7.97624 12.2188 9.87988 14.1198C10.4554 14.6953 11.1325 15.0938 11.8617 15.3203L12.2653 14.9167C12.5283 14.6536 12.7132 14.3411 12.8148 14.0078C12.1273 13.9036 11.4658 13.5885 10.9398 13.0599C9.62467 11.7448 9.62467 9.60156 10.9398 8.28646L13.0596 6.16667C14.3747 4.85156 16.5179 4.85156 17.833 6.16667C19.1481 7.48177 19.1481 9.625 17.833 10.9401L16.5622 12.2083C16.695 12.8984 16.7028 13.6068 16.5908 14.2943C16.6507 14.237 16.7132 14.1797 16.7731 14.1198L18.8929 12C20.7939 10.099 20.7939 7.00781 18.8929 5.10677C17.9424 4.15625 16.695 3.67969 15.4476 3.67969ZM12.1377 8.67969L11.734 9.08333C11.4736 9.34635 11.2861 9.65885 11.1872 9.99219C11.8721 10.0964 12.5335 10.4115 13.0596 10.9401C14.3747 12.2552 14.3747 14.3958 13.0596 15.7109L10.9398 17.8333C9.62467 19.1484 7.48145 19.1484 6.16634 17.8333C4.85124 16.5182 4.85124 14.375 6.16634 13.0599L7.43717 11.7917C7.30436 11.1016 7.29655 10.3932 7.40853 9.70573C7.34863 9.76302 7.28613 9.82031 7.22624 9.88021L5.10645 12C3.2054 13.901 3.2054 16.9922 5.10645 18.8932C7.00749 20.7943 10.0986 20.7943 11.9997 18.8932L14.1195 16.7734C16.0231 14.8724 16.0231 11.7813 14.1195 9.88021C13.5439 9.30469 12.8669 8.90625 12.1377 8.67969Z" fill="currentColor"/>
|
||||||
xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path
|
</svg>
|
||||||
d="M3.7796631,8.75C4.1953125,10.6790771,5.0326538,12,6,12s1.8046875-1.3209229,2.2203369-3.25H3.7796631z" fill="#fff"/><path d="M9.2371216,3.25h2.0916748c-0.664978-1.2857056-1.779541-2.2975464-3.1375122-2.8312378 C8.6710815,1.1671753,9.0209351,2.1549683,9.2371216,3.25z" fill="#fff"/><path d="M8.3912964,4.25H3.6087036C3.5383911,4.803833,3.5,5.3909912,3.5,6s0.0383911,1.196167,0.1087036,1.75 h4.7825928C8.4616089,7.196167,8.5,6.6090088,8.5,6S8.4616089,4.803833,8.3912964,4.25z" fill="#fff"/><path d="M9.5,6c0,0.5882568-0.0372925,1.1765137-0.1055298,1.75h2.3445435C11.9077148,7.196167,12,6.6090088,12,6 s-0.0922852-1.196167-0.2609863-1.75H9.3944702C9.4627075,4.8234863,9.5,5.4117432,9.5,6z" fill="#fff"/><path d="M8.2203369,3.25C7.8046875,1.3209229,6.9673462,0,6,0S4.1953125,1.3209229,3.7796631,3.25H8.2203369z" fill="#fff"/><path d="M2.7628784,8.75H0.6712036c0.664978,1.2857056,1.779541,2.2975464,3.1375122,2.8312378 C3.3289185,10.8328247,2.9790649,9.8450317,2.7628784,8.75z" fill="#fff"/><path d="M2.5,6c0-0.5882568,0.0372925-1.1765137,0.1055298-1.75H0.2609863C0.0922852,4.803833,0,5.3909912,0,6 s0.0922852,1.196167,0.2609863,1.75h2.3445435C2.5372925,7.1765137,2.5,6.5882568,2.5,6z" fill="#fff"/><path d="M9.2371216,8.75c-0.2161865,1.0950317-0.56604,2.0828247-1.0458374,2.8312378 C9.5492554,11.0475464,10.6638184,10.0357056,11.3287964,8.75H9.2371216z" fill="#fff"/><path d="M2.7628784,3.25c0.2161865-1.0950317,0.56604-2.0828247,1.0458374-2.8312378 C2.4507446,0.9524536,1.3361816,1.9642944,0.6712036,3.25H2.7628784z" fill="#fff"/></svg>
|
|
||||||
|
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.4 KiB |
4
public/icons/user-link-dribbble.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.5312 17.0625C10.3411 17.0625 8.82025 17.0339 8.31504 15.6042C7.80202 14.1458 8.01035 12.7865 9.14056 12.0026C9.95306 11.4427 10.6822 11.5807 10.6952 11.5807C10.651 10.9167 10.8255 9.32292 10.8255 9.32292C11.0937 7.88542 11.3046 6.75 12.3749 6.75C13.6952 6.75 13.9947 8.94271 14.0598 9.88542C14.1614 11.3229 14.2109 12.5182 13.8411 13.7214C13.7057 14.1641 13.5156 14.5859 13.2708 14.9896C13.3932 15.1198 13.4973 15.1875 13.5702 15.1927C13.8202 15.0443 14.276 14.1667 14.5208 13.3516C14.6406 12.9557 15.0624 12.7318 15.4557 12.8516C15.8515 12.9688 16.0755 13.388 15.9583 13.7839C15.7135 14.6042 15.0077 16.526 13.7812 16.6823C13.2239 16.75 12.7421 16.5443 12.3359 16.1745C12.065 16.4479 11.427 17.0625 10.5312 17.0625ZM10.013 13.224C9.26816 13.7057 9.43744 14.5521 9.69525 15.013C9.84369 15.2786 10.3072 15.5781 10.8098 15.4349C11.0755 15.3568 11.289 15.1927 11.4635 14.9141C11.2942 14.5547 10.9687 13.4401 10.9192 13.2031C10.8515 13.1224 10.4322 12.9505 10.013 13.224ZM12.4192 8.96875C12.3072 9.28125 11.9765 11.5234 12.4452 13.1536C12.8229 12.026 12.5937 9.4401 12.4192 8.96875Z" fill="currentColor"/>
|
||||||
|
<path d="M16.2995 19.5H7.70052C6.84635 19.5 6.04167 19.1667 5.4375 18.5625C4.83333 17.9583 4.5 17.1536 4.5 16.2969V7.70313C4.5 6.84635 4.83333 6.04167 5.4375 5.4375C6.04167 4.83333 6.84635 4.5 7.70052 4.5H16.2995C17.1536 4.5 17.9583 4.83333 18.5625 5.4375C19.1667 6.04167 19.5 6.84635 19.5 7.70313V16.2995C19.5 17.1536 19.1667 17.9583 18.5625 18.5625C17.9583 19.1667 17.1536 19.5 16.2995 19.5ZM7.70052 6C7.2474 6 6.82031 6.17708 6.4974 6.5C6.17708 6.82031 6 7.2474 6 7.70313V16.2995C6 16.7526 6.17708 17.1797 6.4974 17.5026C6.82031 17.8229 7.2474 18 7.70052 18H16.2995C16.7526 18 17.1797 17.8229 17.5026 17.5026C17.8229 17.1797 18 16.7526 18 16.2969V7.70313C18 7.2474 17.8229 6.82031 17.5026 6.5C17.1797 6.17708 16.7526 6 16.2995 6H7.70052Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
4
public/icons/user-link-dzen.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.9323 4.5C5.03906 4.5 4.5 5.03906 4.5 11.9323V12.0677C4.5 18.9609 5.03906 19.5 11.9323 19.5H12.0677C18.9609 19.5 19.5 18.9609 19.5 12.0677V12C19.5 5.04427 18.9557 4.5 12 4.5H11.9323ZM10.5 6H13.5C17.6745 6 18 6.32552 18 10.5V13.5C18 17.6745 17.6745 18 13.5 18H10.5C6.32552 18 6 17.6745 6 13.5V10.5C6 6.32552 6.32552 6 10.5 6Z" fill="currentColor"/>
|
||||||
|
<path d="M17 12.0536V11.9464C14.7857 11.875 13.775 11.8214 12.9643 11.0357C12.1786 10.225 12.1214 9.21429 12.0536 7H11.9464C11.875 9.21429 11.8214 10.225 11.0357 11.0357C10.225 11.8214 9.21429 11.8786 7 11.9464V12.0536C9.21429 12.125 10.225 12.1786 11.0357 12.9643C11.8214 13.775 11.8786 14.7857 11.9464 17H12.0536C12.125 14.7857 12.1786 13.775 12.9643 12.9643C13.775 12.1786 14.7857 12.1214 17 12.0536Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 890 B |
4
public/icons/user-link-facebook.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H14.4818V13.5156H16.2578L16.5365 11.5182H14.4818C14.4818 11.5182 14.4792 10.349 14.4818 10.0573C14.487 9.48698 14.9635 9.20052 15.3724 9.20313C15.7839 9.20833 16.6302 9.20573 16.6302 9.20573V7.36458C16.6302 7.36458 15.8958 7.27083 15.1276 7.26302C14.4792 7.25521 13.7656 7.42969 13.1875 8.01302C12.6016 8.60417 12.5078 9.48437 12.4974 10.5599C12.4948 10.8698 12.4974 11.5182 12.4974 11.5182H10.7604V13.513H12.4974V18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 913 B |
3
public/icons/user-link-github.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3.75C7.44271 3.75 3.75 7.44271 3.75 12C3.75 16.5573 7.44271 20.25 12 20.25C16.5573 20.25 20.25 16.5573 20.25 12C20.25 7.44271 16.5573 3.75 12 3.75ZM12 5.25C15.7292 5.25 18.75 8.27083 18.75 12C18.75 15.0885 16.6719 17.6875 13.8385 18.4896C13.8047 18.4193 13.7812 18.3385 13.7839 18.25C13.7943 17.7969 13.7839 16.7396 13.7839 16.3516C13.7839 15.6849 13.362 15.2109 13.362 15.2109C13.362 15.2109 16.6693 15.2474 16.6693 11.7161C16.6693 10.3542 15.9583 9.64583 15.9583 9.64583C15.9583 9.64583 16.3307 8.1901 15.8281 7.57292C15.263 7.51302 14.2526 8.11198 13.8203 8.39323C13.8203 8.39323 13.138 8.11198 12 8.11198C10.862 8.11198 10.1797 8.39323 10.1797 8.39323C9.7474 8.11198 8.73698 7.51302 8.17187 7.57292C7.66927 8.1901 8.04167 9.64583 8.04167 9.64583C8.04167 9.64583 7.33073 10.3542 7.33073 11.7161C7.33073 15.2474 10.638 15.2109 10.638 15.2109C10.638 15.2109 10.263 15.6406 10.224 16.2552C10.0026 16.3333 9.70313 16.4245 9.41406 16.4245C8.72135 16.4245 8.19271 15.75 8 15.4375C7.8099 15.1302 7.41927 14.8724 7.05469 14.8724C6.8151 14.8724 6.69792 14.9922 6.69792 15.1302C6.69792 15.2682 7.03385 15.362 7.25521 15.6172C7.72396 16.1536 7.71615 17.3594 9.38281 17.3594C9.58073 17.3594 9.9349 17.3151 10.2161 17.276C10.2161 17.6536 10.2109 18.0182 10.2161 18.25C10.2188 18.3385 10.1953 18.4193 10.1615 18.4896C7.32813 17.6875 5.25 15.0885 5.25 12C5.25 8.27083 8.27083 5.25 12 5.25Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
4
public/icons/user-link-instagram.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M11.9323 4.5C5.03906 4.5 4.5 5.03906 4.5 11.9323V12.0677C4.5 18.9609 5.03906 19.5 11.9323 19.5H12.0677C18.9609 19.5 19.5 18.9609 19.5 12.0677V12C19.5 5.04427 18.9557 4.5 12 4.5H11.9323ZM10.5 6H13.5C17.6745 6 18 6.32552 18 10.5V13.5C18 17.6745 17.6745 18 13.5 18H10.5C6.32552 18 6 17.6745 6 13.5V10.5C6 6.32552 6.32552 6 10.5 6ZM15.7474 7.5C15.3333 7.5 15 7.83854 15 8.2526C15 8.66667 15.3385 9 15.7526 9C16.1667 9 16.5 8.66146 16.5 8.2474C16.5 7.83333 16.1615 7.5 15.7474 7.5ZM11.9922 8.25C9.91927 8.25521 8.24479 9.9375 8.25 12.0078C8.25521 14.0807 9.9375 15.7552 12.0078 15.75C14.0807 15.7448 15.7552 14.0625 15.75 11.9922C15.7448 9.91927 14.0625 8.24479 11.9922 8.25ZM11.9948 9.75C13.237 9.7474 14.2474 10.7526 14.25 11.9948C14.2526 13.237 13.2474 14.2474 12.0052 14.25C10.763 14.2526 9.7526 13.2474 9.75 12.0052C9.7474 10.763 10.7526 9.7526 11.9948 9.75Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 996 B |
6
public/icons/user-link-linkedin.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0625 10.125C15.4089 10.125 16.5 11.2161 16.5 12.5625C16.5 12.5625 16.5 14.3594 16.5 15.2552C16.5 15.5286 16.2786 15.75 16.0052 15.75C15.8411 15.75 15.6589 15.75 15.4948 15.75C15.2214 15.75 15 15.5286 15 15.2552V12.9375C15 12.2161 14.4089 11.625 13.6875 11.625C12.9661 11.625 12.375 12.2161 12.375 12.9375C12.375 12.9375 12.375 14.4479 12.375 15.2552C12.375 15.5286 12.1536 15.75 11.8802 15.75C11.7161 15.75 11.5339 15.75 11.3698 15.75C11.0964 15.75 10.875 15.5286 10.875 15.2552C10.875 14.1875 10.875 11.6875 10.875 10.6198C10.875 10.3464 11.0964 10.125 11.3698 10.125C11.5339 10.125 11.7161 10.125 11.8802 10.125C12.1536 10.125 12.375 10.3464 12.375 10.6198V10.8073C12.8125 10.3854 13.4062 10.125 14.0625 10.125Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.375 10.6276C9.375 11.6953 9.375 14.1797 9.375 15.2474C9.375 15.526 9.15104 15.75 8.8724 15.75C8.71354 15.75 8.53646 15.75 8.3776 15.75C8.09896 15.75 7.875 15.526 7.875 15.2474C7.875 14.1797 7.875 11.6953 7.875 10.6276C7.875 10.349 8.09896 10.125 8.3776 10.125C8.53646 10.125 8.71354 10.125 8.8724 10.125C9.15104 10.125 9.375 10.349 9.375 10.6276Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.625 7.5C9.03906 7.5 9.375 7.83594 9.375 8.25C9.375 8.66406 9.03906 9 8.625 9C8.21094 9 7.875 8.66406 7.875 8.25C7.875 7.83594 8.21094 7.5 8.625 7.5Z" fill="currentColor"/>
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
3
public/icons/user-link-medium.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6ZM9.85677 9.22135C8.32292 9.22135 7.07813 10.4661 7.07813 12C7.07813 13.5339 8.32292 14.7786 9.85677 14.7786C11.3906 14.7786 12.6354 13.5339 12.6354 12C12.6354 10.4661 11.3906 9.22135 9.85677 9.22135ZM14.2891 9.35417C13.5286 9.35417 12.9141 10.5391 12.9141 12C12.9141 13.4609 13.5286 14.6458 14.2891 14.6458C15.0495 14.6458 15.6641 13.4609 15.6641 12C15.6641 10.5391 15.0495 9.35417 14.2891 9.35417ZM16.4245 9.63802C16.1615 9.63802 15.9479 10.6953 15.9479 12C15.9479 13.3047 16.1615 14.362 16.4245 14.362C16.6901 14.362 16.9036 13.3047 16.9036 12C16.9036 10.6953 16.6901 9.63802 16.4245 9.63802Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
5
public/icons/user-link-ok.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.2995 19.5H7.70052C6.84635 19.5 6.04167 19.1667 5.4375 18.5625C4.83333 17.9583 4.5 17.1536 4.5 16.2969V7.70313C4.5 6.84635 4.83333 6.04167 5.4375 5.4375C6.04167 4.83333 6.84635 4.5 7.70052 4.5H16.2995C17.1536 4.5 17.9583 4.83333 18.5625 5.4375C19.1667 6.04167 19.5 6.84635 19.5 7.70313V16.2995C19.5 17.1536 19.1667 17.9583 18.5625 18.5625C17.9583 19.1667 17.1536 19.5 16.2995 19.5ZM7.70052 6C7.2474 6 6.82031 6.17708 6.4974 6.5C6.17708 6.82031 6 7.2474 6 7.70313V16.2995C6 16.7526 6.17708 17.1797 6.4974 17.5026C6.82031 17.8229 7.2474 18 7.70052 18H16.2995C16.7526 18 17.1797 17.8229 17.5026 17.5026C17.8229 17.1797 18 16.7526 18 16.2969V7.70313C18 7.2474 17.8229 6.82031 17.5026 6.5C17.1797 6.17708 16.7526 6 16.2995 6H7.70052Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7.125C13.4505 7.125 14.625 8.29948 14.625 9.75C14.625 11.2005 13.4505 12.375 12 12.375C10.5495 12.375 9.375 11.2005 9.375 9.75C9.375 8.29948 10.5495 7.125 12 7.125ZM12 10.8984C12.6198 10.8984 13.125 10.3932 13.125 9.77344C13.125 9.15104 12.6198 8.64844 12 8.64844C11.3802 8.64844 10.875 9.15104 10.875 9.77344C10.875 10.3932 11.3802 10.8984 12 10.8984Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2734 14.5886C13.7968 14.4401 14.2916 14.2031 14.7447 13.8802C15.0155 13.6823 15.0806 13.2969 14.8931 13.0156C14.703 12.7344 14.328 12.6667 14.0572 12.862C13.453 13.2995 12.7421 13.5287 11.9999 13.5287C11.2603 13.5287 10.552 13.2995 9.94784 12.8646C9.6744 12.6693 9.302 12.7396 9.1119 13.0209C8.92179 13.3021 8.9895 13.6875 9.26294 13.8828C9.71086 14.2058 10.2056 14.4427 10.7265 14.5886L9.177 16.1927C8.94263 16.4349 8.94263 16.8255 9.177 17.0677C9.29159 17.1901 9.44523 17.25 9.59888 17.25C9.75252 17.25 9.90617 17.1901 10.0234 17.0677L11.9999 15.0261L13.9765 17.0677C14.0937 17.1901 14.2473 17.25 14.401 17.25C14.5546 17.25 14.7056 17.1901 14.8228 17.0677C15.0572 16.8255 15.0572 16.4323 14.8228 16.1901L13.2734 14.5886Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
3
public/icons/user-link-pinterest.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3.75C7.44271 3.75 3.75 7.44271 3.75 12C3.75 16.5573 7.44271 20.25 12 20.25C16.5573 20.25 20.25 16.5573 20.25 12C20.25 7.44271 16.5573 3.75 12 3.75ZM12 5.25C15.7292 5.25 18.75 8.27083 18.75 12C18.75 15.7292 15.7292 18.75 12 18.75C11.3828 18.75 10.7891 18.6615 10.2214 18.5052C10.3984 18.138 10.5755 17.7344 10.6536 17.4323C10.75 17.0651 11.1432 15.5703 11.1432 15.5703C11.3984 16.0573 12.1432 16.4688 12.9375 16.4688C15.3021 16.4688 17.0052 14.2969 17.0052 11.5964C17.0052 9.00781 14.8906 7.07031 12.1745 7.07031C8.79167 7.07031 6.9974 9.33854 6.9974 11.8099C6.9974 12.9609 7.60938 14.3906 8.58594 14.8464C8.73438 14.9167 8.8151 14.8854 8.84896 14.7422C8.875 14.6328 9.00781 14.1016 9.06771 13.8542C9.08594 13.7734 9.07552 13.7057 9.01302 13.6302C8.6901 13.237 8.42969 12.5156 8.42969 11.8411C8.42969 10.112 9.73698 8.4401 11.9661 8.4401C13.8906 8.4401 15.2396 9.7526 15.2396 11.6276C15.2396 13.7474 14.1693 15.2161 12.776 15.2161C12.0078 15.2161 11.4323 14.5807 11.6172 13.7995C11.8359 12.8698 12.2656 11.8646 12.2656 11.1927C12.2656 10.5911 11.9427 10.0885 11.2734 10.0885C10.487 10.0885 9.85677 10.901 9.85677 11.9922C9.85677 12.6849 10.0911 13.1536 10.0911 13.1536C10.0911 13.1536 9.3151 16.4375 9.17187 17.0495C9.10937 17.3203 9.09115 17.7083 9.08854 18.0833C6.82031 16.9948 5.25 14.6849 5.25 12C5.25 8.27083 8.27083 5.25 12 5.25Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
4
public/icons/user-link-reddit.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12.573 7.04428C12.4766 7.06251 12.3933 7.13803 12.3699 7.2422V7.2474L11.7605 10.1094C10.6121 10.1276 9.49487 10.4792 8.54175 11.125C8.05737 10.6693 7.29435 10.6927 6.84123 11.1771C6.3855 11.6615 6.40893 12.4245 6.89331 12.8802C6.98706 12.9688 7.09643 13.0443 7.21623 13.0964C7.20841 13.2188 7.20841 13.3385 7.21623 13.4609C7.21623 15.3073 9.36987 16.8125 12.0261 16.8125C14.6824 16.8125 16.836 15.3099 16.836 13.4609C16.8438 13.3385 16.8438 13.2188 16.836 13.0964C17.2501 12.8906 17.5105 12.4635 17.5027 12C17.4766 11.3359 16.9194 10.8177 16.2527 10.8386C15.9584 10.849 15.6772 10.9714 15.4636 11.1745C14.5235 10.5339 13.422 10.1823 12.2865 10.1589L12.823 7.58595L14.5886 7.95574C14.6381 8.40886 15.0444 8.73699 15.4975 8.68751C15.9506 8.63803 16.2787 8.23178 16.2292 7.77865C16.1798 7.32553 15.7735 6.9974 15.3204 7.04688C15.06 7.07292 14.8256 7.22397 14.698 7.45053L12.6746 7.04688C12.6407 7.03907 12.6069 7.03907 12.573 7.04428ZM10.0782 12C10.5339 12 10.9063 12.3698 10.9063 12.8255C10.9063 13.2813 10.5339 13.6537 10.0782 13.6537C9.62248 13.6484 9.25268 13.2813 9.25268 12.8255C9.25268 12.3698 9.62248 12 10.0782 12ZM13.8959 12.0287C14.3516 12.0287 14.724 12.3984 14.724 12.8542C14.7423 13.3099 14.3855 13.6953 13.9324 13.7136H13.8907L13.8959 13.6823C13.4428 13.6823 13.0704 13.3099 13.0704 12.8542C13.0704 12.3984 13.4428 12.0287 13.8959 12.0287ZM10.1407 14.7292C10.1902 14.7292 10.2397 14.7474 10.2813 14.7813C10.7787 15.1432 11.3829 15.3281 11.9975 15.2995C12.6121 15.3333 13.2214 15.1563 13.724 14.7969C13.8126 14.7083 13.961 14.7109 14.0496 14.8021C14.1355 14.8906 14.1355 15.0391 14.0444 15.1276V15.0938C13.4584 15.5365 12.7397 15.7604 12.0079 15.7292C11.2735 15.7604 10.5548 15.5365 9.96883 15.0938C9.89071 15 9.90373 14.8594 9.99748 14.7813C10.0391 14.7474 10.0886 14.7292 10.1407 14.7292Z" fill="currentColor"/>
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
5
public/icons/user-link-telegram.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6Z" fill="currentColor"/>
|
||||||
|
<path
|
||||||
|
d="M6.67663 11.836L8.98083 12.6262L9.8727 15.2616C9.92977 15.4304 10.1544 15.4928 10.3035 15.3808L11.5879 14.4187C11.7225 14.3179 11.9143 14.3129 12.055 14.4067L14.3716 15.9521C14.5311 16.0586 14.7571 15.9783 14.7971 15.8012L16.4941 8.30075C16.5378 8.1073 16.3309 7.94593 16.1304 8.01717L6.67393 11.3691C6.44056 11.4518 6.4426 11.7554 6.67663 11.836ZM9.72897 12.2055L14.2322 9.65707C14.3132 9.61141 14.3964 9.71196 14.3269 9.77119L10.6104 12.9455C10.4798 13.0572 10.3955 13.2067 10.3717 13.369L10.2451 14.2311C10.2283 14.3462 10.0523 14.3576 10.0178 14.2462L9.53087 12.6742C9.4751 12.4949 9.55637 12.3034 9.72897 12.2055Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
3
public/icons/user-link-tiktok.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6ZM12.2786 7.38542V13.6094C12.2786 14.4661 11.5339 14.9089 11 14.9089C10.5964 14.9089 9.68229 14.5938 9.68229 13.6016C9.68229 12.5573 10.5365 12.2917 11.0078 12.2917C11.2839 12.2917 11.3932 12.3516 11.3932 12.3516V10.6901C11.3932 10.6901 11.1875 10.6589 10.9505 10.6589C9.25521 10.6589 8.04948 12.0312 8.04948 13.6016C8.04948 14.9271 9.08594 16.5312 10.9792 16.5312C12.9974 16.5312 13.9193 14.849 13.9193 13.6094V10.5208C13.9193 10.5208 14.8203 11.151 16.0234 11.151V9.57813C14.6432 9.47917 13.9036 8.54948 13.8516 7.38542H12.2786Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
3
public/icons/user-link-twitter.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6ZM14.2266 8.55208C13.1615 8.55208 12.2969 9.41667 12.2969 10.4818C12.2969 10.6328 12.3464 10.9193 12.3464 10.9193C12.3464 10.9193 10.0208 10.9349 8.3724 8.90625C8.3724 8.90625 8.11979 9.24479 8.11198 9.875C8.09896 10.9453 8.96875 11.4792 8.96875 11.4792C8.96875 11.4792 8.53125 11.4922 8.09635 11.2396C8.13542 12.8958 9.64323 13.1536 9.64323 13.1536C9.64323 13.1536 9.15625 13.2474 8.77344 13.1875C9.01823 13.9531 9.72917 14.5104 10.5729 14.526C9.91406 15.0443 8.94271 15.4635 7.71875 15.3255C8.16667 15.8099 9.58594 16.1901 10.6745 16.1901C14.2214 16.1901 16.1615 13.2526 16.1615 10.7057C16.1615 10.6224 16.1589 10.5391 16.1536 10.4557C16.5312 10.1823 16.8594 9.84375 17.1172 9.45573C16.5911 9.69531 16.0104 9.76042 16.0104 9.76042C16.0104 9.76042 16.6901 9.36719 16.8568 8.69271C16.4844 8.91406 16.0729 9.07552 15.6328 9.16146C15.2812 8.78646 14.7812 8.55208 14.2266 8.55208Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
public/icons/user-link-vk.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.91406 4.5C4.82031 4.5 4.5 4.82031 4.5 8.91406V15.0859C4.5 19.1797 4.82031 19.5 8.91406 19.5H15.0859C19.1797 19.5 19.5 19.1797 19.5 15.0859V8.91406C19.5 4.82031 19.1797 4.5 15.0859 4.5H8.91406ZM7.9375 6H16.0625C17.8594 6 18 6.14063 18 7.9375V16.0625C18 17.8594 17.8594 18 16.0625 18H7.9375C6.14063 18 6 17.8594 6 16.0625V7.9375C6 6.14063 6.14063 6 7.9375 6ZM11.5807 9.07813C10.0208 9.07813 9.95052 9.44271 10.1354 9.53125C10.3646 9.64323 10.6797 9.77604 10.6797 10.4297C10.6797 11.3854 10.6849 11.9505 10.4089 11.9505C10.1354 11.9505 9.98958 11.8203 9.39844 10.7448C8.70313 9.47917 8.70833 9.33333 8.21615 9.33333H6.9349C6.67969 9.33333 6.60677 9.54167 6.66927 9.69271C6.75 9.88802 7.90365 12.4974 9.11979 13.7266C10.3359 14.9583 11.5755 14.9219 11.7578 14.9219H12.0964C12.4792 14.9219 12.6146 14.7031 12.6458 14.5286C12.7578 13.9323 12.7917 13.5833 13.026 13.5833C13.25 13.5833 13.388 13.7214 14.3906 14.7318C14.5156 14.8568 14.6224 14.9219 14.9505 14.9219H16.4453C16.7161 14.9219 17.1615 14.8776 16.8229 14.3099C16.7943 14.263 16.625 13.8932 15.8021 13.1276C15.1016 12.4766 15.138 12.4036 16.0625 11.0495C16.8021 9.96354 16.9115 9.76563 16.8568 9.58594C16.8047 9.41927 16.7422 9.35417 16.3568 9.35417C16.026 9.35417 15.1927 9.33333 14.9245 9.33333C14.5182 9.33333 14.4609 9.63021 13.888 10.7917C13.4271 11.7292 13.1901 12.0104 12.987 12.0104C12.7578 12.0104 12.6354 11.7708 12.6354 10.8776C12.6354 9.60938 12.6979 9.07813 11.5807 9.07813Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
public/icons/user-link-youtube.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 5.625C5.60156 5.625 4.7474 5.69271 3.92708 6.53646C3.10677 7.38021 3 8.71875 3 12C3 15.2812 3.10677 16.6224 3.92708 17.4635C4.7474 18.3073 5.60156 18.375 12 18.375C18.3984 18.375 19.2526 18.3073 20.0729 17.4635C20.8932 16.6198 21 15.2812 21 12C21 8.71875 20.8932 7.38021 20.0729 6.53646C19.2526 5.69271 18.3984 5.625 12 5.625ZM12 7.125C17.2396 7.125 18.5182 7.14844 18.9505 7.58594C19.3854 8.02604 19.5 9.20052 19.5 12C19.5 14.7995 19.3854 15.974 18.9505 16.4141C18.5182 16.8516 17.2396 16.875 12 16.875C6.76042 16.875 5.48177 16.8516 5.04948 16.4141C4.61458 15.974 4.51042 14.7995 4.51042 12C4.51042 9.20052 4.61458 8.02604 5.04948 7.58594C5.48177 7.14844 6.76042 7.125 12 7.125ZM10.4818 9.38281V14.6172L15.0365 11.9792L10.4818 9.38281Z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 877 B |
|
@ -3,14 +3,18 @@
|
||||||
"About myself": "About myself",
|
"About myself": "About myself",
|
||||||
"About the project": "About the project",
|
"About the project": "About the project",
|
||||||
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title",
|
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title",
|
||||||
|
"Add a link or click plus to embed media": "Add a link or click plus to embed media",
|
||||||
|
"Add an embed widget": "Add an embed widget",
|
||||||
"Add another image": "Add another image",
|
"Add another image": "Add another image",
|
||||||
"Add audio": "Add audio",
|
"Add audio": "Add audio",
|
||||||
|
"Add blockquote": "Add blockquote",
|
||||||
"Add comment": "Comment",
|
"Add comment": "Comment",
|
||||||
"Add cover": "Add cover",
|
"Add cover": "Add cover",
|
||||||
"Add image": "Add image",
|
"Add image": "Add image",
|
||||||
"Add images": "Add images",
|
"Add images": "Add images",
|
||||||
"Add intro": "Add intro",
|
"Add intro": "Add intro",
|
||||||
"Add link": "Add link",
|
"Add link": "Add link",
|
||||||
|
"Add rule": "Add rule",
|
||||||
"Add signature": "Add signature",
|
"Add signature": "Add signature",
|
||||||
"Add subtitle": "Add subtitle",
|
"Add subtitle": "Add subtitle",
|
||||||
"Add url": "Add url",
|
"Add url": "Add url",
|
||||||
|
@ -61,6 +65,7 @@
|
||||||
"Collaborate": "Help Edit",
|
"Collaborate": "Help Edit",
|
||||||
"Come up with a subtitle for your story": "Come up with a subtitle for your story",
|
"Come up with a subtitle for your story": "Come up with a subtitle for your story",
|
||||||
"Come up with a title for your story": "Come up with a title for your story",
|
"Come up with a title for your story": "Come up with a title for your story",
|
||||||
|
"Comment successfully deleted": "Comment successfully deleted",
|
||||||
"Comments": "Comments",
|
"Comments": "Comments",
|
||||||
"Communities": "Communities",
|
"Communities": "Communities",
|
||||||
"Confirm": "Confirm",
|
"Confirm": "Confirm",
|
||||||
|
@ -86,7 +91,7 @@
|
||||||
"Delete cover": "Delete cover",
|
"Delete cover": "Delete cover",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Discours": "Discours",
|
"Discours": "Discours",
|
||||||
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects",
|
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects.<br/><em>We are convinced that one voice is good, but many is better. We create the most amazing stories together</em>",
|
||||||
"Discours is created with our common effort": "Discours exists because of our common effort",
|
"Discours is created with our common effort": "Discours exists because of our common effort",
|
||||||
"Discussing": "Discussing",
|
"Discussing": "Discussing",
|
||||||
"Discussion rules": "Discussion rules",
|
"Discussion rules": "Discussion rules",
|
||||||
|
@ -100,6 +105,7 @@
|
||||||
"Email": "Mail",
|
"Email": "Mail",
|
||||||
"Enter": "Enter",
|
"Enter": "Enter",
|
||||||
"Enter URL address": "Enter URL address",
|
"Enter URL address": "Enter URL address",
|
||||||
|
"Enter footnote text": "Enter footnote text",
|
||||||
"Enter image description": "Enter image description",
|
"Enter image description": "Enter image description",
|
||||||
"Enter image title": "Enter image title",
|
"Enter image title": "Enter image title",
|
||||||
"Enter text": "Enter text",
|
"Enter text": "Enter text",
|
||||||
|
@ -139,6 +145,7 @@
|
||||||
"Help": "Помощь",
|
"Help": "Помощь",
|
||||||
"Help to edit": "Help to edit",
|
"Help to edit": "Help to edit",
|
||||||
"Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.",
|
"Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.",
|
||||||
|
"Hide table of contents": "Hide table of contents",
|
||||||
"Highlight": "Highlight",
|
"Highlight": "Highlight",
|
||||||
"Hooray! Welcome!": "Hooray! Welcome!",
|
"Hooray! Welcome!": "Hooray! Welcome!",
|
||||||
"Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform",
|
"Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform",
|
||||||
|
@ -258,9 +265,9 @@
|
||||||
"Send link again": "Send link again",
|
"Send link again": "Send link again",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Share": "Share",
|
"Share": "Share",
|
||||||
"Short opening": "Short opening",
|
|
||||||
"Show": "Show",
|
"Show": "Show",
|
||||||
"Show lyrics": "Текст песни",
|
"Show lyrics": "Show lyrics",
|
||||||
|
"Show table of contents": "Show table of contents",
|
||||||
"Slug": "Slug",
|
"Slug": "Slug",
|
||||||
"Social networks": "Social networks",
|
"Social networks": "Social networks",
|
||||||
"Something went wrong, check email and password": "Something went wrong. Check your email and password",
|
"Something went wrong, check email and password": "Something went wrong. Check your email and password",
|
||||||
|
@ -318,9 +325,7 @@
|
||||||
"Video": "Video",
|
"Video": "Video",
|
||||||
"Video format not supported": "Video format not supported",
|
"Video format not supported": "Video format not supported",
|
||||||
"Views": "Views",
|
"Views": "Views",
|
||||||
"We are convinced that one voice is good, but many is better": "We are convinced that one voice is good, but many is better",
|
|
||||||
"We can't find you, check email or": "We can't find you, check email or",
|
"We can't find you, check email or": "We can't find you, check email or",
|
||||||
"We create the most amazing stories together": "We create the most amazing stories together",
|
|
||||||
"We know you, please try to login": "This email address is already registered, please try to login",
|
"We know you, please try to login": "This email address is already registered, please try to login",
|
||||||
"We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.",
|
"We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.",
|
||||||
"Where": "From",
|
"Where": "From",
|
||||||
|
@ -388,5 +393,12 @@
|
||||||
"user already exist": "user already exists",
|
"user already exist": "user already exists",
|
||||||
"video": "video",
|
"video": "video",
|
||||||
"view": "view",
|
"view": "view",
|
||||||
"zine": "zine"
|
"zine": "zine",
|
||||||
|
"subscriber": "subscriber",
|
||||||
|
"subscriber_rp": "subscriber",
|
||||||
|
"subscribers": "subscribers",
|
||||||
|
"subscription": "subscription",
|
||||||
|
"subscription_rp": "subscription",
|
||||||
|
"subscriptions": "subscriptions",
|
||||||
|
"Users": "Users"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
{
|
{
|
||||||
"...subscribing": "...подписываем",
|
"...subscribing": "...подписываем",
|
||||||
"A short introduction to keep the reader interested": "Небольшое вступление, чтобы заинтересовать читателя",
|
"A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя",
|
||||||
"About myself": "О себе",
|
"About myself": "О себе",
|
||||||
"About the project": "О проекте",
|
"About the project": "О проекте",
|
||||||
"Accomplices": "Соучастники",
|
"Accomplices": "Соучастники",
|
||||||
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной",
|
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной",
|
||||||
|
"Add a link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа",
|
||||||
|
"Add an embed widget": "Добавить embed-виджет",
|
||||||
"Add another image": "Добавить другое изображение",
|
"Add another image": "Добавить другое изображение",
|
||||||
"Add audio": "Добавить аудио",
|
"Add audio": "Добавить аудио",
|
||||||
|
"Add blockquote": "Добавить цитату",
|
||||||
"Add comment": "Комментировать",
|
"Add comment": "Комментировать",
|
||||||
"Add cover": "Добавить обложку",
|
"Add cover": "Добавить обложку",
|
||||||
"Add image": "Добавить изображение",
|
"Add image": "Добавить изображение",
|
||||||
"Add images": "Добавить изображения",
|
"Add images": "Добавить изображения",
|
||||||
"Add intro": "Добавить вступление",
|
"Add intro": "Добавить вступление",
|
||||||
"Add link": "Добавить ссылку",
|
"Add link": "Добавить ссылку",
|
||||||
|
"Add rule": "Добавить разделитель",
|
||||||
"Add signature": "Добавить подпись",
|
"Add signature": "Добавить подпись",
|
||||||
"Add subtitle": "Добавить подзаголовок",
|
"Add subtitle": "Добавить подзаголовок",
|
||||||
"Add to bookmarks": "Добавить в закладки",
|
"Add to bookmarks": "Добавить в закладки",
|
||||||
|
@ -65,6 +69,7 @@
|
||||||
"Collaborate": "Помочь редактировать",
|
"Collaborate": "Помочь редактировать",
|
||||||
"Come up with a subtitle for your story": "Придумайте подзаголовок вашей истории",
|
"Come up with a subtitle for your story": "Придумайте подзаголовок вашей истории",
|
||||||
"Come up with a title for your story": "Придумайте заголовок вашей истории",
|
"Come up with a title for your story": "Придумайте заголовок вашей истории",
|
||||||
|
"Comment successfully deleted": "Комментарий успешно удален",
|
||||||
"Comments": "Комментарии",
|
"Comments": "Комментарии",
|
||||||
"Communities": "Сообщества",
|
"Communities": "Сообщества",
|
||||||
"Confirm": "Подтвердить",
|
"Confirm": "Подтвердить",
|
||||||
|
@ -90,7 +95,7 @@
|
||||||
"Delete cover": "Удалить обложку",
|
"Delete cover": "Удалить обложку",
|
||||||
"Description": "Описание",
|
"Description": "Описание",
|
||||||
"Discours": "Дискурс",
|
"Discours": "Дискурс",
|
||||||
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов",
|
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов.<br/>Мы убеждены, один голос хорошо, а много — лучше. Самые потрясающиe истории мы создаём вместе.",
|
||||||
"Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
|
"Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
|
||||||
"Discussing": "Обсуждаемое",
|
"Discussing": "Обсуждаемое",
|
||||||
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
||||||
|
@ -147,9 +152,10 @@
|
||||||
"Help": "Помощь",
|
"Help": "Помощь",
|
||||||
"Help to edit": "Помочь редактировать",
|
"Help to edit": "Помочь редактировать",
|
||||||
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
||||||
|
"Hide table of contents": "Скрыть главление",
|
||||||
"Highlight": "Подсветка",
|
"Highlight": "Подсветка",
|
||||||
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||||
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
|
"Horizontal collaborative journalistic platform": "Открытая платформа<br/>для независимой журналистики",
|
||||||
"Hot topics": "Горячие темы",
|
"Hot topics": "Горячие темы",
|
||||||
"Hotkeys": "Горячие клавиши",
|
"Hotkeys": "Горячие клавиши",
|
||||||
"How can I help/skills": "Чем могу помочь/навыки",
|
"How can I help/skills": "Чем могу помочь/навыки",
|
||||||
|
@ -222,7 +228,7 @@
|
||||||
"Password again": "Пароль ещё раз",
|
"Password again": "Пароль ещё раз",
|
||||||
"Password should be at least 8 characters": "Пароль должен быть не менее 8 символов",
|
"Password should be at least 8 characters": "Пароль должен быть не менее 8 символов",
|
||||||
"Password should contain at least one number": "Пароль должен содержать хотя бы одну цифру",
|
"Password should contain at least one number": "Пароль должен содержать хотя бы одну цифру",
|
||||||
"Password should contain at least one special character: !@#$%^&*": "Пароль должен содержать хотя бы один специальный символ: !@#$%^&*",
|
"Password should contain at least one special character: !@#$%^&*": "Пароль должен содержать хотя бы один спецсимвол: !@#$%^&*",
|
||||||
"Passwords are not equal": "Пароли не совпадают",
|
"Passwords are not equal": "Пароли не совпадают",
|
||||||
"Paste Embed code": "Вставьте embed код",
|
"Paste Embed code": "Вставьте embed код",
|
||||||
"Personal": "Личные",
|
"Personal": "Личные",
|
||||||
|
@ -274,9 +280,10 @@
|
||||||
"Send link again": "Прислать ссылку ещё раз",
|
"Send link again": "Прислать ссылку ещё раз",
|
||||||
"Settings": "Настройки",
|
"Settings": "Настройки",
|
||||||
"Share": "Поделиться",
|
"Share": "Поделиться",
|
||||||
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
|
"Short opening": "Расскажите вашу историю...",
|
||||||
"Show": "Показать",
|
"Show": "Показать",
|
||||||
"Show lyrics": "Текст песни",
|
"Show lyrics": "Текст песни",
|
||||||
|
"Show table of contents": "Показать главление",
|
||||||
"Slug": "Постоянная ссылка",
|
"Slug": "Постоянная ссылка",
|
||||||
"Social networks": "Социальные сети",
|
"Social networks": "Социальные сети",
|
||||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||||
|
@ -293,6 +300,7 @@
|
||||||
"Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
"Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||||
"Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
"Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||||
"Subscription": "Подписка",
|
"Subscription": "Подписка",
|
||||||
|
"subscription": "подписка",
|
||||||
"Subscriptions": "Подписки",
|
"Subscriptions": "Подписки",
|
||||||
"Substrate": "Подложка",
|
"Substrate": "Подложка",
|
||||||
"Success": "Успешно",
|
"Success": "Успешно",
|
||||||
|
@ -315,7 +323,7 @@
|
||||||
"Top authors": "Рейтинг авторов",
|
"Top authors": "Рейтинг авторов",
|
||||||
"Top commented": "Самое комментируемое",
|
"Top commented": "Самое комментируемое",
|
||||||
"Top discussed": "Обсуждаемое",
|
"Top discussed": "Обсуждаемое",
|
||||||
"Top month articles": "Лучшее за месяц",
|
"Top month articles": "Лучшие материалы месяца",
|
||||||
"Top rated": "Популярное",
|
"Top rated": "Популярное",
|
||||||
"Top recent": "Самое новое",
|
"Top recent": "Самое новое",
|
||||||
"Top topics": "Интересные темы",
|
"Top topics": "Интересные темы",
|
||||||
|
@ -335,9 +343,7 @@
|
||||||
"Video": "Видео",
|
"Video": "Видео",
|
||||||
"Video format not supported": "Тип видео не поддерживается",
|
"Video format not supported": "Тип видео не поддерживается",
|
||||||
"Views": "Просмотры",
|
"Views": "Просмотры",
|
||||||
"We are convinced that one voice is good, but many is better": "Мы убеждены, один голос хорошо, а много — лучше",
|
|
||||||
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
|
"We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или",
|
||||||
"We create the most amazing stories together": "Самые потрясающиe истории мы создаём вместе",
|
|
||||||
"We know you, please try to login": "Такой адрес почты уже зарегистрирован, попробуйте залогиниться",
|
"We know you, please try to login": "Такой адрес почты уже зарегистрирован, попробуйте залогиниться",
|
||||||
"We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.",
|
"We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.",
|
||||||
"Welcome!": "Добро пожаловать!",
|
"Welcome!": "Добро пожаловать!",
|
||||||
|
@ -382,7 +388,6 @@
|
||||||
"email not confirmed": "email не подтвержден",
|
"email not confirmed": "email не подтвержден",
|
||||||
"enter": "войдите",
|
"enter": "войдите",
|
||||||
"feed": "лента",
|
"feed": "лента",
|
||||||
"follower": "подписчик",
|
|
||||||
"general feed": "Общая лента",
|
"general feed": "Общая лента",
|
||||||
"header 1": "заголовок 1",
|
"header 1": "заголовок 1",
|
||||||
"header 2": "заголовок 2",
|
"header 2": "заголовок 2",
|
||||||
|
@ -413,5 +418,13 @@
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
"video": "видео",
|
"video": "видео",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал"
|
"zine": "журнал",
|
||||||
|
"Enter footnote text": "Введите текст сноски",
|
||||||
|
"follower": "подписчик",
|
||||||
|
"subscriber": "подписчик",
|
||||||
|
"subscriber_rp": "подписчика",
|
||||||
|
"subscribers": "подписчиков",
|
||||||
|
"subscription_rp": "подписки",
|
||||||
|
"subscriptions": "подписок",
|
||||||
|
"Users": "Пользователи"
|
||||||
}
|
}
|
||||||
|
|
|
@ -540,13 +540,13 @@ a[data-toggle='tooltip'] {
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 10px;
|
width: 14px;
|
||||||
height: 10px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin: -2px 0 0 1px;
|
margin: -2px 0 0 1px;
|
||||||
border: unset;
|
border: unset;
|
||||||
background-size: 10px;
|
background-size: contain;
|
||||||
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImJpIGJpLWluZm8tY2lyY2xlIiBmaWxsPSJjdXJyZW50Q29sb3IiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04IDE1QTcgNyAwIDEgMSA4IDFhNyA3IDAgMCAxIDAgMTR6bTAgMUE4IDggMCAxIDAgOCAwYTggOCAwIDAgMCAwIDE2eiIvPjxwYXRoIGQ9Im04LjkzIDYuNTg4LTIuMjkuMjg3LS4wODIuMzguNDUuMDgzYy4yOTQuMDcuMzUyLjE3Ni4yODguNDY5bC0uNzM4IDMuNDY4Yy0uMTk0Ljg5Ny4xMDUgMS4zMTkuODA4IDEuMzE5LjU0NSAwIDEuMTc4LS4yNTIgMS40NjUtLjU5OGwuMDg4LS40MTZjLS4yLjE3Ni0uNDkyLjI0Ni0uNjg2LjI0Ni0uMjc1IDAtLjM3NS0uMTkzLS4zMDQtLjUzM0w4LjkzIDYuNTg4ek05IDQuNWExIDEgMCAxIDEtMiAwIDEgMSAwIDAgMSAyIDB6Ii8+PC9zdmc+');
|
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTcgMTRDMTAuODY2IDE0IDE0IDEwLjg2NiAxNCA3QzE0IDMuMTM0MDEgMTAuODY2IDAgNyAwQzMuMTM0MDEgMCAwIDMuMTM0MDEgMCA3QzAgMTAuODY2IDMuMTM0MDEgMTQgNyAxNFpNNy44ODQzNSAzLjQyMzA3QzcuODg0MzUgMi45MTQyNCA3LjQ3MDExIDIuNSA2Ljk2MTI4IDIuNUM2LjQ1MjM2IDIuNSA2LjAyNjMyIDIuOTE0MjQgNi4wMzgyIDMuNDIzMDdDNi4wMzgyIDMuOTMxOTEgNi40NTI0NSA0LjM0NjE1IDYuOTYxMjggNC4zNDYxNUM3LjQ3MDExIDQuMzQ2MTUgNy44ODQzNSAzLjkzMTkxIDcuODg0MzUgMy40MjMwN1pNOC40NDA1NiAxMC41QzguNTU4OTUgMTAuNSA4LjY2NTQ0IDEwLjM5MzUgOC42NjU0NCAxMC4yNzUxTDguNjY1NDYgOS4xMDM1NUM4LjY2NTQ2IDguOTczMzYgOC41NTg5NiA4Ljg3ODY4IDguNDQwNTggOC44Nzg2OEg3Ljk0MzU0VjUuMjIxODhDNy45NDM1NCA1LjEwMzUgNy44MzcwNSA0Ljk5NzAxIDcuNzE4NjcgNC45OTcwMUg1LjU1M0M1LjQzNDYxIDQuOTk3MDEgNS4zMjgxMiA1LjA5MTcgNS4zMjgxMiA1LjIyMTg4VjYuMzkzNUM1LjMyODEyIDYuNTIzNjkgNS40MzQ2MiA2LjYxODM3IDUuNTUzIDYuNjE4MzdINi4wNTAwNFY4Ljg3ODcyTDUuNTUzIDguODc4NjRDNS40MzQ2MSA4Ljg3ODY0IDUuMzI4MTIgOC45NzMzMyA1LjMyODEyIDkuMTAzNTFWMTAuMjc1MUM1LjMyODEyIDEwLjM5MzUgNS40MzQ2MiAxMC41IDUuNTUzIDEwLjVIOC40NDA1NloiIGZpbGw9IiM0MDQwNDAiLz48L3N2Zz4=');
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
|
@ -554,9 +554,50 @@ a[data-toggle='tooltip'] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: #141414;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
|
||||||
color: white;
|
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--black-500);
|
||||||
|
|
||||||
|
.tooltipContent {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
color: var(--default-color-invert);
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -4px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 4px 4px 0 4px;
|
||||||
|
border-color: var(--black-500) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
@include font-size(1.8rem);
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@ export const AudioPlayer = (props: Props) => {
|
||||||
() => currentTrackIndex(),
|
() => currentTrackIndex(),
|
||||||
() => {
|
() => {
|
||||||
setCurrentTrackDuration(0)
|
setCurrentTrackDuration(0)
|
||||||
}
|
},
|
||||||
|
{ defer: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import { createEffect, For, createMemo, onMount, Show, createSignal } from 'solid-js'
|
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
|
|
||||||
import { MediaItem } from '../../pages/types'
|
import { MediaItem } from '../../pages/types'
|
||||||
|
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
|
|
||||||
import { formatDate } from '../../utils'
|
import { formatDate } from '../../utils'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
import { imageProxy } from '../../utils/imageProxy'
|
||||||
|
@ -24,9 +19,8 @@ import { AudioPlayer } from './AudioPlayer'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
import stylesHeader from '../Nav/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import { AudioHeader } from './AudioHeader'
|
import { AudioHeader } from './AudioHeader'
|
||||||
|
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
@ -47,6 +41,7 @@ export const FullArticle = (props: Props) => {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
actions: { requireAuthentication }
|
actions: { requireAuthentication }
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
||||||
|
@ -128,24 +123,79 @@ export const FullArticle = (props: Props) => {
|
||||||
setIsReactionsLoaded(true)
|
setIsReactionsLoaded(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
const clickHandlers = []
|
||||||
const tooltipElements: NodeListOf<HTMLLinkElement> =
|
const documentClickHandlers = []
|
||||||
document.querySelectorAll('[data-toggle="tooltip"]')
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!body()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
|
||||||
|
'[data-toggle="tooltip"], footnote'
|
||||||
|
)
|
||||||
if (!tooltipElements) return
|
if (!tooltipElements) return
|
||||||
tooltipElements.forEach((element) => {
|
tooltipElements.forEach((element) => {
|
||||||
const tooltip = document.createElement('div')
|
const tooltip = document.createElement('div')
|
||||||
tooltip.classList.add(styles.tooltip)
|
tooltip.classList.add(styles.tooltip)
|
||||||
tooltip.textContent = element.dataset.originalTitle
|
const tooltipContent = document.createElement('div')
|
||||||
|
tooltipContent.classList.add(styles.tooltipContent)
|
||||||
|
tooltipContent.innerHTML = element.dataset.originalTitle || element.dataset.value
|
||||||
|
|
||||||
|
tooltip.appendChild(tooltipContent)
|
||||||
|
|
||||||
document.body.appendChild(tooltip)
|
document.body.appendChild(tooltip)
|
||||||
createPopper(element, tooltip, { placement: 'top' })
|
|
||||||
|
if (element.hasAttribute('href')) {
|
||||||
|
element.setAttribute('href', 'javascript: void(0);')
|
||||||
|
}
|
||||||
|
createPopper(element, tooltip, {
|
||||||
|
placement: 'top',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'offset',
|
||||||
|
options: {
|
||||||
|
offset: [0, 8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
tooltip.style.visibility = 'hidden'
|
tooltip.style.visibility = 'hidden'
|
||||||
element.addEventListener('mouseenter', () => {
|
let isTooltipVisible = false
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isTooltipVisible) {
|
||||||
|
tooltip.style.visibility = 'hidden'
|
||||||
|
isTooltipVisible = false
|
||||||
|
} else {
|
||||||
tooltip.style.visibility = 'visible'
|
tooltip.style.visibility = 'visible'
|
||||||
})
|
isTooltipVisible = true
|
||||||
element.addEventListener('mouseleave', () => {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDocumentClick = (e) => {
|
||||||
|
if (isTooltipVisible && e.target !== element && e.target !== tooltip) {
|
||||||
tooltip.style.visibility = 'hidden'
|
tooltip.style.visibility = 'hidden'
|
||||||
|
isTooltipVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener('click', handleClick)
|
||||||
|
document.addEventListener('click', handleDocumentClick)
|
||||||
|
|
||||||
|
clickHandlers.push({ element, handler: handleClick })
|
||||||
|
documentClickHandlers.push(handleDocumentClick)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
clickHandlers.forEach(({ element, handler }) => {
|
||||||
|
element.removeEventListener('click', handler)
|
||||||
|
})
|
||||||
|
documentClickHandlers.forEach((handler) => {
|
||||||
|
document.removeEventListener('click', handler)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -190,6 +240,9 @@ export const FullArticle = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={props.article.lead}>
|
||||||
|
<section class={styles.lead} innerHTML={props.article.lead} />
|
||||||
|
</Show>
|
||||||
<Show when={props.article.layout === 'audio'}>
|
<Show when={props.article.layout === 'audio'}>
|
||||||
<AudioHeader
|
<AudioHeader
|
||||||
title={props.article.title}
|
title={props.article.title}
|
||||||
|
@ -231,8 +284,11 @@ export const FullArticle = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<Show when={isDesktop() && body()}>
|
<Show when={isDesktop() && body()}>
|
||||||
|
<div class="col-md-6 offset-md-1">
|
||||||
<TableOfContents variant="article" parentSelector="#shoutBody" body={body()} />
|
<TableOfContents variant="article" parentSelector="#shoutBody" body={body()} />
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,237 +0,0 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
|
||||||
import { Userpic } from './Userpic'
|
|
||||||
import { Icon } from '../_shared/Icon'
|
|
||||||
import styles from './AuthorCard.module.scss'
|
|
||||||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
|
||||||
import { translit } from '../../utils/ru2en'
|
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import { useSession } from '../../context/session'
|
|
||||||
import { StatMetrics } from '../_shared/StatMetrics'
|
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
|
||||||
import { router, useRouter } from '../../stores/router'
|
|
||||||
import { openPage } from '@nanostores/router'
|
|
||||||
import { useLocalize } from '../../context/localize'
|
|
||||||
|
|
||||||
interface AuthorCardProps {
|
|
||||||
caption?: string
|
|
||||||
hideWriteButton?: boolean
|
|
||||||
hideDescription?: boolean
|
|
||||||
hideFollow?: boolean
|
|
||||||
hasLink?: boolean
|
|
||||||
subscribed?: boolean
|
|
||||||
author: Author
|
|
||||||
isAuthorPage?: boolean
|
|
||||||
noSocialButtons?: boolean
|
|
||||||
isAuthorsList?: boolean
|
|
||||||
truncateBio?: boolean
|
|
||||||
liteButtons?: boolean
|
|
||||||
isTextButton?: boolean
|
|
||||||
isComments?: boolean
|
|
||||||
isFeedMode?: boolean
|
|
||||||
isNowrap?: boolean
|
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AuthorCard = (props: AuthorCardProps) => {
|
|
||||||
const { t, lang } = useLocalize()
|
|
||||||
|
|
||||||
const {
|
|
||||||
session,
|
|
||||||
isSessionLoaded,
|
|
||||||
actions: { loadSession, requireAuthentication }
|
|
||||||
} = useSession()
|
|
||||||
|
|
||||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
|
||||||
|
|
||||||
const subscribed = createMemo<boolean>(() => {
|
|
||||||
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
|
||||||
})
|
|
||||||
|
|
||||||
const subscribe = async (really = true) => {
|
|
||||||
setIsSubscribing(true)
|
|
||||||
|
|
||||||
await (really
|
|
||||||
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
|
||||||
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
|
||||||
|
|
||||||
await loadSession()
|
|
||||||
setIsSubscribing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
|
||||||
|
|
||||||
const name = createMemo(() => {
|
|
||||||
if (lang() !== 'ru') {
|
|
||||||
if (props.author.name === 'Дискурс') {
|
|
||||||
return 'Discours'
|
|
||||||
}
|
|
||||||
|
|
||||||
return translit(props.author.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.author.name
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: reimplement AuthorCard
|
|
||||||
const { changeSearchParam } = useRouter()
|
|
||||||
const initChat = () => {
|
|
||||||
requireAuthentication(() => {
|
|
||||||
openPage(router, `inbox`)
|
|
||||||
changeSearchParam('initChat', `${props.author.id}`)
|
|
||||||
}, 'discussions')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
|
||||||
requireAuthentication(() => {
|
|
||||||
subscribe(true)
|
|
||||||
}, 'subscribe')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={clsx(styles.author, props.class)}
|
|
||||||
classList={{
|
|
||||||
[styles.authorPage]: props.isAuthorPage,
|
|
||||||
[styles.authorComments]: props.isComments,
|
|
||||||
[styles.authorsListItem]: props.isAuthorsList,
|
|
||||||
[styles.feedMode]: props.isFeedMode,
|
|
||||||
[styles.nowrapView]: props.isNowrap
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Userpic
|
|
||||||
name={props.author.name}
|
|
||||||
userpic={props.author.userpic}
|
|
||||||
hasLink={props.hasLink}
|
|
||||||
isBig={props.isAuthorPage}
|
|
||||||
isAuthorsList={props.isAuthorsList}
|
|
||||||
isFeedMode={props.isFeedMode}
|
|
||||||
class={styles.circlewrap}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class={styles.authorDetails}>
|
|
||||||
<div class={styles.authorDetailsWrapper}>
|
|
||||||
<Show when={props.hasLink}>
|
|
||||||
<div class={styles.authorNameContainer}>
|
|
||||||
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
|
||||||
{name()}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={!props.hasLink}>
|
|
||||||
<div class={styles.authorName}>{name()}</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!props.hideDescription && props.author.bio}>
|
|
||||||
{props.isAuthorsList}
|
|
||||||
<div
|
|
||||||
class={styles.authorAbout}
|
|
||||||
classList={{ 'text-truncate': props.truncateBio }}
|
|
||||||
innerHTML={props.author.bio}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.author.stat}>
|
|
||||||
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={props.author.stat} />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<ShowOnlyOnClient>
|
|
||||||
<Show when={isSessionLoaded()}>
|
|
||||||
<Show when={canFollow()}>
|
|
||||||
<div class={styles.authorSubscribe}>
|
|
||||||
<Show
|
|
||||||
when={subscribed()}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
onClick={handleSubscribe}
|
|
||||||
class={clsx('button', styles.button)}
|
|
||||||
classList={{
|
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
|
||||||
'button--subscribe': !props.isAuthorsList,
|
|
||||||
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
|
||||||
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
|
||||||
[styles.isSubscribing]: isSubscribing()
|
|
||||||
}}
|
|
||||||
disabled={isSubscribing()}
|
|
||||||
>
|
|
||||||
<Show when={!props.isAuthorsList && !props.isTextButton}>
|
|
||||||
<Icon name="author-subscribe" class={styles.icon} />
|
|
||||||
</Show>
|
|
||||||
<Show when={props.isTextButton}>
|
|
||||||
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
|
|
||||||
{t('Follow')}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => subscribe(false)}
|
|
||||||
class={clsx('button', styles.button)}
|
|
||||||
classList={{
|
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
|
||||||
'button--subscribe': !props.isAuthorsList,
|
|
||||||
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
|
||||||
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
|
||||||
[styles.isSubscribing]: isSubscribing()
|
|
||||||
}}
|
|
||||||
disabled={isSubscribing()}
|
|
||||||
>
|
|
||||||
<Show when={!props.isAuthorsList && !props.isTextButton}>
|
|
||||||
<Icon name="author-unsubscribe" class={styles.icon} />
|
|
||||||
</Show>
|
|
||||||
<Show when={props.isTextButton}>
|
|
||||||
<span
|
|
||||||
class={clsx(
|
|
||||||
styles.buttonLabel,
|
|
||||||
styles.buttonLabelVisible,
|
|
||||||
styles.buttonUnfollowLabel
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('Unfollow')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class={clsx(
|
|
||||||
styles.buttonLabel,
|
|
||||||
styles.buttonLabelVisible,
|
|
||||||
styles.buttonSubscribedLabel
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('You are subscribed')}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!props.hideWriteButton}>
|
|
||||||
<button
|
|
||||||
class={styles.button}
|
|
||||||
classList={{
|
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList,
|
|
||||||
'button--subscribe': !props.isAuthorsList,
|
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
|
||||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
|
||||||
}}
|
|
||||||
onClick={initChat}
|
|
||||||
>
|
|
||||||
<Show when={!props.isTextButton}>
|
|
||||||
<Icon name="comment" class={styles.icon} />
|
|
||||||
</Show>
|
|
||||||
<Show when={!props.liteButtons || props.isTextButton}>{t('Write')}</Show>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Show when={!props.noSocialButtons}>
|
|
||||||
<div class={styles.authorSubscribeSocial}>
|
|
||||||
<For each={props.author.links}>{(link) => <a href={link} />}</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</ShowOnlyOnClient>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -14,9 +14,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorDetails {
|
.authorDetails {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(sm) {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
|
&.authorDetailsShrinked {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -58,29 +65,28 @@
|
||||||
|
|
||||||
.authorAbout {
|
.authorAbout {
|
||||||
color: rgb(0 0 0 / 60%);
|
color: rgb(0 0 0 / 60%);
|
||||||
font-size: 1.5rem;
|
font-size: 1.4rem;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorSubscribe {
|
.authorSubscribe {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
//display: flex;
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background: #f7f7f7;
|
|
||||||
border: none;
|
border: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 32px;
|
height: 24px;
|
||||||
margin-right: 0.4rem;
|
margin-right: 0.4rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 32px;
|
width: 24px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/user-link-default.svg);
|
background-image: url(/icons/user-link-default.svg);
|
||||||
|
@ -88,59 +94,131 @@
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
content: '';
|
content: '';
|
||||||
filter: invert(1);
|
height: 100%;
|
||||||
height: 18px;
|
|
||||||
left: 50%;
|
left: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
transition: filter 0.2s;
|
transition: filter 0.2s;
|
||||||
width: 18px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #000;
|
background: #000;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
filter: invert(0);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='facebook.com/'] {
|
a[href*='facebook.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/facebook-white.svg);
|
background-image: url(/icons/user-link-facebook.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='twitter.com/'] {
|
a[href*='twitter.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/twitter-white.svg);
|
background-image: url(/icons/user-link-twitter.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='telegram.com/'] {
|
a[href*='telegram.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/telegram-white.svg);
|
background-image: url(/icons/user-link-telegram.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='vk.cc/'],
|
a[href*='vk.cc/'],
|
||||||
a[href*='vk.com/'] {
|
a[href*='vk.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/vk-white.svg);
|
background-image: url(/icons/user-link-vk.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='tumblr.com/'] {
|
a[href*='tumblr.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/tumblr-white.svg);
|
background-image: url(/icons/user-link-tumblr.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a[href*='instagram.com/'] {
|
a[href*='instagram.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/instagram-white.svg);
|
background-image: url(/icons/user-link-instagram.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='behance.net/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-behance.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='dribbble.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-dribbble.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='github.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-github.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='linkedin.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-linkedin.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='medium.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-medium.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='ok.ru/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-ok.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='pinterest.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-pinterest.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='reddit.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-reddit.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='tiktok.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-tiktok.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='vk.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-vk.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='youtube.com/'],
|
||||||
|
a[href*='youtu.be/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-youtube.svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href*='dzen.ru/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-dzen.svg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,6 +249,7 @@
|
||||||
.authorSubscribeSocial {
|
.authorSubscribeSocial {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin: 0 0.8rem 1.6rem;
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
flex: 1 100%;
|
flex: 1 100%;
|
||||||
|
@ -237,8 +316,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorAbout {
|
.authorAbout {
|
||||||
@include font-size(1.7rem);
|
@include font-size(2rem);
|
||||||
|
|
||||||
color: #696969;
|
color: #696969;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,3 +444,38 @@
|
||||||
width: 1.6rem;
|
width: 1.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subscribersContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribers {
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
margin-right: 3rem;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
.userpic {
|
||||||
|
background: var(--background-color);
|
||||||
|
box-shadow: 0 0 0 2px var(--background-color);
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: -2.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribersCounter {
|
||||||
|
margin-left: -0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listWrapper {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
417
src/components/Author/AuthorCard/AuthorCard.tsx
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
import type { Author } from '../../../graphql/types.gen'
|
||||||
|
import { Userpic } from '../Userpic'
|
||||||
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
import styles from './AuthorCard.module.scss'
|
||||||
|
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||||
|
import { translit } from '../../../utils/ru2en'
|
||||||
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
|
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
|
||||||
|
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||||
|
import { router, useRouter } from '../../../stores/router'
|
||||||
|
import { openPage } from '@nanostores/router'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||||
|
import { Modal } from '../../Nav/Modal'
|
||||||
|
import { showModal } from '../../../stores/ui'
|
||||||
|
import { TopicCard } from '../../Topic/Card'
|
||||||
|
import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension'
|
||||||
|
|
||||||
|
type SubscriptionFilter = 'all' | 'users' | 'topics'
|
||||||
|
type AuthorCardProps = {
|
||||||
|
caption?: string
|
||||||
|
hideWriteButton?: boolean
|
||||||
|
hideDescription?: boolean
|
||||||
|
hideFollow?: boolean
|
||||||
|
hasLink?: boolean
|
||||||
|
subscribed?: boolean
|
||||||
|
author: Author
|
||||||
|
isAuthorPage?: boolean
|
||||||
|
noSocialButtons?: boolean
|
||||||
|
isAuthorsList?: boolean
|
||||||
|
truncateBio?: boolean
|
||||||
|
liteButtons?: boolean
|
||||||
|
isTextButton?: boolean
|
||||||
|
isComments?: boolean
|
||||||
|
isFeedMode?: boolean
|
||||||
|
isNowrap?: boolean
|
||||||
|
class?: string
|
||||||
|
followers?: Author[]
|
||||||
|
subscriptions?: Array<Author | Topic>
|
||||||
|
showPublicationsCounter?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthor(value: Author | Topic): value is Author {
|
||||||
|
return 'name' in value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
|
const { t, lang } = useLocalize()
|
||||||
|
|
||||||
|
const {
|
||||||
|
session,
|
||||||
|
isSessionLoaded,
|
||||||
|
actions: { loadSession, requireAuthentication }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
|
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||||
|
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>(props.subscriptions)
|
||||||
|
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||||
|
|
||||||
|
const subscribed = createMemo<boolean>(() => {
|
||||||
|
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||||
|
})
|
||||||
|
|
||||||
|
const subscribe = async (really = true) => {
|
||||||
|
setIsSubscribing(true)
|
||||||
|
|
||||||
|
await (really
|
||||||
|
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
||||||
|
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
||||||
|
|
||||||
|
await loadSession()
|
||||||
|
setIsSubscribing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
||||||
|
|
||||||
|
const name = createMemo(() => {
|
||||||
|
if (lang() !== 'ru') {
|
||||||
|
if (props.author.name === 'Дискурс') {
|
||||||
|
return 'Discours'
|
||||||
|
}
|
||||||
|
|
||||||
|
return translit(props.author.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.author.name
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: reimplement AuthorCard
|
||||||
|
const { changeSearchParam } = useRouter()
|
||||||
|
const initChat = () => {
|
||||||
|
requireAuthentication(() => {
|
||||||
|
openPage(router, `inbox`)
|
||||||
|
changeSearchParam('initChat', `${props.author.id}`)
|
||||||
|
}, 'discussions')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubscribe = () => {
|
||||||
|
requireAuthentication(() => {
|
||||||
|
subscribe(true)
|
||||||
|
}, 'subscribe')
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.subscriptions) {
|
||||||
|
if (subscriptionFilter() === 'users') {
|
||||||
|
setSubscriptions(props.subscriptions.filter((s) => 'name' in s))
|
||||||
|
} else if (subscriptionFilter() === 'topics') {
|
||||||
|
setSubscriptions(props.subscriptions.filter((s) => 'title' in s))
|
||||||
|
} else {
|
||||||
|
setSubscriptions(props.subscriptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.author, props.class)}
|
||||||
|
classList={{
|
||||||
|
['row']: props.isAuthorPage,
|
||||||
|
[styles.authorPage]: props.isAuthorPage,
|
||||||
|
[styles.authorComments]: props.isComments,
|
||||||
|
[styles.authorsListItem]: props.isAuthorsList,
|
||||||
|
[styles.feedMode]: props.isFeedMode,
|
||||||
|
[styles.nowrapView]: props.isNowrap
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.isAuthorPage}
|
||||||
|
fallback={
|
||||||
|
<Userpic
|
||||||
|
name={props.author.name}
|
||||||
|
userpic={props.author.userpic}
|
||||||
|
hasLink={props.hasLink}
|
||||||
|
isBig={props.isAuthorPage}
|
||||||
|
isAuthorsList={props.isAuthorsList}
|
||||||
|
isFeedMode={props.isFeedMode}
|
||||||
|
class={styles.circlewrap}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<Userpic
|
||||||
|
name={props.author.name}
|
||||||
|
userpic={props.author.userpic}
|
||||||
|
hasLink={props.hasLink}
|
||||||
|
isBig={props.isAuthorPage}
|
||||||
|
isAuthorsList={props.isAuthorsList}
|
||||||
|
isFeedMode={props.isFeedMode}
|
||||||
|
class={styles.circlewrap}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={styles.authorDetails}
|
||||||
|
classList={{
|
||||||
|
'col-md-15 col-xl-13': props.isAuthorPage,
|
||||||
|
[styles.authorDetailsShrinked]: props.isAuthorPage
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class={styles.authorDetailsWrapper}>
|
||||||
|
<div class={styles.authorNameContainer}>
|
||||||
|
<ConditionalWrapper
|
||||||
|
condition={props.hasLink}
|
||||||
|
wrapper={(children) => (
|
||||||
|
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
|
||||||
|
</ConditionalWrapper>
|
||||||
|
</div>
|
||||||
|
{/*TODO: implement plurals by i18n*/}
|
||||||
|
<Show
|
||||||
|
when={props.author.bio}
|
||||||
|
fallback={
|
||||||
|
props.showPublicationsCounter ? (
|
||||||
|
<div class={styles.authorAbout}>{props.author.stat?.shouts} публикаций</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={styles.authorAbout}
|
||||||
|
classList={{ 'text-truncate': props.truncateBio }}
|
||||||
|
innerHTML={props.author.bio}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
(props.followers && props.followers.length > 0) ||
|
||||||
|
(props.subscriptions && props.subscriptions.length > 0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class={styles.subscribersContainer}>
|
||||||
|
<Show when={props.followers && props.followers.length > 0}>
|
||||||
|
<div class={styles.subscribers} onClick={() => showModal('followers')}>
|
||||||
|
<For each={props.followers.slice(0, 3)}>
|
||||||
|
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||||
|
</For>
|
||||||
|
<div class={styles.subscribersCounter}>
|
||||||
|
{props.followers.length}
|
||||||
|
{getNumeralsDeclension(props.followers.length, [
|
||||||
|
t('subscriber'),
|
||||||
|
t('subscriber_rp'),
|
||||||
|
t('subscribers')
|
||||||
|
])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.subscriptions && props.subscriptions.length > 0}>
|
||||||
|
<div class={styles.subscribers} onClick={() => showModal('subscriptions')}>
|
||||||
|
<For each={props.subscriptions.slice(0, 3)}>
|
||||||
|
{(f) => {
|
||||||
|
if ('name' in f) {
|
||||||
|
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
||||||
|
} else if ('title' in f) {
|
||||||
|
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
<div class={styles.subscribersCounter}>
|
||||||
|
{props.subscriptions.length}
|
||||||
|
{getNumeralsDeclension(props.subscriptions.length, [
|
||||||
|
t('subscription'),
|
||||||
|
t('subscription_rp'),
|
||||||
|
t('subscriptions')
|
||||||
|
])}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<ShowOnlyOnClient>
|
||||||
|
<Show when={isSessionLoaded()}>
|
||||||
|
<Show when={canFollow()}>
|
||||||
|
<div class={styles.authorSubscribe}>
|
||||||
|
<Show when={!props.noSocialButtons && !props.hideWriteButton}>
|
||||||
|
<div class={styles.authorSubscribeSocial}>
|
||||||
|
<For each={props.author.links}>{(link) => <a href={link} />}</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={subscribed()}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
onClick={handleSubscribe}
|
||||||
|
class={clsx('button', styles.button)}
|
||||||
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
|
>
|
||||||
|
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||||
|
<Icon name="author-subscribe" class={styles.icon} />
|
||||||
|
</Show>
|
||||||
|
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||||
|
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
|
||||||
|
{t('Follow')}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => subscribe(false)}
|
||||||
|
class={clsx('button', styles.button)}
|
||||||
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
|
>
|
||||||
|
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||||
|
<Icon name="author-unsubscribe" class={styles.icon} />
|
||||||
|
</Show>
|
||||||
|
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
styles.buttonLabel,
|
||||||
|
styles.buttonLabelVisible,
|
||||||
|
styles.buttonUnfollowLabel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('Unfollow')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
styles.buttonLabel,
|
||||||
|
styles.buttonLabelVisible,
|
||||||
|
styles.buttonSubscribedLabel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('You are subscribed')}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!props.hideWriteButton}>
|
||||||
|
<button
|
||||||
|
class={styles.button}
|
||||||
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
|
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||||
|
}}
|
||||||
|
onClick={initChat}
|
||||||
|
>
|
||||||
|
<Show when={!props.isTextButton && !props.isAuthorPage}>
|
||||||
|
<Icon name="comment" class={styles.icon} />
|
||||||
|
</Show>
|
||||||
|
<Show when={!props.liteButtons || props.isTextButton}>{t('Write')}</Show>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</ShowOnlyOnClient>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={props.followers}>
|
||||||
|
<Modal variant="wide" name="followers">
|
||||||
|
<>
|
||||||
|
<h2>{t('Followers')}</h2>
|
||||||
|
<div class={styles.listWrapper}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-24">
|
||||||
|
<For each={props.followers}>
|
||||||
|
{(follower: Author) => (
|
||||||
|
<AuthorCard
|
||||||
|
author={follower}
|
||||||
|
hideWriteButton={true}
|
||||||
|
hasLink={true}
|
||||||
|
isTextButton={true}
|
||||||
|
liteButtons={true}
|
||||||
|
truncateBio={true}
|
||||||
|
showPublicationsCounter={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.subscriptions}>
|
||||||
|
<Modal variant="wide" name="subscriptions">
|
||||||
|
<>
|
||||||
|
<h2>{t('Subscriptions')}</h2>
|
||||||
|
<ul class="view-switcher">
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||||
|
{t('All')} {props.subscriptions.length}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
||||||
|
{t('Users')} {props.subscriptions.filter((s) => 'name' in s).length}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||||
|
{t('Topics')} {props.subscriptions.filter((s) => 'title' in s).length}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<div class={styles.listWrapper}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-24">
|
||||||
|
<For each={subscriptions()}>
|
||||||
|
{(subscription: Author | Topic) =>
|
||||||
|
isAuthor(subscription) ? (
|
||||||
|
<AuthorCard
|
||||||
|
author={subscription}
|
||||||
|
hideWriteButton={true}
|
||||||
|
hasLink={true}
|
||||||
|
isTextButton={true}
|
||||||
|
truncateBio={true}
|
||||||
|
showPublicationsCounter={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TopicCard compact isTopicInRow showDescription isCardMode topic={subscription} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/Author/AuthorCard/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { AuthorCard } from './AuthorCard'
|
|
@ -1,30 +0,0 @@
|
||||||
.user-details {
|
|
||||||
margin: 0 0 5.4rem;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
margin-left: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
margin-left: 235px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.author-page {
|
|
||||||
.view-switcher {
|
|
||||||
margin-top: 0;
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-size: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.group__controls {
|
|
||||||
margin-bottom: 2em !important;
|
|
||||||
margin-top: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import type { Author } from '../../graphql/types.gen'
|
|
||||||
import { AuthorCard } from './AuthorCard'
|
|
||||||
import './Full.scss'
|
|
||||||
|
|
||||||
export const AuthorFull = (props: { author: Author }) => {
|
|
||||||
return (
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-18 col-lg-16 user-details">
|
|
||||||
<AuthorCard author={props.author} isAuthorPage={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -55,19 +55,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
margin-right: 0;
|
aspect-ratio: 1/1;
|
||||||
|
margin: 0 auto;
|
||||||
max-width: 168px;
|
max-width: 168px;
|
||||||
min-width: 168px;
|
height: auto;
|
||||||
height: 168px;
|
width: 100%;
|
||||||
width: 168px;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
margin-right: 4.8rem;
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.letters {
|
.letters {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
line-height: 168px;
|
justify-content: center;
|
||||||
|
line-height: normal;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,30 @@
|
||||||
.about-discours {
|
.aboutDiscours {
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.6rem);
|
||||||
|
background: #fef2f2;
|
||||||
background: #000;
|
font-weight: 500;
|
||||||
color: #fff;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-bottom: 6.4rem;
|
margin-bottom: 6.4rem;
|
||||||
padding: 3.6rem 0;
|
padding: 8rem 0 6.4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
margin-bottom: 4rem;
|
@include font-size(4rem);
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
em {
|
em {
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-discours__actions {
|
.aboutDiscoursActions {
|
||||||
margin-top: 4.8rem;
|
margin-top: 4.8rem;
|
||||||
|
|
||||||
.button {
|
:global(.button) {
|
||||||
border: 3px solid;
|
border: 3px solid #000;
|
||||||
border-radius: 1.2em;
|
border-radius: 0.8rem;
|
||||||
color: inherit;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -39,7 +35,6 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-color: #fff;
|
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,38 +1,37 @@
|
||||||
import './Hero.scss'
|
import styles from './Hero.module.scss'
|
||||||
|
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { useRouter } from '../../stores/router'
|
||||||
|
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="about-discours">
|
<div class={styles.aboutDiscours}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-20 offset-lg-2 col-xl-16 offset-xl-4">
|
<div class="col-lg-20 offset-lg-2 col-xl-18 offset-xl-3">
|
||||||
<h4>{t('Horizontal collaborative journalistic platform')}</h4>
|
<h4 innerHTML={t('Horizontal collaborative journalistic platform')} />
|
||||||
<p>
|
<p
|
||||||
{t(
|
innerHTML={t(
|
||||||
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects'
|
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects'
|
||||||
)}
|
)}
|
||||||
.
|
/>
|
||||||
<br />
|
<div class={styles.aboutDiscoursActions}>
|
||||||
<em>
|
|
||||||
{t('We are convinced that one voice is good, but many is better') +
|
|
||||||
'. ' +
|
|
||||||
t('We create the most amazing stories together')}
|
|
||||||
.
|
|
||||||
</em>
|
|
||||||
</p>
|
|
||||||
<div class="about-discours__actions">
|
|
||||||
<a class="button" onClick={() => showModal('auth')}>
|
|
||||||
{t('Join the community')}
|
|
||||||
</a>
|
|
||||||
<a class="button" href="/create">
|
<a class="button" href="/create">
|
||||||
{t('Become an author')}
|
{t('Create post')}
|
||||||
</a>
|
</a>
|
||||||
<a class="button" href="/about/manifest">
|
<a
|
||||||
{t('About the project')}
|
class="button"
|
||||||
|
onClick={() => {
|
||||||
|
showModal('auth')
|
||||||
|
changeSearchParam('mode', 'register')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Join the community')}
|
||||||
</a>
|
</a>
|
||||||
<a class="button" href="/about/help">
|
<a class="button" href="/about/help">
|
||||||
{t('Support us')}
|
{t('Support us')}
|
||||||
|
|
|
@ -1,22 +1,8 @@
|
||||||
@mixin input-placeholder-overflow($direction: 'down') {
|
|
||||||
@if $direction == 'down' {
|
|
||||||
@media (width <= 1410px) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else if $direction == 'up' {
|
|
||||||
@media (width > 1410px) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
@error "Unknown direction #{$direction}.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@include input-placeholder-overflow(down) {
|
@include media-breakpoint-down(xxl) {
|
||||||
margin-bottom: 2.4rem;
|
margin-bottom: 2.4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +11,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include input-placeholder-overflow(down) {
|
@include media-breakpoint-down(xxl) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@include font-size(2rem);
|
@include font-size(2rem);
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
@ -45,11 +30,11 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
|
||||||
@include input-placeholder-overflow(up) {
|
@include media-breakpoint-up(xxl) {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include input-placeholder-overflow(down) {
|
@include media-breakpoint-down(xxl) {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +47,7 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
@include input-placeholder-overflow(down) {
|
@include media-breakpoint-down(xxl) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,6 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false)
|
const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false)
|
||||||
return (
|
return (
|
||||||
<div ref={props.ref} class={styles.BubbleMenu}>
|
<div ref={props.ref} class={styles.BubbleMenu}>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.bubbleMenuButton}
|
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('left').run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-image-align-left" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={styles.bubbleMenuButton}
|
class={styles.bubbleMenuButton}
|
||||||
|
@ -46,15 +39,6 @@ export const IncutBubbleMenu = (props: Props) => {
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-half-align-right" />
|
<Icon name="editor-image-half-align-right" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class={styles.bubbleMenuButton}
|
|
||||||
onClick={() => props.editor.chain().focus().setArticleFloat('right').run()}
|
|
||||||
>
|
|
||||||
<Icon name="editor-image-align-right" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
<div class={styles.dropDownHolder}>
|
<div class={styles.dropDownHolder}>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createEffect, createSignal, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup } from 'solid-js'
|
||||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
|
@ -41,12 +41,9 @@ import Article from './extensions/Article'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import { TableOfContents } from '../TableOfContents'
|
|
||||||
|
|
||||||
import { isDesktop } from '../../utils/media-query'
|
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
import { Image } from '@tiptap/extension-image'
|
import { Image } from '@tiptap/extension-image'
|
||||||
|
import { Footnote } from './extensions/Footnote'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -62,6 +59,7 @@ export const Editor = (props: Props) => {
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
|
|
||||||
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
|
||||||
|
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
|
||||||
|
|
||||||
const docName = `shout-${props.shoutId}`
|
const docName = `shout-${props.shoutId}`
|
||||||
|
|
||||||
|
@ -159,7 +157,7 @@ export const Editor = (props: Props) => {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
placeholder: t('Short opening')
|
placeholder: t('Add a link or click plus to embed media')
|
||||||
}),
|
}),
|
||||||
Focus,
|
Focus,
|
||||||
Gapcursor,
|
Gapcursor,
|
||||||
|
@ -173,8 +171,9 @@ export const Editor = (props: Props) => {
|
||||||
ImageFigure,
|
ImageFigure,
|
||||||
Image,
|
Image,
|
||||||
Figcaption,
|
Figcaption,
|
||||||
|
Footnote,
|
||||||
Embed,
|
Embed,
|
||||||
CharacterCount,
|
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
pluginKey: 'textBubbleMenu',
|
pluginKey: 'textBubbleMenu',
|
||||||
element: textBubbleMenuRef.current,
|
element: textBubbleMenuRef.current,
|
||||||
|
@ -183,7 +182,11 @@ export const Editor = (props: Props) => {
|
||||||
const { empty } = selection
|
const { empty } = selection
|
||||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
setIsCommonMarkup(e.isActive('figcaption'))
|
setIsCommonMarkup(e.isActive('figcaption'))
|
||||||
return view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')
|
const result =
|
||||||
|
(view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')) ||
|
||||||
|
e.isActive('footnote')
|
||||||
|
setShouldShowTextBubbleMenu(result)
|
||||||
|
return result
|
||||||
},
|
},
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
sticky: true
|
sticky: true
|
||||||
|
@ -244,13 +247,21 @@ export const Editor = (props: Props) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
editor().destroy()
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-5" />
|
||||||
|
<div class="col-md-12">
|
||||||
<div ref={(el) => (editorElRef.current = el)} id="editorBody" />
|
<div ref={(el) => (editorElRef.current = el)} id="editorBody" />
|
||||||
<Show when={isDesktop() && html()}>
|
</div>
|
||||||
<TableOfContents variant="editor" parentSelector="#editorBody" body={html()} />
|
</div>
|
||||||
</Show>
|
|
||||||
<TextBubbleMenu
|
<TextBubbleMenu
|
||||||
|
shouldShow={shouldShowTextBubbleMenu()}
|
||||||
isCommonMarkup={isCommonMarkup()}
|
isCommonMarkup={isCommonMarkup()}
|
||||||
editor={editor()}
|
editor={editor()}
|
||||||
ref={(el) => (textBubbleMenuRef.current = el)}
|
ref={(el) => (textBubbleMenuRef.current = el)}
|
||||||
|
|
|
@ -8,10 +8,9 @@ import { useLocalize } from '../../../context/localize'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { Menu } from './Menu'
|
import { Menu } from './Menu'
|
||||||
import type { MenuItem } from './Menu/Menu'
|
import type { MenuItem } from './Menu/Menu'
|
||||||
import { hideModal, showModal } from '../../../stores/ui'
|
import { showModal } from '../../../stores/ui'
|
||||||
import { UploadModalContent } from '../UploadModalContent'
|
import { UploadModalContent } from '../UploadModalContent'
|
||||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import styles from './Menu.module.scss'
|
import styles from './Menu.module.scss'
|
||||||
import { Icon } from '../../../_shared/Icon'
|
import { Icon } from '../../../_shared/Icon'
|
||||||
|
import { Popover } from '../../../_shared/Popover'
|
||||||
|
import { useLocalize } from '../../../../context/localize'
|
||||||
|
|
||||||
export type MenuItem = 'image' | 'embed' | 'horizontal-rule'
|
export type MenuItem = 'image' | 'embed' | 'horizontal-rule'
|
||||||
|
|
||||||
|
@ -8,21 +10,34 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Menu = (props: Props) => {
|
export const Menu = (props: Props) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
const setSelectedMenuItem = (value: MenuItem) => {
|
const setSelectedMenuItem = (value: MenuItem) => {
|
||||||
props.selectedItem(value)
|
props.selectedItem(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.Menu}>
|
<div class={styles.Menu}>
|
||||||
<button type="button" onClick={() => setSelectedMenuItem('image')}>
|
<Popover content={t('Add image')}>
|
||||||
|
{(triggerRef: (el) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('image')}>
|
||||||
<Icon class={styles.icon} name="editor-image" />
|
<Icon class={styles.icon} name="editor-image" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setSelectedMenuItem('embed')}>
|
)}
|
||||||
|
</Popover>
|
||||||
|
<Popover content={t('Add an embed widget')}>
|
||||||
|
{(triggerRef: (el) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('embed')}>
|
||||||
<Icon class={styles.icon} name="editor-embed" />
|
<Icon class={styles.icon} name="editor-embed" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => setSelectedMenuItem('horizontal-rule')}>
|
)}
|
||||||
|
</Popover>
|
||||||
|
<Popover content={t('Add rule')}>
|
||||||
|
{(triggerRef: (el) => void) => (
|
||||||
|
<button ref={triggerRef} type="button" onClick={() => setSelectedMenuItem('horizontal-rule')}>
|
||||||
<Icon class={styles.icon} name="editor-horizontal-rule" />
|
<Icon class={styles.icon} name="editor-horizontal-rule" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,79 +9,11 @@
|
||||||
float: left;
|
float: left;
|
||||||
height: 0;
|
height: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-weight: 500;
|
|
||||||
font-size: 20px;
|
|
||||||
line-height: 30px;
|
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeping the cursor active when moving outside the editable area
|
// Keeping the cursor active when moving outside the editable area
|
||||||
|
|
||||||
.articleEditor p,
|
|
||||||
.articleEditor ul,
|
|
||||||
.articleEditor h4,
|
|
||||||
.articleEditor ol {
|
|
||||||
box-sizing: content-box;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
@media (width >= 768px) {
|
|
||||||
padding-left: calc(21.9% + 3px);
|
|
||||||
max-width: 72.7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1200px) {
|
|
||||||
padding-left: calc(21.5% + 3px);
|
|
||||||
max-width: 64.9%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleEditor blockquote,
|
|
||||||
.articleEditor figure,
|
|
||||||
.articleEditor article[data-type='incut'] {
|
|
||||||
@media (width >= 768px) {
|
|
||||||
margin-left: calc(21.9% + 3px) !important;
|
|
||||||
max-width: 73.6%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1200px) {
|
|
||||||
margin-left: calc(21.4% + 3px) !important;
|
|
||||||
max-width: 65.3%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleEditor h2 {
|
|
||||||
@media (width >= 768px) {
|
|
||||||
padding-left: calc(21.9% + 2px);
|
|
||||||
max-width: 72.7%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1200px) {
|
|
||||||
padding-left: 21.5%;
|
|
||||||
max-width: 87.1%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleEditor h3 {
|
|
||||||
@media (width >= 768px) {
|
|
||||||
padding-left: calc(21.9% + 2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width >= 1200px) {
|
|
||||||
padding-left: 21.5%;
|
|
||||||
max-width: 87.1%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleEditor * p,
|
|
||||||
.articleEditor * h2,
|
|
||||||
.articleEditor * h3,
|
|
||||||
.articleEditor * h4 {
|
|
||||||
@media (width >= 768px) {
|
|
||||||
padding-left: unset;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Give a remote user a caret */
|
/* Give a remote user a caret */
|
||||||
.collaboration-cursor__caret {
|
.collaboration-cursor__caret {
|
||||||
border-left: 1px solid #0d0d0d;
|
border-left: 1px solid #0d0d0d;
|
||||||
|
@ -326,3 +258,27 @@ mark.highlight {
|
||||||
figure[data-type='capturedImage'] {
|
figure[data-type='capturedImage'] {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footnote {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 1em;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: -2px;
|
||||||
|
border: unset;
|
||||||
|
background-size: 10px;
|
||||||
|
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImJpIGJpLWluZm8tY2lyY2xlIiBmaWxsPSJjdXJyZW50Q29sb3IiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04IDE1QTcgNyAwIDEgMSA4IDFhNyA3IDAgMCAxIDAgMTR6bTAgMUE4IDggMCAxIDAgOCAwYTggOCAwIDAgMCAwIDE2eiIvPjxwYXRoIGQ9Im04LjkzIDYuNTg4LTIuMjkuMjg3LS4wODIuMzguNDUuMDgzYy4yOTQuMDcuMzUyLjE3Ni4yODguNDY5bC0uNzM4IDMuNDY4Yy0uMTk0Ljg5Ny4xMDUgMS4zMTkuODA4IDEuMzE5LjU0NSAwIDEuMTc4LS4yNTIgMS40NjUtLjU5OGwuMDg4LS40MTZjLS4yLjE3Ni0uNDkyLjI0Ni0uNjg2LjI0Ni0uMjc1IDAtLjM3NS0uMTkzLS4zMDQtLjUzM0w4LjkzIDYuNTg4ek05IDQuNWExIDEgMCAxIDEtMiAwIDEgMSAwIDAgMSAyIDB6Ii8+PC9zdmc+');
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
background: var(--black-50);
|
background: var(--black-50);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 16px 16px 8px;
|
padding: 16px 16px 8px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
.simplifiedEditorField {
|
.simplifiedEditorField {
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
@ -54,6 +55,11 @@
|
||||||
bottom: -1rem;
|
bottom: -1rem;
|
||||||
transition: 0.3s ease-in-out;
|
transition: 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&.alwaysVisible {
|
||||||
|
opacity: unset;
|
||||||
|
bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -92,4 +98,48 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.minimal {
|
||||||
|
background: unset;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& div[contenteditable] {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bordered {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 16px 12px 6px 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 2px solid var(--black-100);
|
||||||
|
background: var(--white-500);
|
||||||
|
|
||||||
|
& div[contenteditable] {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.labelVisible {
|
||||||
|
padding-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limit {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 12px;
|
||||||
|
color: var(--black-400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createEffect, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
import { Portal } from 'solid-js/web'
|
||||||
import {
|
import {
|
||||||
createEditorTransaction,
|
createEditorTransaction,
|
||||||
createTiptapEditor,
|
createTiptapEditor,
|
||||||
|
@ -30,13 +31,20 @@ import { UploadedFile } from '../../pages/types'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
import { Image } from '@tiptap/extension-image'
|
import { Image } from '@tiptap/extension-image'
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
|
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialContent?: string
|
|
||||||
onSubmit?: (text: string) => void
|
|
||||||
onChange?: (text: string) => void
|
|
||||||
placeholder: string
|
placeholder: string
|
||||||
|
initialContent?: string
|
||||||
|
label?: string
|
||||||
|
onSubmit?: (text: string) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
onChange?: (text: string) => void
|
||||||
|
variant?: 'minimal' | 'bordered'
|
||||||
|
maxLength?: number
|
||||||
|
maxHeight?: number
|
||||||
submitButtonText?: string
|
submitButtonText?: string
|
||||||
quoteEnabled?: boolean
|
quoteEnabled?: boolean
|
||||||
imageEnabled?: boolean
|
imageEnabled?: boolean
|
||||||
|
@ -44,10 +52,15 @@ type Props = {
|
||||||
smallHeight?: boolean
|
smallHeight?: boolean
|
||||||
submitByEnter?: boolean
|
submitByEnter?: boolean
|
||||||
submitByShiftEnter?: boolean
|
submitByShiftEnter?: boolean
|
||||||
|
onlyBubbleControls?: boolean
|
||||||
|
controlsAlwaysVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MAX_DESCRIPTION_LIMIT = 400
|
||||||
|
|
||||||
const SimplifiedEditor = (props: Props) => {
|
const SimplifiedEditor = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
const [counter, setCounter] = createSignal<number>()
|
||||||
|
|
||||||
const wrapperEditorElRef: {
|
const wrapperEditorElRef: {
|
||||||
current: HTMLElement
|
current: HTMLElement
|
||||||
|
@ -61,6 +74,12 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const textBubbleMenuRef: {
|
||||||
|
current: HTMLDivElement
|
||||||
|
} = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
actions: { setEditor }
|
actions: { setEditor }
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
|
@ -70,6 +89,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
content: 'figcaption image'
|
content: 'figcaption image'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const content = props.initialContent
|
||||||
const editor = createTiptapEditor(() => ({
|
const editor = createTiptapEditor(() => ({
|
||||||
element: editorElRef.current,
|
element: editorElRef.current,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
|
@ -86,11 +106,25 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
Link.configure({
|
Link.configure({
|
||||||
openOnClick: false
|
openOnClick: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
CharacterCount.configure({
|
||||||
|
limit: MAX_DESCRIPTION_LIMIT
|
||||||
|
}),
|
||||||
Blockquote.configure({
|
Blockquote.configure({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: styles.blockQuote
|
class: styles.blockQuote
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'textBubbleMenu',
|
||||||
|
element: textBubbleMenuRef.current,
|
||||||
|
shouldShow: ({ view, state }) => {
|
||||||
|
if (!props.onlyBubbleControls) return
|
||||||
|
const { selection } = state
|
||||||
|
const { empty } = selection
|
||||||
|
return view.hasFocus() && !empty
|
||||||
|
}
|
||||||
|
}),
|
||||||
ImageFigure,
|
ImageFigure,
|
||||||
Image,
|
Image,
|
||||||
Figcaption,
|
Figcaption,
|
||||||
|
@ -99,7 +133,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
placeholder: props.placeholder
|
placeholder: props.placeholder
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
content: props.initialContent ?? null
|
content: content ?? null
|
||||||
}))
|
}))
|
||||||
|
|
||||||
setEditor(editor)
|
setEditor(editor)
|
||||||
|
@ -149,6 +183,9 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
props.onCancel()
|
||||||
|
}
|
||||||
editor().commands.clearContent(true)
|
editor().commands.clearContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +211,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
|
|
||||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
showModal('editorInsertLink')
|
showModal('simplifiedEditorInsertLink')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +220,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
editor().destroy()
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -192,18 +230,39 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInsertLink = () => !editor().state.selection.empty && showModal('editorInsertLink')
|
const handleInsertLink = () => !editor().state.selection.empty && showModal('simplifiedEditorInsertLink')
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (html()) {
|
||||||
|
setCounter(editor().storage.characterCount.characters())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxHeightStyle = {
|
||||||
|
overflow: 'auto',
|
||||||
|
'max-height': `${props.maxHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(el) => (wrapperEditorElRef.current = el)}
|
ref={(el) => (wrapperEditorElRef.current = el)}
|
||||||
class={clsx(styles.SimplifiedEditor, {
|
class={clsx(styles.SimplifiedEditor, {
|
||||||
[styles.smallHeight]: props.smallHeight,
|
[styles.smallHeight]: props.smallHeight,
|
||||||
[styles.isFocused]: isFocused() || !isEmpty()
|
[styles.minimal]: props.variant === 'minimal',
|
||||||
|
[styles.bordered]: props.variant === 'bordered',
|
||||||
|
[styles.isFocused]: isFocused() || !isEmpty(),
|
||||||
|
[styles.labelVisible]: props.label && counter() > 0
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div ref={(el) => (editorElRef.current = el)} />
|
<Show when={props.maxLength && editor()}>
|
||||||
<div class={styles.controls}>
|
<div class={styles.limit}>{MAX_DESCRIPTION_LIMIT - counter()}</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.label && counter() > 0}>
|
||||||
|
<div class={styles.label}>{props.label}</div>
|
||||||
|
</Show>
|
||||||
|
<div style={props.maxHeight && maxHeightStyle} ref={(el) => (editorElRef.current = el)} />
|
||||||
|
<Show when={!props.onlyBubbleControls}>
|
||||||
|
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
<Popover content={t('Bold')}>
|
<Popover content={t('Bold')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
|
@ -261,7 +320,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showModal('uploadImage')}
|
onClick={() => showModal('simplifiedEditorUploadImage')}
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-dd-full" />
|
<Icon name="editor-image-dd-full" />
|
||||||
|
@ -272,7 +331,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.onChange}>
|
<Show when={!props.onChange}>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<Button value={t('Cancel')} variant="secondary" disabled={isEmpty()} onClick={handleClear} />
|
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||||
<Button
|
<Button
|
||||||
value={props.submitButtonText ?? t('Send')}
|
value={props.submitButtonText ?? t('Send')}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
@ -282,17 +341,30 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Modal variant="narrow" name="editorInsertLink">
|
</Show>
|
||||||
|
<Portal>
|
||||||
|
<Modal variant="narrow" name="simplifiedEditorInsertLink">
|
||||||
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</Portal>
|
||||||
<Show when={props.imageEnabled}>
|
<Show when={props.imageEnabled}>
|
||||||
<Modal variant="narrow" name="uploadImage">
|
<Portal>
|
||||||
|
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
||||||
<UploadModalContent
|
<UploadModalContent
|
||||||
onClose={(value) => {
|
onClose={(value) => {
|
||||||
renderImage(value)
|
renderImage(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.onlyBubbleControls}>
|
||||||
|
<TextBubbleMenu
|
||||||
|
shouldShow={true}
|
||||||
|
isCommonMarkup={true}
|
||||||
|
editor={editor()}
|
||||||
|
ref={(el) => (textBubbleMenuRef.current = el)}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
background: var(--editor-bubble-menu-background);
|
background: var(--editor-bubble-menu-background);
|
||||||
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
||||||
|
|
||||||
|
&.growWidth {
|
||||||
|
min-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
.bubbleMenuButton {
|
.bubbleMenuButton {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -86,4 +90,8 @@
|
||||||
height: 0;
|
height: 0;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noWrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Switch, Match, createSignal, Show, onMount, onCleanup } from 'solid-js'
|
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
|
||||||
import type { Editor } from '@tiptap/core'
|
import type { Editor } from '@tiptap/core'
|
||||||
import styles from './TextBubbleMenu.module.scss'
|
import styles from './TextBubbleMenu.module.scss'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
@ -7,11 +7,13 @@ import { createEditorTransaction } from 'solid-tiptap'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Popover } from '../../_shared/Popover'
|
import { Popover } from '../../_shared/Popover'
|
||||||
import { InsertLinkForm } from '../InsertLinkForm'
|
import { InsertLinkForm } from '../InsertLinkForm'
|
||||||
|
import SimplifiedEditor from '../SimplifiedEditor'
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type BubbleMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
isCommonMarkup: boolean
|
isCommonMarkup: boolean
|
||||||
ref: (el: HTMLDivElement) => void
|
ref: (el: HTMLDivElement) => void
|
||||||
|
shouldShow: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
|
@ -20,24 +22,35 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const isActive = (name: string, attributes?: unknown) =>
|
const isActive = (name: string, attributes?: unknown) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => props.editor,
|
() => props.editor,
|
||||||
(editor) => {
|
(editor) => editor && editor.isActive(name, attributes)
|
||||||
return editor && editor.isActive(name, attributes)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||||
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
||||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
||||||
|
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
|
||||||
|
const [footNote, setFootNote] = createSignal<string>()
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!props.shouldShow) {
|
||||||
|
setFootNote()
|
||||||
|
setFootnoteEditorOpen(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const isBold = isActive('bold')
|
const isBold = isActive('bold')
|
||||||
const isItalic = isActive('italic')
|
const isItalic = isActive('italic')
|
||||||
const isH1 = isActive('heading', { level: 2 })
|
const isH1 = isActive('heading', { level: 2 })
|
||||||
const isH2 = isActive('heading', { level: 3 })
|
const isH2 = isActive('heading', { level: 3 })
|
||||||
const isH3 = isActive('heading', { level: 4 })
|
const isH3 = isActive('heading', { level: 4 })
|
||||||
const isBlockQuote = isActive('blockquote')
|
const isQuote = isActive('blockquote', { 'data-type': 'quote' })
|
||||||
|
const isPunchLine = isActive('blockquote', { 'data-type': 'punchline' })
|
||||||
const isOrderedList = isActive('isOrderedList')
|
const isOrderedList = isActive('isOrderedList')
|
||||||
const isBulletList = isActive('isBulletList')
|
const isBulletList = isActive('isBulletList')
|
||||||
const isLink = isActive('link')
|
const isLink = isActive('link')
|
||||||
const isHighlight = isActive('highlight')
|
const isHighlight = isActive('highlight')
|
||||||
|
const isFootnote = isActive('footnote')
|
||||||
|
const isIncut = isActive('article')
|
||||||
|
|
||||||
const toggleTextSizePopup = () => {
|
const toggleTextSizePopup = () => {
|
||||||
if (listBubbleOpen()) {
|
if (listBubbleOpen()) {
|
||||||
|
@ -58,6 +71,47 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateCurrentFootnoteValue = createEditorTransaction(
|
||||||
|
() => props.editor,
|
||||||
|
(ed) => {
|
||||||
|
if (!isFootnote()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const value = ed.getAttributes('footnote').value
|
||||||
|
setFootNote(value)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleAddFootnote = (footnote) => {
|
||||||
|
if (footNote()) {
|
||||||
|
props.editor.chain().focus().updateFootnote(footnote).run()
|
||||||
|
} else {
|
||||||
|
props.editor.chain().focus().setFootnote({ value: footnote }).run()
|
||||||
|
}
|
||||||
|
setFootNote()
|
||||||
|
setFootnoteEditorOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenFootnoteEditor = () => {
|
||||||
|
updateCurrentFootnoteValue()
|
||||||
|
setFootnoteEditorOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetPunchline = () => {
|
||||||
|
if (isPunchLine()) {
|
||||||
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||||
|
}
|
||||||
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}
|
||||||
|
const handleSetQuote = () => {
|
||||||
|
if (isQuote()) {
|
||||||
|
props.editor.chain().focus().toggleBlockquote('quote').run()
|
||||||
|
}
|
||||||
|
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
||||||
|
toggleTextSizePopup()
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
@ -67,12 +121,27 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={props.ref} class={styles.TextBubbleMenu}>
|
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={linkEditorOpen()}>
|
<Match when={linkEditorOpen()}>
|
||||||
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!linkEditorOpen()}>
|
<Match when={footnoteEditorOpen()}>
|
||||||
|
<SimplifiedEditor
|
||||||
|
maxHeight={180}
|
||||||
|
controlsAlwaysVisible={true}
|
||||||
|
imageEnabled={true}
|
||||||
|
placeholder={t('Enter footnote text')}
|
||||||
|
onSubmit={(value) => handleAddFootnote(value)}
|
||||||
|
variant={'bordered'}
|
||||||
|
initialContent={footNote()}
|
||||||
|
onCancel={() => {
|
||||||
|
setFootnoteEditorOpen(false)
|
||||||
|
}}
|
||||||
|
submitButtonText={t('Send')}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!linkEditorOpen() || !footnoteEditorOpen()}>
|
||||||
<>
|
<>
|
||||||
<Show when={!props.isCommonMarkup}>
|
<Show when={!props.isCommonMarkup}>
|
||||||
<>
|
<>
|
||||||
|
@ -151,12 +220,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
[styles.bubbleMenuButtonActive]: isQuote()
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={handleSetPunchline}
|
||||||
props.editor.chain().focus().toggleBlockquote('quote').run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="editor-blockquote" />
|
<Icon name="editor-blockquote" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -168,12 +234,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
[styles.bubbleMenuButtonActive]: isPunchLine()
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={handleSetQuote}
|
||||||
props.editor.chain().focus().toggleBlockquote('punchline').run()
|
|
||||||
toggleTextSizePopup()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Icon name="editor-quote" />
|
<Icon name="editor-quote" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -188,7 +251,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={clsx(styles.bubbleMenuButton, {
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
[styles.bubbleMenuButtonActive]: isBlockQuote()
|
[styles.bubbleMenuButtonActive]: isIncut()
|
||||||
})}
|
})}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().toggleArticle().run()
|
props.editor.chain().focus().toggleArticle().run()
|
||||||
|
@ -252,7 +315,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
</Popover>
|
</Popover>
|
||||||
<div class={styles.delimiter} />
|
<div class={styles.delimiter} />
|
||||||
</Show>
|
</Show>
|
||||||
<Popover content={t('Add url')}>
|
<Popover content={<div class={styles.noWrap}>{t('Add url')}</div>}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
|
@ -270,7 +333,14 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
<>
|
<>
|
||||||
<Popover content={t('Insert footnote')}>
|
<Popover content={t('Insert footnote')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<button ref={triggerRef} type="button" class={styles.bubbleMenuButton}>
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isFootnote()
|
||||||
|
})}
|
||||||
|
onClick={handleOpenFootnoteEditor}
|
||||||
|
>
|
||||||
<Icon name="editor-footnote" />
|
<Icon name="editor-footnote" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,7 +4,7 @@ declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
Article: {
|
Article: {
|
||||||
toggleArticle: () => ReturnType
|
toggleArticle: () => ReturnType
|
||||||
setArticleFloat: (float: null | 'left' | 'half-left' | 'right' | 'half-right') => ReturnType
|
setArticleFloat: (float: null | 'half-left' | 'half-right') => ReturnType
|
||||||
setArticleBg: (bg: null | string) => ReturnType
|
setArticleBg: (bg: null | string) => ReturnType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ declare module '@tiptap/core' {
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: 'article',
|
name: 'article',
|
||||||
|
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
'data-type': 'incut'
|
'data-type': 'incut'
|
||||||
|
|
|
@ -14,10 +14,10 @@ declare module '@tiptap/core' {
|
||||||
export const CustomBlockquote = Blockquote.extend({
|
export const CustomBlockquote = Blockquote.extend({
|
||||||
name: 'blockquote',
|
name: 'blockquote',
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
HTMLAttributes: {},
|
HTMLAttributes: {}
|
||||||
group: 'block',
|
|
||||||
content: 'block+'
|
|
||||||
},
|
},
|
||||||
|
group: 'block',
|
||||||
|
content: 'block+',
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
'data-float': {
|
'data-float': {
|
||||||
|
|
97
src/components/Editor/extensions/Footnote.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
Footnote: {
|
||||||
|
setFootnote: (options: { value: string }) => ReturnType
|
||||||
|
updateFootnote: (options: { value: string }) => ReturnType
|
||||||
|
deleteFootnote: () => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Footnote = Node.create({
|
||||||
|
name: 'footnote',
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group: 'inline',
|
||||||
|
content: 'text*',
|
||||||
|
inline: true,
|
||||||
|
isolating: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.dataset.value || null,
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
return {
|
||||||
|
'data-value': attributes.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'footnote'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['footnote', mergeAttributes(HTMLAttributes), 0]
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setFootnote:
|
||||||
|
(attributes) =>
|
||||||
|
({ tr, state }) => {
|
||||||
|
const { selection } = state
|
||||||
|
const position = selection.$to.pos
|
||||||
|
const node = this.type.create(attributes)
|
||||||
|
tr.insert(position, node)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
updateFootnote:
|
||||||
|
(newValue) =>
|
||||||
|
({ tr, state }) => {
|
||||||
|
const { selection } = state
|
||||||
|
const { $from, $to } = selection
|
||||||
|
|
||||||
|
if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') {
|
||||||
|
const node = $from.parent.type.name === 'footnote' ? $from.parent : $to.parent
|
||||||
|
const pos = $from.parent.type.name === 'footnote' ? $from.pos - 1 : $to.pos - 1
|
||||||
|
|
||||||
|
const newNode = node.type.create({ value: newValue })
|
||||||
|
tr.setNodeMarkup(pos, null, newNode.attrs)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
deleteFootnote:
|
||||||
|
() =>
|
||||||
|
({ tr, state }) => {
|
||||||
|
const { selection } = state
|
||||||
|
const { $from, $to } = selection
|
||||||
|
|
||||||
|
if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') {
|
||||||
|
const startPos = $from.start($from.depth)
|
||||||
|
const endPos = $to.end($to.depth)
|
||||||
|
tr.delete(startPos, endPos)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -71,6 +71,7 @@
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -180,12 +181,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardLead {
|
.shoutCardDescription {
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
color: var(--secondary-color);
|
color: var(--default-color);
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.3;
|
|
||||||
margin-bottom: 1.4rem;
|
margin-bottom: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,8 +398,10 @@
|
||||||
padding: 0 2.4rem;
|
padding: 0 2.4rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(xl) {
|
||||||
padding-top: 100%;
|
aspect-ratio: auto;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.swiper-slide {
|
&.swiper-slide {
|
||||||
|
@ -451,6 +452,12 @@
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
padding: 2.4rem;
|
padding: 2.4rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(xl) {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shoutCardCover {
|
.shoutCardCover {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { clsx } from 'clsx'
|
||||||
import { CardTopic } from './CardTopic'
|
import { CardTopic } from './CardTopic'
|
||||||
import { ShoutRatingControl } from '../Article/ShoutRatingControl'
|
import { ShoutRatingControl } from '../Article/ShoutRatingControl'
|
||||||
import { getShareUrl, SharePopup } from '../Article/SharePopup'
|
import { getShareUrl, SharePopup } from '../Article/SharePopup'
|
||||||
import stylesHeader from '../Nav/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import { FeedArticlePopup } from './FeedArticlePopup'
|
import { FeedArticlePopup } from './FeedArticlePopup'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -164,10 +164,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<Show when={props.article.lead}>
|
|
||||||
<div class={styles.shoutCardLead}>{props.article.lead}</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
<Show when={!props.settings?.noauthor || !props.settings?.nodate}>
|
||||||
|
@ -196,7 +192,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={props.article.description}>
|
||||||
|
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
||||||
|
</Show>
|
||||||
<Show when={props.settings?.isFeedMode}>
|
<Show when={props.settings?.isFeedMode}>
|
||||||
<Show when={!props.settings?.noimage && props.article.cover}>
|
<Show when={!props.settings?.noimage && props.article.cover}>
|
||||||
<div class={styles.shoutCardCoverContainer}>
|
<div class={styles.shoutCardCoverContainer}>
|
||||||
|
|
|
@ -141,9 +141,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.authInfo {
|
.authInfo {
|
||||||
min-height: 5em;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
|
margin-top: -2em;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
.warn {
|
.warn {
|
||||||
color: #a00;
|
color: #a00;
|
||||||
|
@ -158,6 +159,7 @@
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,11 +168,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.validationError {
|
.validationError {
|
||||||
position: relative;
|
position: absolute;
|
||||||
top: -8px;
|
top: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
margin-bottom: 8px;
|
margin-top: 0.3em;
|
||||||
|
|
||||||
&.registerPassword {
|
&.registerPassword {
|
||||||
margin-bottom: -32px;
|
margin-bottom: -32px;
|
||||||
|
|
|
@ -80,31 +80,7 @@ export const ForgotPasswordForm = () => {
|
||||||
<div>
|
<div>
|
||||||
<h4>{t('Forgot password?')}</h4>
|
<h4>{t('Forgot password?')}</h4>
|
||||||
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
<div class={styles.authSubtitle}>{t('Everything is ok, please give us your email address')}</div>
|
||||||
<Show when={submitError()}>
|
|
||||||
<div class={styles.authInfo}>
|
|
||||||
<ul>
|
|
||||||
<li class={styles.warn}>{submitError()}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={isUserNotFount()}>
|
|
||||||
<div class={styles.authSubtitle}>
|
|
||||||
{/*TODO: text*/}
|
|
||||||
{t("We can't find you, check email or")}{' '}
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
changeSearchParam('mode', 'register')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('register')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={validationErrors().email}>
|
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
|
||||||
</Show>
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().email
|
'pretty-form__item--error': validationErrors().email
|
||||||
|
@ -123,6 +99,33 @@ export const ForgotPasswordForm = () => {
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={submitError()}>
|
||||||
|
<div class={styles.authInfo}>
|
||||||
|
<ul>
|
||||||
|
<li class={styles.warn}>{submitError()}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isUserNotFount()}>
|
||||||
|
<div class={styles.authSubtitle}>
|
||||||
|
{/*TODO: text*/}
|
||||||
|
{t("We can't find you, check email or")}{' '}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
changeSearchParam('mode', 'register')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('register')}
|
||||||
|
</a>
|
||||||
|
<Show when={validationErrors().email}>
|
||||||
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
{isSubmitting() ? '...' : t('Restore password')}
|
{isSubmitting() ? '...' : t('Restore password')}
|
||||||
|
|
|
@ -160,10 +160,10 @@ export const LoginForm = () => {
|
||||||
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().email}>
|
<Show when={validationErrors().email}>
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
|
@ -186,11 +186,11 @@ export const LoginForm = () => {
|
||||||
>
|
>
|
||||||
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={validationErrors().password}>
|
<Show when={validationErrors().password}>
|
||||||
<div class={styles.validationError}>{validationErrors().password}</div>
|
<div class={styles.validationError}>{validationErrors().password}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
{isSubmitting() ? '...' : t('Enter')}
|
{isSubmitting() ? '...' : t('Enter')}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { register } from '../../../stores/auth'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { validateEmail } from '../../../utils/validateEmail'
|
import { validateEmail } from '../../../utils/validateEmail'
|
||||||
import { AuthModalHeader } from './AuthModalHeader'
|
import { AuthModalHeader } from './AuthModalHeader'
|
||||||
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
fullName: string
|
fullName: string
|
||||||
|
@ -22,6 +23,10 @@ type FormFields = {
|
||||||
|
|
||||||
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setEmail(newEmail)
|
||||||
|
}
|
||||||
|
|
||||||
export const RegisterForm = () => {
|
export const RegisterForm = () => {
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -31,16 +36,12 @@ export const RegisterForm = () => {
|
||||||
const [fullName, setFullName] = createSignal('')
|
const [fullName, setFullName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
const [showPassword, setShowPassword] = createSignal(false)
|
||||||
const [isSuccess, setIsSuccess] = createSignal(false)
|
const [isSuccess, setIsSuccess] = createSignal(false)
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
|
|
||||||
const handleEmailInput = (newEmail: string) => {
|
|
||||||
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
|
||||||
setEmail(newEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEmailBlur = () => {
|
const handleEmailBlur = () => {
|
||||||
if (validateEmail(email())) {
|
if (validateEmail(email())) {
|
||||||
checkEmail(email())
|
checkEmail(email())
|
||||||
|
@ -65,23 +66,25 @@ export const RegisterForm = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePasswordInput = (newPassword: string) => {
|
const handlePasswordInput = (newPassword: string) => {
|
||||||
const passwordError = isValidPassword(newPassword)
|
|
||||||
if (passwordError) {
|
|
||||||
setValidationErrors((errors) => ({ ...errors, password: passwordError }))
|
|
||||||
} else {
|
|
||||||
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
|
||||||
}
|
|
||||||
setPassword(newPassword)
|
setPassword(newPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNameInput = (newPasswordCopy: string) => {
|
const handleNameInput = (newPasswordCopy: string) => {
|
||||||
setValidationErrors(({ fullName: _notNeeded, ...rest }) => rest)
|
|
||||||
setFullName(newPasswordCopy)
|
setFullName(newPasswordCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
const passwordError = isValidPassword(password())
|
||||||
|
if (passwordError) {
|
||||||
|
setValidationErrors((errors) => ({ ...errors, password: passwordError }))
|
||||||
|
} else {
|
||||||
|
setValidationErrors(({ password: _notNeeded, ...rest }) => rest)
|
||||||
|
}
|
||||||
|
setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
|
||||||
|
setValidationErrors(({ fullName: _notNeeded, ...rest }) => rest)
|
||||||
|
|
||||||
setSubmitError('')
|
setSubmitError('')
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
@ -164,10 +167,11 @@ export const RegisterForm = () => {
|
||||||
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="name">{t('Full name')}</label>
|
<label for="name">{t('Full name')}</label>
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().fullName}>
|
<Show when={validationErrors().fullName}>
|
||||||
<div class={styles.validationError}>{validationErrors().fullName}</div>
|
<div class={styles.validationError}>{validationErrors().fullName}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().email
|
'pretty-form__item--error': validationErrors().email
|
||||||
|
@ -184,7 +188,6 @@ export const RegisterForm = () => {
|
||||||
onBlur={handleEmailBlur}
|
onBlur={handleEmailBlur}
|
||||||
/>
|
/>
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
</div>
|
|
||||||
<Show when={validationErrors().email}>
|
<Show when={validationErrors().email}>
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -202,6 +205,8 @@ export const RegisterForm = () => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().password
|
'pretty-form__item--error': validationErrors().password
|
||||||
|
@ -211,17 +216,24 @@ export const RegisterForm = () => {
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
type="password"
|
type={showPassword() ? 'text' : 'password'}
|
||||||
placeholder={t('Password')}
|
placeholder={t('Password')}
|
||||||
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
onInput={(event) => handlePasswordInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="password">{t('Password')}</label>
|
<label for="password">{t('Password')}</label>
|
||||||
</div>
|
<button
|
||||||
|
type="button"
|
||||||
|
class={styles.passwordToggle}
|
||||||
|
onClick={() => setShowPassword(!showPassword())}
|
||||||
|
>
|
||||||
|
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
||||||
|
</button>
|
||||||
<Show when={validationErrors().password}>
|
<Show when={validationErrors().password}>
|
||||||
<div class={clsx(styles.registerPassword, styles.validationError)}>
|
<div class={clsx(styles.registerPassword, styles.validationError)}>
|
||||||
{validationErrors().password}
|
{validationErrors().password}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
<button class={clsx('button', styles.submitButton)} disabled={isSubmitting()} type="submit">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
margin-bottom: 2.2rem;
|
margin-bottom: 2.2rem;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 10;
|
z-index: 10000;
|
||||||
|
|
||||||
.wide-container {
|
.wide-container {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
@ -104,6 +104,11 @@
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.usernavEditor) {
|
||||||
|
flex: 0 0 40% !important;
|
||||||
|
max-width: 400px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainNavigationWrapper {
|
.mainNavigationWrapper {
|
|
@ -2,21 +2,21 @@ import { Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js'
|
||||||
import { getPagePath, redirectPage } from '@nanostores/router'
|
import { getPagePath, redirectPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
import { Modal } from './Modal'
|
import { Modal } from '../Modal'
|
||||||
import { AuthModal } from './AuthModal'
|
import { AuthModal } from '../AuthModal'
|
||||||
import { HeaderAuth } from './HeaderAuth'
|
import { HeaderAuth } from '../HeaderAuth'
|
||||||
import { ConfirmModal } from './ConfirmModal'
|
import { ConfirmModal } from '../ConfirmModal'
|
||||||
import { getShareUrl, SharePopup } from '../Article/SharePopup'
|
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||||
import { Snackbar } from './Snackbar'
|
import { Snackbar } from '../Snackbar'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
import { useModalStore } from '../../stores/ui'
|
import { useModalStore } from '../../../stores/ui'
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../../stores/router'
|
||||||
|
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../../utils/meta'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
|
|
1
src/components/Nav/Header/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { Header } from './Header'
|
|
@ -1,4 +1,4 @@
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header/Header.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
@ -107,7 +107,10 @@ export const HeaderAuth = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
|
<div
|
||||||
|
class={clsx('col-sm-6 col-lg-7', styles.usernav)}
|
||||||
|
classList={{ [styles.usernavEditor]: showSaveButton() }}
|
||||||
|
>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={showCreatePostButton()}>
|
<Show when={showCreatePostButton()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 100;
|
z-index: 10002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
|
|
@ -1,27 +1,51 @@
|
||||||
.TableOfContentsFixedWrapper {
|
.TableOfContentsFixedWrapper {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 281px;
|
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&:not(.TableOfContentsFixedWrapperLefted) {
|
||||||
|
margin-top: -0.2em;
|
||||||
|
|
||||||
|
.TableOfContentsPrimaryButton {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsFixedWrapperLefted {
|
.TableOfContentsFixedWrapperLefted {
|
||||||
|
margin-top: -2em;
|
||||||
right: auto;
|
right: auto;
|
||||||
left: 70px;
|
left: 70px;
|
||||||
|
|
||||||
|
.TableOfContentsPrimaryButton {
|
||||||
|
left: auto;
|
||||||
|
right: 40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.TableOfContentsPrimaryButtonLefted {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsContainer {
|
.TableOfContentsContainer {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 150px;
|
top: 100px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: calc(100vh - 120px);
|
||||||
padding: 20px;
|
overflow: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
.TableOfContentsFixedWrapperLefted & {
|
||||||
|
height: calc(100vh - 250px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsHeader {
|
.TableOfContentsHeader {
|
||||||
|
@ -41,27 +65,20 @@
|
||||||
|
|
||||||
.TableOfContentsPrimaryButton {
|
.TableOfContentsPrimaryButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 0;
|
||||||
top: 10px;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 0 1px 1px rgb(0 0 0 / 30%);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsPrimaryButtonLefted {
|
|
||||||
right: auto;
|
|
||||||
left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TableOfContentsHeadingsList {
|
.TableOfContentsHeadingsList {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -99,7 +116,3 @@
|
||||||
.TableOfContentsHeadingsItemH4 {
|
.TableOfContentsHeadingsItemH4 {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsIconRotated {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { For, Show, createSignal, createEffect, on, onMount } from 'solid-js'
|
import { For, Show, createSignal, createEffect, on } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
||||||
|
@ -99,17 +99,16 @@ export const TableOfContents = (props: Props) => {
|
||||||
})}
|
})}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
toggleIsVisible()
|
toggleIsVisible()
|
||||||
}}
|
}}
|
||||||
|
title={isVisible() ? t('Hide table of contents') : t('Show table of contents')}
|
||||||
>
|
>
|
||||||
<Show when={isVisible()} fallback={<Icon name="show-table-of-contents" class={'icon'} />}>
|
<Show when={isVisible()} fallback={<Icon name="show-table-of-contents" class={'icon'} />}>
|
||||||
<Icon
|
{props.variant === 'editor' ? (
|
||||||
name="hide-table-of-contents"
|
<Icon name="hide-table-of-contents" class="icon" />
|
||||||
class={clsx('icon', {
|
) : (
|
||||||
[styles.TableOfContentsIconRotated]: props.variant === 'editor'
|
<Icon name="hide-table-of-contents-2" class="icon" />
|
||||||
})}
|
)}
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
|
.authorPage {
|
||||||
|
:global(.view-switcher) {
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupControls {
|
||||||
|
margin-bottom: 2em !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ratingContainer {
|
.ratingContainer {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratingControl {
|
.ratingControl {
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -21,57 +34,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userpic {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 0 2px #fff;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: -1.2rem;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribers {
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-block;
|
|
||||||
margin: -0.4rem 2em 0 0;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribersCounter {
|
|
||||||
@include font-size(1rem);
|
|
||||||
|
|
||||||
background: #fff;
|
|
||||||
border: 2px solid #000;
|
|
||||||
border-radius: 100%;
|
|
||||||
font-weight: bold;
|
|
||||||
height: 32px;
|
|
||||||
line-height: 30px;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
width: 32px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscribersList {
|
|
||||||
max-height: 15em;
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.subscriber {
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
transition: background 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingWrapper {
|
.loadingWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 40vh;
|
min-height: 40vh;
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffe
|
||||||
import type { Author, Shout, Topic } from '../../../graphql/types.gen'
|
import type { Author, Shout, Topic } from '../../../graphql/types.gen'
|
||||||
import { Row1 } from '../../Feed/Row1'
|
import { Row1 } from '../../Feed/Row1'
|
||||||
import { Row2 } from '../../Feed/Row2'
|
import { Row2 } from '../../Feed/Row2'
|
||||||
import { AuthorFull } from '../../Author/Full'
|
import { Row3 } from '../../Feed/Row3'
|
||||||
|
|
||||||
import { useAuthorsStore } from '../../../stores/zine/authors'
|
import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
|
@ -12,15 +11,12 @@ import { splitToPages } from '../../../utils/splitToPages'
|
||||||
import styles from './Author.module.scss'
|
import styles from './Author.module.scss'
|
||||||
import stylesArticle from '../../Article/Article.module.scss'
|
import stylesArticle from '../../Article/Article.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Userpic } from '../../Author/Userpic'
|
|
||||||
import { Popup } from '../../_shared/Popup'
|
|
||||||
import { AuthorCard } from '../../Author/AuthorCard'
|
import { AuthorCard } from '../../Author/AuthorCard'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
import { apiClient } from '../../../utils/apiClient'
|
||||||
import { Comment } from '../../Article/Comment'
|
import { Comment } from '../../Article/Comment'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||||
import { TopicCard } from '../../Topic/Card'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { Loading } from '../../_shared/Loading'
|
|
||||||
|
|
||||||
type AuthorProps = {
|
type AuthorProps = {
|
||||||
shouts: Shout[]
|
shouts: Shout[]
|
||||||
|
@ -29,24 +25,12 @@ type AuthorProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthorPageSearchParams = {
|
export type AuthorPageSearchParams = {
|
||||||
by:
|
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'about' | 'popular'
|
||||||
| ''
|
|
||||||
| 'viewed'
|
|
||||||
| 'rating'
|
|
||||||
| 'commented'
|
|
||||||
| 'recent'
|
|
||||||
| 'subscriptions'
|
|
||||||
| 'followers'
|
|
||||||
| 'about'
|
|
||||||
| 'popular'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 12
|
export const PRERENDERED_ARTICLES_COUNT = 12
|
||||||
const LOAD_MORE_PAGE_SIZE = 9
|
const LOAD_MORE_PAGE_SIZE = 9
|
||||||
|
|
||||||
function isAuthor(value: Author | Topic): value is Author {
|
|
||||||
return 'name' in value
|
|
||||||
}
|
|
||||||
export const AuthorView = (props: AuthorProps) => {
|
export const AuthorView = (props: AuthorProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
|
@ -58,7 +42,6 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [followers, setFollowers] = createSignal<Author[]>([])
|
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||||
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>([])
|
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>([])
|
||||||
const [isLoaded, setIsLoaded] = createSignal<boolean>()
|
|
||||||
|
|
||||||
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
|
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
|
||||||
try {
|
try {
|
||||||
|
@ -76,6 +59,7 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
hideModal()
|
||||||
try {
|
try {
|
||||||
const userSubscribers = await apiClient.getAuthorFollowers({ slug: props.authorSlug })
|
const userSubscribers = await apiClient.getAuthorFollowers({ slug: props.authorSlug })
|
||||||
setFollowers(userSubscribers)
|
setFollowers(userSubscribers)
|
||||||
|
@ -88,6 +72,8 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
|
||||||
await loadMore()
|
await loadMore()
|
||||||
}
|
}
|
||||||
|
const { authors, topics } = await fetchSubscriptions()
|
||||||
|
setSubscriptions([...authors, ...topics])
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
|
@ -117,13 +103,6 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
const [commented, setCommented] = createSignal([])
|
const [commented, setCommented] = createSignal([])
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (searchParams().by === 'subscriptions') {
|
|
||||||
setIsLoaded(false)
|
|
||||||
const { authors, topics } = await fetchSubscriptions()
|
|
||||||
setSubscriptions([...authors, ...topics])
|
|
||||||
setIsLoaded(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchParams().by === 'commented') {
|
if (searchParams().by === 'commented') {
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
|
@ -136,12 +115,17 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<div class="author-page">
|
<div class={styles.authorPage}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<Show when={author()}>
|
<Show when={author()}>
|
||||||
<AuthorFull author={author()} />
|
<AuthorCard
|
||||||
|
author={author()}
|
||||||
|
isAuthorPage={true}
|
||||||
|
followers={followers()}
|
||||||
|
subscriptions={subscriptions()}
|
||||||
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="row group__controls">
|
<div class={clsx(styles.groupControls, 'row')}>
|
||||||
<div class="col-md-16">
|
<div class="col-md-16">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'rating' }}>
|
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'rating' }}>
|
||||||
|
@ -149,16 +133,6 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
{t('Publications')}
|
{t('Publications')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'followers' }}>
|
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'followers')}>
|
|
||||||
{t('Followers')}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'subscriptions' }}>
|
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'subscriptions')}>
|
|
||||||
{t('Subscriptions')}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'commented' }}>
|
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'commented' }}>
|
||||||
<button type="button" onClick={() => changeSearchParam('by', 'commented')}>
|
<button type="button" onClick={() => changeSearchParam('by', 'commented')}>
|
||||||
{t('Comments')}
|
{t('Comments')}
|
||||||
|
@ -172,45 +146,6 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class={clsx('col-md-8', styles.additionalControls)}>
|
<div class={clsx('col-md-8', styles.additionalControls)}>
|
||||||
<Popup
|
|
||||||
trigger={
|
|
||||||
<div class={styles.subscribers}>
|
|
||||||
<Switch>
|
|
||||||
<Match when={followers().length <= 3}>
|
|
||||||
<For each={followers().slice(0, 3)}>
|
|
||||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
|
||||||
</For>
|
|
||||||
</Match>
|
|
||||||
<Match when={followers().length > 3}>
|
|
||||||
<For each={followers().slice(0, 2)}>
|
|
||||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
|
||||||
</For>
|
|
||||||
<div class={clsx(styles.userpic, styles.subscribersCounter)}>
|
|
||||||
{followers().length}
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
variant="tiny"
|
|
||||||
>
|
|
||||||
<ul class={clsx('nodash', styles.subscribersList)}>
|
|
||||||
<For each={followers()}>
|
|
||||||
{(item: Author) => (
|
|
||||||
<li class={styles.subscriber}>
|
|
||||||
<AuthorCard
|
|
||||||
author={item}
|
|
||||||
isNowrap={true}
|
|
||||||
hideDescription={true}
|
|
||||||
hideFollow={true}
|
|
||||||
hasLink={true}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
|
||||||
</Popup>
|
|
||||||
|
|
||||||
<div class={styles.ratingContainer}>
|
<div class={styles.ratingContainer}>
|
||||||
{t('Karma')}
|
{t('Karma')}
|
||||||
<AuthorRatingControl author={props.author} class={styles.ratingControl} />
|
<AuthorRatingControl author={props.author} class={styles.ratingControl} />
|
||||||
|
@ -238,53 +173,21 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={searchParams().by === 'followers'}>
|
|
||||||
<div class="wide-container">
|
|
||||||
<div class="row">
|
|
||||||
<For each={followers()}>
|
|
||||||
{(follower: Author) => (
|
|
||||||
<div class="col-md-6 col-lg-4">
|
|
||||||
<AuthorCard author={follower} hideWriteButton={true} hasLink={true} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={searchParams().by === 'subscriptions'}>
|
|
||||||
<div class={clsx('wide-container', styles.subscriptions)}>
|
|
||||||
<div class="row position-relative">
|
|
||||||
<Show
|
|
||||||
when={isLoaded()}
|
|
||||||
fallback={
|
|
||||||
<div class={styles.loadingWrapper}>
|
|
||||||
<Loading />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={subscriptions()}>
|
|
||||||
{(subscription: Author | Topic) => (
|
|
||||||
<div class="col-md-20 col-lg-18">
|
|
||||||
{isAuthor(subscription) ? (
|
|
||||||
<div class={styles.authorContainer}>
|
|
||||||
<AuthorCard
|
|
||||||
author={subscription}
|
|
||||||
hideWriteButton={true}
|
|
||||||
hasLink={true}
|
|
||||||
isTextButton={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<TopicCard compact isTopicInRow showDescription topic={subscription} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Match>
|
|
||||||
<Match when={searchParams().by === 'rating'}>
|
<Match when={searchParams().by === 'rating'}>
|
||||||
|
<Show when={sortedArticles().length === 1}>
|
||||||
|
<Row1 article={sortedArticles()[0]} noAuthorLink={true} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={sortedArticles().length === 2}>
|
||||||
|
<Row2 articles={sortedArticles()} isEqual={true} noAuthorLink={true} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={sortedArticles().length === 3}>
|
||||||
|
<Row3 articles={sortedArticles()} noAuthorLink={true} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={sortedArticles().length > 3}>
|
||||||
<Row1 article={sortedArticles()[0]} noAuthorLink={true} />
|
<Row1 article={sortedArticles()[0]} noAuthorLink={true} />
|
||||||
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} noAuthorLink={true} />
|
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} noAuthorLink={true} />
|
||||||
<Row1 article={sortedArticles()[3]} noAuthorLink={true} />
|
<Row1 article={sortedArticles()[3]} noAuthorLink={true} />
|
||||||
|
@ -304,6 +207,7 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={isLoadMoreButtonVisible()}>
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
left: 2rem;
|
left: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: calc(100vh - 40px);
|
top: calc(100vh - 40px);
|
||||||
width: 2.8rem;
|
width: 2.8rem;
|
||||||
|
@ -151,7 +151,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(xl) {
|
||||||
left: 4rem;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -215,3 +215,11 @@
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapperTableOfContents {
|
||||||
|
position: fixed;
|
||||||
|
left: 40px;
|
||||||
|
top: 106px;
|
||||||
|
width: 240px;
|
||||||
|
padding-top: 100px;
|
||||||
|
}
|
||||||
|
|
|
@ -15,19 +15,21 @@ import { AudioUploader } from '../Editor/AudioUploader'
|
||||||
import { slugify } from '../../utils/slugify'
|
import { slugify } from '../../utils/slugify'
|
||||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||||
import { DropArea } from '../_shared/DropArea'
|
import { DropArea } from '../_shared/DropArea'
|
||||||
import { LayoutType, MediaItem, UploadedFile } from '../../pages/types'
|
import { LayoutType, MediaItem } from '../../pages/types'
|
||||||
import { clone } from '../../utils/clone'
|
import { clone } from '../../utils/clone'
|
||||||
import deepEqual from 'fast-deep-equal'
|
import deepEqual from 'fast-deep-equal'
|
||||||
import { AutoSaveNotice } from '../Editor/AutoSaveNotice'
|
import { AutoSaveNotice } from '../Editor/AutoSaveNotice'
|
||||||
import { PublishSettings } from './PublishSettings'
|
import { PublishSettings } from './PublishSettings'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
||||||
|
import { isDesktop } from '../../utils/media-query'
|
||||||
|
import { TableOfContents } from '../TableOfContents'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shout: Shout
|
shout: Shout
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAX_HEADER_LIMIT = 100
|
export const MAX_HEADER_LIMIT = 100
|
||||||
export const MAX_LEAD_LIMIT = 400
|
|
||||||
export const EMPTY_TOPIC: Topic = {
|
export const EMPTY_TOPIC: Topic = {
|
||||||
id: -1,
|
id: -1,
|
||||||
slug: ''
|
slug: ''
|
||||||
|
@ -64,6 +66,8 @@ export const EditView = (props: Props) => {
|
||||||
slug: props.shout.slug,
|
slug: props.shout.slug,
|
||||||
shoutId: props.shout.id,
|
shoutId: props.shout.id,
|
||||||
title: props.shout.title,
|
title: props.shout.title,
|
||||||
|
lead: props.shout.lead,
|
||||||
|
description: props.shout.description,
|
||||||
subtitle: props.shout.subtitle,
|
subtitle: props.shout.subtitle,
|
||||||
selectedTopics: shoutTopics,
|
selectedTopics: shoutTopics,
|
||||||
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
|
mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC,
|
||||||
|
@ -75,7 +79,6 @@ export const EditView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
const subtitleInput: { current: HTMLTextAreaElement } = { current: null }
|
||||||
const leadInput: { current: HTMLTextAreaElement } = { current: null }
|
|
||||||
|
|
||||||
const [prevForm, setPrevForm] = createStore<ShoutForm>(clone(form))
|
const [prevForm, setPrevForm] = createStore<ShoutForm>(clone(form))
|
||||||
const [saving, setSaving] = createSignal(false)
|
const [saving, setSaving] = createSignal(false)
|
||||||
|
@ -226,7 +229,6 @@ export const EditView = (props: Props) => {
|
||||||
}
|
}
|
||||||
const showLeadInput = () => {
|
const showLeadInput = () => {
|
||||||
setIsLeadVisible(true)
|
setIsLeadVisible(true)
|
||||||
leadInput.current.focus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -235,7 +237,6 @@ export const EditView = (props: Props) => {
|
||||||
<Title>{pageTitle()}</Title>
|
<Title>{pageTitle()}</Title>
|
||||||
<form>
|
<form>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<AutoSaveNotice active={saving()} />
|
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.scrollTopButton, {
|
class={clsx(styles.scrollTopButton, {
|
||||||
[styles.visible]: isScrolled()
|
[styles.visible]: isScrolled()
|
||||||
|
@ -246,6 +247,14 @@ export const EditView = (props: Props) => {
|
||||||
<span class={styles.scrollTopButtonLabel}>{t('Scroll up')}</span>
|
<span class={styles.scrollTopButtonLabel}>{t('Scroll up')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<AutoSaveNotice active={saving()} />
|
||||||
|
|
||||||
|
<div class={styles.wrapperTableOfContents}>
|
||||||
|
<Show when={isDesktop() && form.body}>
|
||||||
|
<TableOfContents variant="editor" parentSelector="#editorBody" body={form.body} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
|
||||||
<Show when={page().route === 'edit'}>
|
<Show when={page().route === 'edit'}>
|
||||||
|
@ -320,16 +329,13 @@ export const EditView = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={isLeadVisible()}>
|
<Show when={isLeadVisible()}>
|
||||||
<GrowingTextarea
|
<SimplifiedEditor
|
||||||
textAreaRef={(el) => {
|
variant="minimal"
|
||||||
leadInput.current = el
|
onlyBubbleControls={true}
|
||||||
}}
|
smallHeight={true}
|
||||||
allowEnterKey={true}
|
placeholder={t('A short introduction to keep the reader interested')}
|
||||||
value={(value) => setForm('lead', value)}
|
initialContent={form.lead}
|
||||||
class={styles.leadInput}
|
onChange={(value) => setForm('lead', value)}
|
||||||
placeholder={t('Description')}
|
|
||||||
initialValue={form.subtitle}
|
|
||||||
maxLength={MAX_LEAD_LIMIT}
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -55,13 +55,18 @@ export const FeedView = () => {
|
||||||
actions: { loadReactionsBy }
|
actions: { loadReactionsBy }
|
||||||
} = useReactions()
|
} = useReactions()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadMore()
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => page().route + searchParams().by,
|
() => page().route + searchParams().by,
|
||||||
() => {
|
() => {
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
loadMore()
|
loadMore()
|
||||||
}
|
},
|
||||||
|
{ defer: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const FourOuFourView = (_props) => {
|
||||||
return (
|
return (
|
||||||
<div class={styles.errorPageWrapper}>
|
<div class={styles.errorPageWrapper}>
|
||||||
<div class={styles.errorPage}>
|
<div class={styles.errorPage}>
|
||||||
<div class="container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-14 offset-md-6 col-lg-12">
|
<div class="col-md-14 offset-md-6 col-lg-12">
|
||||||
<a href="/" class="image-link">
|
<a href="/" class="image-link">
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useLocalize } from '../../../context/localize'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { Topic } from '../../../graphql/types.gen'
|
import { Topic } from '../../../graphql/types.gen'
|
||||||
import { apiClient } from '../../../utils/apiClient'
|
import { apiClient } from '../../../utils/apiClient'
|
||||||
import { EMPTY_TOPIC, MAX_LEAD_LIMIT } from '../Edit'
|
import { EMPTY_TOPIC } from '../Edit'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import stylesBeside from '../../Feed/Beside.module.scss'
|
import stylesBeside from '../../Feed/Beside.module.scss'
|
||||||
|
@ -19,6 +19,7 @@ import { router } from '../../../stores/router'
|
||||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
import { UploadedFile } from '../../../pages/types'
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -35,12 +36,13 @@ export const PublishSettings = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { user } = useSession()
|
const { user } = useSession()
|
||||||
|
|
||||||
const composeLead = () => {
|
const composeDescription = () => {
|
||||||
if (!props.form.lead) {
|
if (!props.form.description) {
|
||||||
const leadText = props.form.body.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
|
||||||
return shorten(leadText, MAX_LEAD_LIMIT).trim()
|
const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
||||||
|
return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim()
|
||||||
}
|
}
|
||||||
return props.form.lead
|
return props.form.description
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialData: Partial<ShoutForm> = {
|
const initialData: Partial<ShoutForm> = {
|
||||||
|
@ -49,7 +51,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
slug: props.form.slug,
|
slug: props.form.slug,
|
||||||
title: props.form.title,
|
title: props.form.title,
|
||||||
subtitle: props.form.subtitle,
|
subtitle: props.form.subtitle,
|
||||||
lead: composeLead()
|
description: composeDescription()
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -183,15 +185,15 @@ export const PublishSettings = (props: Props) => {
|
||||||
allowEnterKey={false}
|
allowEnterKey={false}
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
<GrowingTextarea
|
<SimplifiedEditor
|
||||||
class={styles.settingInput}
|
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
fieldName={t('Description')}
|
onlyBubbleControls={true}
|
||||||
|
smallHeight={true}
|
||||||
placeholder={t('Write a short introduction')}
|
placeholder={t('Write a short introduction')}
|
||||||
initialValue={`${settingsForm.lead}`}
|
label={t('Description')}
|
||||||
value={(value) => setSettingsForm('lead', value)}
|
initialContent={composeDescription()}
|
||||||
allowEnterKey={false}
|
onChange={(value) => setForm('description', value)}
|
||||||
maxLength={MAX_LEAD_LIMIT}
|
maxLength={MAX_DESCRIPTION_LIMIT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ type Props = {
|
||||||
type?: 'submit' | 'button'
|
type?: 'submit' | 'button'
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onClick?: () => void
|
onClick?: (event?: MouseEvent) => void
|
||||||
class?: string
|
class?: string
|
||||||
ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void)
|
ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import styles from './DarkModeToggle.module.scss'
|
||||||
import { Icon } from '../Icon'
|
import { Icon } from '../Icon'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { createSignal, onCleanup, onMount } from 'solid-js'
|
import { createSignal, onCleanup, onMount } from 'solid-js'
|
||||||
import { createPrefersDark } from '@solid-primitives/media'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
class?: string
|
class?: string
|
||||||
|
@ -14,7 +13,6 @@ const editorDarkModeAttr = document.documentElement.getAttribute('editorDarkMode
|
||||||
|
|
||||||
export const DarkModeToggle = (props: Props) => {
|
export const DarkModeToggle = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const prefersDark = createPrefersDark()
|
|
||||||
const [editorDarkMode, setEditorDarkMode] = createSignal(false)
|
const [editorDarkMode, setEditorDarkMode] = createSignal(false)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -27,9 +25,8 @@ export const DarkModeToggle = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editorDarkModeAttr && !editorDarkModeSelected) {
|
if (!editorDarkModeAttr && !editorDarkModeSelected) {
|
||||||
setEditorDarkMode(prefersDark())
|
localStorage.setItem('editorDarkMode', 'false')
|
||||||
localStorage.setItem('editorDarkMode', prefersDark() ? 'true' : 'false')
|
document.documentElement.dataset.editorDarkMode = 'false'
|
||||||
document.documentElement.dataset.editorDarkMode = prefersDark() ? 'true' : 'false'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.PanelWrapper {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
max-width: 430px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 14px;
|
||||||
|
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 2px solid black;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
input {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.PanelWrapperVisible {
|
||||||
|
display: flex;
|
||||||
|
}
|
35
src/components/_shared/FloatingPanel/FloatingPanel.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
|
import { Button } from '../Button'
|
||||||
|
|
||||||
|
import styles from './FloatingPanel.module.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isVisible: boolean
|
||||||
|
confirmTitle: string
|
||||||
|
confirmAction: () => void
|
||||||
|
declineTitle: string
|
||||||
|
declineAction: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(styles.PanelWrapper, {
|
||||||
|
[styles.PanelWrapperVisible]: props.isVisible
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="L"
|
||||||
|
variant="bordered"
|
||||||
|
value={props.declineTitle}
|
||||||
|
onClick={() => {
|
||||||
|
props.declineAction()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" size="L" value={props.confirmTitle} onClick={props.confirmAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -9,7 +9,7 @@
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
border: 2px solid var(--black-100);
|
border: 2px solid var(--black-100);
|
||||||
background: var(--white-500, #fff);
|
background: var(--white-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hasFieldName {
|
&.hasFieldName {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styles from './Popover.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: (setTooltipEl: (el: HTMLElement | null) => void) => JSX.Element
|
children: (setTooltipEl: (el: HTMLElement | null) => void) => JSX.Element
|
||||||
content: string
|
content: string | JSX.Element
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,8 @@ export const SolidSwiper = (props: Props) => {
|
||||||
() => {
|
() => {
|
||||||
mainSwipeRef.current?.swiper.update()
|
mainSwipeRef.current?.swiper.update()
|
||||||
thumbSwipeRef.current?.swiper.update()
|
thumbSwipeRef.current?.swiper.update()
|
||||||
}
|
},
|
||||||
|
{ defer: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,7 +96,7 @@ export const SolidSwiper = (props: Props) => {
|
||||||
const results: UploadedFile[] = []
|
const results: UploadedFile[] = []
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
const result = await handleFileUpload(file)
|
const result = await handleFileUpload(file)
|
||||||
results.push(result.url)
|
results.push(result)
|
||||||
}
|
}
|
||||||
props.onImagesAdd(composeMediaItems(results))
|
props.onImagesAdd(composeMediaItems(results))
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
@ -316,18 +317,18 @@ export const SolidSwiper = (props: Props) => {
|
||||||
type="text"
|
type="text"
|
||||||
class={clsx(styles.input, styles.title)}
|
class={clsx(styles.input, styles.title)}
|
||||||
placeholder={t('Enter image title')}
|
placeholder={t('Enter image title')}
|
||||||
value={props.images[slideIndex()].title}
|
value={props.images[slideIndex()]?.title}
|
||||||
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)}
|
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'title', event.target.value)}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class={styles.input}
|
class={styles.input}
|
||||||
placeholder={t('Specify the source and the name of the author')}
|
placeholder={t('Specify the source and the name of the author')}
|
||||||
value={props.images[slideIndex()].source}
|
value={props.images[slideIndex()]?.source}
|
||||||
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)}
|
onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)}
|
||||||
/>
|
/>
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
initialContent={props.images[slideIndex()].body}
|
initialContent={props.images[slideIndex()]?.body}
|
||||||
smallHeight={true}
|
smallHeight={true}
|
||||||
placeholder={t('Enter image description')}
|
placeholder={t('Enter image description')}
|
||||||
onChange={(value) => setSlideBody(value)}
|
onChange={(value) => setSlideBody(value)}
|
||||||
|
|
|
@ -93,8 +93,9 @@ $navigation-reserve: 32px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
background: rgba(#000, 0.3);
|
background: rgba(var(--default-color), 0.3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
z-index: 12;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -129,14 +130,14 @@ $navigation-reserve: 32px;
|
||||||
background: var(--placeholder-color-semi);
|
background: var(--placeholder-color-semi);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover .action {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover .action {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.editorMode {
|
&.editorMode {
|
||||||
.holder {
|
.holder {
|
||||||
|
|
|
@ -21,12 +21,13 @@ export type ShoutForm = {
|
||||||
slug: string
|
slug: string
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
|
lead?: string
|
||||||
|
description?: string
|
||||||
selectedTopics: Topic[]
|
selectedTopics: Topic[]
|
||||||
mainTopic?: Topic
|
mainTopic?: Topic
|
||||||
body: string
|
body: string
|
||||||
coverImageUrl: string
|
coverImageUrl: string
|
||||||
media?: string
|
media?: string
|
||||||
lead?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorContextType = {
|
type EditorContextType = {
|
||||||
|
@ -91,15 +92,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const [form, setForm] = createStore<ShoutForm>(null)
|
const [form, setForm] = createStore<ShoutForm>(null)
|
||||||
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
||||||
|
|
||||||
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
||||||
characters: 0,
|
characters: 0,
|
||||||
words: 0
|
words: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
|
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
|
||||||
const countWords = (value) => setWordCounter(value)
|
const countWords = (value) => setWordCounter(value)
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!form.title) {
|
if (!form.title) {
|
||||||
setFormErrors('title', t('Required'))
|
setFormErrors('title', t('Required'))
|
||||||
|
@ -136,6 +134,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
slug: formToUpdate.slug,
|
slug: formToUpdate.slug,
|
||||||
subtitle: formToUpdate.subtitle,
|
subtitle: formToUpdate.subtitle,
|
||||||
title: formToUpdate.title,
|
title: formToUpdate.title,
|
||||||
|
lead: formToUpdate.lead,
|
||||||
|
description: formToUpdate.description,
|
||||||
cover: formToUpdate.coverImageUrl,
|
cover: formToUpdate.coverImageUrl,
|
||||||
media: formToUpdate.media
|
media: formToUpdate.media
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,8 @@ export default gql`
|
||||||
slug
|
slug
|
||||||
title
|
title
|
||||||
subtitle
|
subtitle
|
||||||
|
lead
|
||||||
|
description
|
||||||
body
|
body
|
||||||
visibility
|
visibility
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ export default gql`
|
||||||
loadShout(slug: $slug, shout_id: $shoutId) {
|
loadShout(slug: $slug, shout_id: $shoutId) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
lead
|
||||||
|
description
|
||||||
visibility
|
visibility
|
||||||
subtitle
|
subtitle
|
||||||
slug
|
slug
|
||||||
|
|
|
@ -5,6 +5,8 @@ export default gql`
|
||||||
loadShouts(options: $options) {
|
loadShouts(options: $options) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
|
lead
|
||||||
|
description
|
||||||
subtitle
|
subtitle
|
||||||
slug
|
slug
|
||||||
layout
|
layout
|
||||||
|
|
|
@ -7,6 +7,10 @@ export default gql`
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
userpic
|
userpic
|
||||||
|
bio
|
||||||
|
stat {
|
||||||
|
shouts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -7,6 +7,10 @@ export default gql`
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
userpic
|
userpic
|
||||||
|
bio
|
||||||
|
stat {
|
||||||
|
shouts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -554,6 +554,7 @@ export type Shout = {
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['DateTime']
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>
|
deletedAt?: Maybe<Scalars['DateTime']>
|
||||||
deletedBy?: Maybe<User>
|
deletedBy?: Maybe<User>
|
||||||
|
description?: Maybe<Scalars['String']>
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
lang?: Maybe<Scalars['String']>
|
lang?: Maybe<Scalars['String']>
|
||||||
layout?: Maybe<Scalars['String']>
|
layout?: Maybe<Scalars['String']>
|
||||||
|
@ -577,7 +578,9 @@ export type ShoutInput = {
|
||||||
body?: InputMaybe<Scalars['String']>
|
body?: InputMaybe<Scalars['String']>
|
||||||
community?: InputMaybe<Scalars['Int']>
|
community?: InputMaybe<Scalars['Int']>
|
||||||
cover?: InputMaybe<Scalars['String']>
|
cover?: InputMaybe<Scalars['String']>
|
||||||
|
description?: InputMaybe<Scalars['String']>
|
||||||
layout?: InputMaybe<Scalars['String']>
|
layout?: InputMaybe<Scalars['String']>
|
||||||
|
lead?: InputMaybe<Scalars['String']>
|
||||||
mainTopic?: InputMaybe<TopicInput>
|
mainTopic?: InputMaybe<TopicInput>
|
||||||
media?: InputMaybe<Scalars['String']>
|
media?: InputMaybe<Scalars['String']>
|
||||||
slug?: InputMaybe<Scalars['String']>
|
slug?: InputMaybe<Scalars['String']>
|
||||||
|
|
|
@ -10,13 +10,13 @@ export const DogmaPage = () => {
|
||||||
<div class="col-md-12 col-xl-14 offset-md-5 order-md-first">
|
<div class="col-md-12 col-xl-14 offset-md-5 order-md-first">
|
||||||
<h4>Редакционные принципы</h4>
|
<h4>Редакционные принципы</h4>
|
||||||
<p>
|
<p>
|
||||||
Дискурс - журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым
|
Дискурс — журнал с открытой горизонтальной редакцией. Содержание журнала определяется прямым
|
||||||
голосованием его авторов. Мы нередко занимаем различные позиции по разным проблемам, но
|
голосованием его авторов. Мы нередко занимаем различные позиции по разным проблемам, но
|
||||||
придерживаемся общих профессиональных принципов:
|
придерживаемся общих профессиональных принципов:
|
||||||
</p>
|
</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>
|
||||||
<b>На первое место ставим факты.</b> Наша задача - не судить, а наблюдать и непредвзято
|
<b>На первое место ставим факты.</b> Наша задача — не судить, а наблюдать и непредвзято
|
||||||
фиксировать происходящее. Все утверждения и выводы, которые мы делаем, подтверждаются
|
фиксировать происходящее. Все утверждения и выводы, которые мы делаем, подтверждаются
|
||||||
фактами, цифрами, мнениями экспертов или ссылками на авторитетные источники.
|
фактами, цифрами, мнениями экспертов или ссылками на авторитетные источники.
|
||||||
</li>
|
</li>
|
||||||
|
@ -39,7 +39,7 @@ export const DogmaPage = () => {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>Всегда исправляем ошибки, если мы их допустили.</b>
|
<b>Всегда исправляем ошибки, если мы их допустили.</b>
|
||||||
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку - отправьте{' '}
|
Никто не безгрешен, иногда и мы ошибаемся. Заметили ошибку — отправьте{' '}
|
||||||
<a href="/about/guide#editing">ремарку</a> автору или напишите нам на{' '}
|
<a href="/about/guide#editing">ремарку</a> автору или напишите нам на{' '}
|
||||||
<a href="mailto:welcome@discours.io" target="_blank">
|
<a href="mailto:welcome@discours.io" target="_blank">
|
||||||
welcome@discours.io
|
welcome@discours.io
|
||||||
|
|
|
@ -17,14 +17,15 @@ export const AuthorPage = (props: PageProps) => {
|
||||||
Boolean(props.authorShouts) && Boolean(props.author) && props.author.slug === slug()
|
Boolean(props.authorShouts) && Boolean(props.author) && props.author.slug === slug()
|
||||||
)
|
)
|
||||||
|
|
||||||
const preload = () =>
|
const preload = () => {
|
||||||
Promise.all([
|
return Promise.all([
|
||||||
loadShouts({
|
loadShouts({
|
||||||
filters: { author: slug(), visibility: 'community' },
|
filters: { author: slug(), visibility: 'community' },
|
||||||
limit: PRERENDERED_ARTICLES_COUNT
|
limit: PRERENDERED_ARTICLES_COUNT
|
||||||
}),
|
}),
|
||||||
loadAuthor({ slug: slug() })
|
loadAuthor({ slug: slug() })
|
||||||
])
|
])
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (isLoaded()) {
|
if (isLoaded()) {
|
||||||
|
@ -44,7 +45,8 @@ export const AuthorPage = (props: PageProps) => {
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
await preload()
|
await preload()
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}
|
},
|
||||||
|
{ defer: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useProfileForm } from '../../context/profile'
|
||||||
import { validateUrl } from '../../utils/validateUrl'
|
import { validateUrl } from '../../utils/validateUrl'
|
||||||
import { createFileUploader } from '@solid-primitives/upload'
|
import { createFileUploader } from '@solid-primitives/upload'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { Button } from '../../components/_shared/Button'
|
import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel'
|
||||||
import { useSnackbar } from '../../context/snackbar'
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { handleFileUpload } from '../../utils/handleFileUpload'
|
import { handleFileUpload } from '../../utils/handleFileUpload'
|
||||||
|
@ -21,8 +21,8 @@ export const ProfileSettingsPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
|
||||||
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
|
||||||
|
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
actions: { showSnackbar }
|
actions: { showSnackbar }
|
||||||
|
@ -31,6 +31,7 @@ export const ProfileSettingsPage = () => {
|
||||||
const {
|
const {
|
||||||
actions: { loadSession }
|
actions: { loadSession }
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
|
||||||
const { form, updateFormField, submit, slugError } = useProfileForm()
|
const { form, updateFormField, submit, slugError } = useProfileForm()
|
||||||
const [prevForm, setPrevForm] = createStore(clone(form))
|
const [prevForm, setPrevForm] = createStore(clone(form))
|
||||||
|
|
||||||
|
@ -45,8 +46,6 @@ export const ProfileSettingsPage = () => {
|
||||||
|
|
||||||
const handleSubmit = async (event: Event) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setIsSubmitting(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submit(form)
|
await submit(form)
|
||||||
setPrevForm(clone(form))
|
setPrevForm(clone(form))
|
||||||
|
@ -56,7 +55,6 @@ export const ProfileSettingsPage = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSession()
|
loadSession()
|
||||||
setIsSubmitting(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||||
|
@ -68,6 +66,7 @@ export const ProfileSettingsPage = () => {
|
||||||
const result = await handleFileUpload(uploadFile)
|
const result = await handleFileUpload(uploadFile)
|
||||||
updateFormField('userpic', result.url)
|
updateFormField('userpic', result.url)
|
||||||
setIsUserpicUpdating(false)
|
setIsUserpicUpdating(false)
|
||||||
|
setIsFloatingPanelVisible(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[upload avatar] error', error)
|
console.error('[upload avatar] error', error)
|
||||||
}
|
}
|
||||||
|
@ -92,6 +91,11 @@ export const ProfileSettingsPage = () => {
|
||||||
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleSaveProfile = () => {
|
||||||
|
setIsFloatingPanelVisible(false)
|
||||||
|
setPrevForm(clone(form))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<Show when={form}>
|
<Show when={form}>
|
||||||
|
@ -107,7 +111,15 @@ export const ProfileSettingsPage = () => {
|
||||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||||
<h1>{t('Profile settings')}</h1>
|
<h1>{t('Profile settings')}</h1>
|
||||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||||
<form onSubmit={handleSubmit} enctype="multipart/form-data">
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onChange={() => {
|
||||||
|
if (!deepEqual(form, prevForm)) {
|
||||||
|
setIsFloatingPanelVisible(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
<h4>{t('Userpic')}</h4>
|
<h4>{t('Userpic')}</h4>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<Userpic
|
<Userpic
|
||||||
|
@ -235,7 +247,13 @@ export const ProfileSettingsPage = () => {
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<Button type="submit" size="L" value={t('Save settings')} loading={isSubmitting()} />
|
<FloatingPanel
|
||||||
|
isVisible={isFloatingPanelVisible()}
|
||||||
|
confirmTitle={t('Save settings')}
|
||||||
|
confirmAction={handleSaveProfile}
|
||||||
|
declineTitle={t('Cancel')}
|
||||||
|
declineAction={() => setIsFloatingPanelVisible(false)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,7 +41,8 @@ export const TopicPage = (props: PageProps) => {
|
||||||
resetSortedArticles()
|
resetSortedArticles()
|
||||||
await preload()
|
await preload()
|
||||||
setIsLoaded(true)
|
setIsLoaded(true)
|
||||||
}
|
},
|
||||||
|
{ defer: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -16,8 +16,12 @@ export type ModalType =
|
||||||
| 'donate'
|
| 'donate'
|
||||||
| 'inviteToChat'
|
| 'inviteToChat'
|
||||||
| 'uploadImage'
|
| 'uploadImage'
|
||||||
|
| 'simplifiedEditorUploadImage'
|
||||||
| 'uploadCoverImage'
|
| 'uploadCoverImage'
|
||||||
| 'editorInsertLink'
|
| 'editorInsertLink'
|
||||||
|
| 'simplifiedEditorInsertLink'
|
||||||
|
| 'followers'
|
||||||
|
| 'subscriptions'
|
||||||
|
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
type WarnKind = 'error' | 'warn' | 'info'
|
||||||
|
|
||||||
|
@ -36,8 +40,12 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
donate: 'donate',
|
donate: 'donate',
|
||||||
inviteToChat: 'inviteToChat',
|
inviteToChat: 'inviteToChat',
|
||||||
uploadImage: 'uploadImage',
|
uploadImage: 'uploadImage',
|
||||||
|
simplifiedEditorUploadImage: 'simplifiedEditorUploadImage',
|
||||||
uploadCoverImage: 'uploadCoverImage',
|
uploadCoverImage: 'uploadCoverImage',
|
||||||
editorInsertLink: 'editorInsertLink'
|
editorInsertLink: 'editorInsertLink',
|
||||||
|
simplifiedEditorInsertLink: 'simplifiedEditorInsertLink',
|
||||||
|
followers: 'followers',
|
||||||
|
subscriptions: 'subscriptions'
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal, setModal] = createSignal<ModalType | null>(null)
|
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Author, Shout, ShoutInput, Topic, LoadShoutsOptions } from '../../graphql/types.gen'
|
import type { Author, Shout, ShoutInput, LoadShoutsOptions } from '../../graphql/types.gen'
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
import { addAuthorsByTopic } from './authors'
|
import { addAuthorsByTopic } from './authors'
|
||||||
import { addTopicsByAuthor } from './topics'
|
|
||||||
import { byStat } from '../../utils/sortby'
|
import { byStat } from '../../utils/sortby'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
@ -97,26 +96,6 @@ const addArticles = (...args: Shout[][]) => {
|
||||||
}, {} as { [topicSlug: string]: Author[] })
|
}, {} as { [topicSlug: string]: Author[] })
|
||||||
|
|
||||||
addAuthorsByTopic(authorsByTopic)
|
addAuthorsByTopic(authorsByTopic)
|
||||||
|
|
||||||
const topicsByAuthor = allArticles.reduce((acc, article) => {
|
|
||||||
const { authors, topics } = article
|
|
||||||
|
|
||||||
authors.forEach((author) => {
|
|
||||||
if (!acc[author.slug]) {
|
|
||||||
acc[author.slug] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
topics.forEach((topic) => {
|
|
||||||
if (!acc[author.slug].some((t) => t.slug === topic.slug)) {
|
|
||||||
acc[author.slug].push(topic)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {} as { [authorSlug: string]: Topic[] })
|
|
||||||
|
|
||||||
addTopicsByAuthor(topicsByAuthor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSortedArticles = (articles: Shout[]) => {
|
const addSortedArticles = (articles: Shout[]) => {
|
||||||
|
|
|
@ -38,12 +38,18 @@ const addAuthors = (authors: Author[]) => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, Author>)
|
}, {} as Record<string, Author>)
|
||||||
|
|
||||||
setAuthorEntities((prevAuthorEntities) => {
|
setAuthorEntities((prevAuthorEntities) =>
|
||||||
return {
|
Object.keys(newAuthorEntities).reduce(
|
||||||
...prevAuthorEntities,
|
(acc, authorSlug) => {
|
||||||
...newAuthorEntities
|
acc[authorSlug] = {
|
||||||
|
...acc[authorSlug],
|
||||||
|
...newAuthorEntities[authorSlug]
|
||||||
}
|
}
|
||||||
})
|
return acc
|
||||||
|
},
|
||||||
|
{ ...prevAuthorEntities }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
||||||
|
|
|
@ -12,7 +12,6 @@ export const setTopicsSort = (sortBy: TopicsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
||||||
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||||
const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({})
|
|
||||||
|
|
||||||
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
||||||
const topics = Object.values(topicEntities())
|
const topics = Object.values(topicEntities())
|
||||||
|
@ -68,27 +67,6 @@ const addTopics = (...args: Topic[][]) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => {
|
|
||||||
const allTopics = Object.values(newTopicsByAuthors).flat()
|
|
||||||
addTopics(allTopics)
|
|
||||||
|
|
||||||
setTopicByAuthor((prevTopicsByAuthor) => {
|
|
||||||
return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => {
|
|
||||||
if (!acc[authorSlug]) {
|
|
||||||
acc[authorSlug] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
topics.forEach((topic) => {
|
|
||||||
if (!acc[authorSlug].some((t) => t.slug === topic.slug)) {
|
|
||||||
acc[authorSlug].push(topic)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, prevTopicsByAuthor)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadAllTopics = async (): Promise<void> => {
|
export const loadAllTopics = async (): Promise<void> => {
|
||||||
const topics = await apiClient.getAllTopics()
|
const topics = await apiClient.getAllTopics()
|
||||||
addTopics(topics)
|
addTopics(topics)
|
||||||
|
@ -121,5 +99,5 @@ export const useTopicsStore = (initialState: InitialState = {}) => {
|
||||||
setRandomTopics(initialState.randomTopics)
|
setRandomTopics(initialState.randomTopics)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor }
|
return { topicEntities, sortedTopics, randomTopics, topTopics }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
.errorPageWrapper {
|
.errorPageWrapper {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
margin: -120px 0 -2em;
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorPage {
|
.errorPage {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 35%;
|
top: 35%;
|
||||||
transform: translateY(-45%);
|
transform: translateY(-50%);
|
||||||
|
|
||||||
.image-link:hover {
|
.image-link:hover {
|
||||||
background: none;
|
background: none;
|
||||||
|
@ -36,13 +37,14 @@
|
||||||
.errorImage {
|
.errorImage {
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-height: 60vh;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorText {
|
.errorText {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
left: 52px;
|
left: 52px;
|
||||||
margin-bottom: 1em;
|
margin: 0 60px 1em 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2.25em;
|
top: -2.25em;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ $grid-breakpoints: (
|
||||||
md: 768px,
|
md: 768px,
|
||||||
lg: 992px,
|
lg: 992px,
|
||||||
xl: 1200px,
|
xl: 1200px,
|
||||||
// xxl: 1500px,
|
xxl: 1400px
|
||||||
) !default;
|
) !default;
|
||||||
$default-color: #141414;
|
$default-color: #141414;
|
||||||
$link-color: #2638d9;
|
$link-color: #2638d9;
|
||||||
|
|
|
@ -478,6 +478,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
.pretty-form__item {
|
.pretty-form__item {
|
||||||
|
margin-bottom: 2em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
|
3
src/utils/getNumeralsDeclension.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// Usage in tsx: {getNumeralsDeclension(NUMBER, ['яблоко', 'яблока', 'яблок'])}
|
||||||
|
export const getNumeralsDeclension = (number: number, words: string[], cases = [2, 0, 1, 1, 1, 2]) =>
|
||||||
|
words[number % 100 > 4 && number % 100 < 20 ? 2 : cases[number % 10 < 5 ? number % 10 : 5]]
|
|
@ -1,9 +1,10 @@
|
||||||
import { UploadFile } from '@solid-primitives/upload'
|
import { UploadFile } from '@solid-primitives/upload'
|
||||||
import { apiBaseUrl } from './config'
|
import { apiBaseUrl } from './config'
|
||||||
|
import { UploadedFile } from '../pages/types'
|
||||||
|
|
||||||
const apiUrl = `${apiBaseUrl}/upload`
|
const apiUrl = `${apiBaseUrl}/upload`
|
||||||
|
|
||||||
export const handleFileUpload = async (uploadFile: UploadFile) => {
|
export const handleFileUpload = async (uploadFile: UploadFile): Promise<UploadedFile> => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', uploadFile.file, uploadFile.name)
|
formData.append('file', uploadFile.file, uploadFile.name)
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
|
|