diff --git a/README.md b/README.md index 165aec113..0942ecf8b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,41 @@

- +

FriendLink

-

A Static web social media application

+

A social media application, where you can effortlessly connect with others through a range of features

## Features -* Profile page - - showing your profile and your posts +* Post managment + - Create, edit, and delete posts to share your thoughts and experiences. + +* Reaction System + - Express your feelings with a variety of reaction icons for posts. + +* Commenting + - Engage in conversations by leaving comments on posts. + +* Search Functionality + - Easily find users and posts by title with our intuitive search feature. + +* Profile Customization + - Personalize your profile by updating your image and banner. * Responsive design - Desktop, tablet and mobile friendly across most common browsers ## Demo -Figma Prototypes -- [Mobile](https://www.figma.com/proto/HtvWXS4I3o5pnIGFNyv2NY/FriendLink?type=design&node-id=2-37&t=b0oCfkt4D4vnbGkv-1&scaling=scale-down&page-id=2%3A35&starting-point-node-id=2%3A36&mode=design) -- [Desktop](https://www.figma.com/proto/HtvWXS4I3o5pnIGFNyv2NY/FriendLink?type=design&node-id=13-4&t=O8xADYAZgqsdzDGb-1&scaling=scale-down&page-id=13%3A2&starting-point-node-id=13%3A3&mode=design) + +[live demo](https://strong-sprinkles-75871e.netlify.app/) + +## Project Plan + +[trello board](https://trello.com/b/NeKrdf8j/development-tasks) + ## Built With - Bootstrap - SASS +- Javascript ## How to Use @@ -37,12 +54,15 @@ cd css-frameworks-ca # Install dependencies npm install +# Compile Sass +npm run build + ``` ### Development To run the application in development mode ```bash -npm start +npm run dev ``` ## Contact diff --git a/feed/index.html b/feed/index.html index 758035963..da35fdfb6 100644 --- a/feed/index.html +++ b/feed/index.html @@ -15,18 +15,15 @@
@@ -34,241 +31,129 @@

FriendLink Feed

-
-

Filters

-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • +
    +
      +

      Popular Tags

    -
    -
    -
    - -
    - +
    +
    + + + + +
    + Posts +
      + Profiles +
        +
        +
        +
        + +
        +
        +
        + + + +
        +
        + +
        +
        + +
        + + +
        + +
        + + +
        +
        +
        + + + +
        +
        +
        + +
        +
        Something went wrong!
        +
        - -
        + +
        +
        + Loading... +
        + +
        +
        + +
        - + + \ No newline at end of file diff --git a/index.html b/index.html index 5cc93a525..e52117094 100644 --- a/index.html +++ b/index.html @@ -31,14 +31,16 @@

        FriendLink

      -
      +
      - + +
      Email needs to be @noroff.no or @stud.noroff.no
      - + +
      Password needs to be atleast 8 characters
      @@ -46,18 +48,26 @@

      FriendLink

      -
      + +
      + + +
      Username can not contain punctuation symbols apart from (_)
      +
      - + +
      Email needs to be @noroff.no or @stud.noroff.no
      - + +
      Password needs to be atleast 8 characters
      - + +
      Password does not match
      @@ -65,6 +75,9 @@

      FriendLink

      +
      +
      Something went wrong!
      +
      @@ -72,6 +85,7 @@

      FriendLink

      - + + \ No newline at end of file diff --git a/js/bootstrap.bundle.min.js b/js/bootstrap.bundle.min.js new file mode 100644 index 000000000..b1999d9a9 --- /dev/null +++ b/js/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v5.3.2 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),c=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,f=[],p=()=>"rtl"===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(f.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:"mouseover",mouseleave:"mouseout"},C=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function j(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${j(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${j(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${j(e)}`))};class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${r}" but expected type "${s}".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?n(i.trim()):null}return e},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},q=".bs.alert",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return"alert"}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(Q,"close"),m(Q);const X='[data-bs-toggle="button"]';class Y extends W{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}N.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U=".bs.swipe",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return"swipe"}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add("pointer-event")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const ot=".bs.carousel",rt=".data-api",at="next",lt="prev",ct="left",ht="right",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt="carousel",yt="active",wt=".active",At=".carousel-item",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Ot={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return"carousel"}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),"hover"===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find(".carousel-item img",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)),rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute("aria-current");const i=z.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}N.on(document,bt,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===F.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride="carousel"]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt=".bs.collapse",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt="show",Pt="collapse",Mt="collapsing",jt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle="collapse"]',Ht={parent:null,toggle:!0},Wt={parent:"(null|element)",toggle:"boolean"};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static get DefaultType(){return Wt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Pt),this._element.classList.add(Mt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt,Nt),this._element.style[e]="",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(Mt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(Mt),this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=z.find(jt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}N.on(document,It,Ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt="top",Rt="bottom",qt="right",Vt="left",Kt="auto",Qt=[zt,Rt,qt,Vt],Xt="start",Yt="end",Ut="clippingParents",Gt="viewport",Jt="popper",Zt="reference",te=Qt.reduce((function(t,e){return t.concat([e+"-"+Xt,e+"-"+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e,e+"-"+Xt,e+"-"+Yt])}),[]),ie="beforeRead",ne="read",se="afterRead",oe="beforeMain",re="main",ae="afterMain",le="beforeWrite",ce="write",he="afterWrite",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"").toLowerCase():null}function fe(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t instanceof HTMLElement}function ge(t){return"undefined"!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function be(t){return t.split("-")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return["table","td","th"].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return"html"===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&"fixed"!==xe(t).position?t.offsetParent:null}function $e(t){for(var e=fe(t),i=De(t);i&&ke(i)&&"static"===xe(i).position;)i=De(i);return i&&("html"===ue(i)||"body"===ue(i)&&"static"===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&"fixed"===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&["html","body"].indexOf(ue(i))<0;){var n=xe(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Ie(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function Me(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const je={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Pe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:Me(t,Qt))}(s.padding,i),d=Ce(o),u="y"===l?zt:Vt,f="y"===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Fe(t){return t.split("-")[1]}var He={top:"auto",right:"auto",bottom:"auto",left:"auto"};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Vt,y=zt,w=window;if(c){var A=$e(i),E="clientHeight",T="clientWidth";A===fe(i)&&"static"!==xe(A=Le(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const Be={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var ze={passive:!0};const Re={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,ze)})),a&&l.addEventListener("resize",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,ze)})),a&&l.removeEventListener("resize",i.update,ze)}},data:{}};var qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:"end",end:"start"};function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return["html","body","#document"].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var i=Te(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return"rtl"===xe(s||i).direction&&(a+=ve(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe("number"!=typeof g?g:Me(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=Je(Se(t)),i=["absolute","fixed"].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&"body"!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference),E=ei({reference:A,element:v,strategy:"absolute",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==P(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:"reference"}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},li={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},hi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?zt:Vt,D="y"===y?Rt:qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],M=f?-T[$]/2:0,j=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-M-q-z-O.mainAxis:j-q-z-O.mainAxis,K=v?-E[$]/2+M+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?zt:Vt,tt="x"===y?Rt:qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:"bottom",modifiers:[],strategy:"absolute"};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(F.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi="backdrop",Ki="show",Qi=`mousedown.bs.${Vi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Yi={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const Gi=".bs.focustrap",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn="backward",en={autofocus:!0,trapElement:null},nn={autofocus:"boolean",trapElement:"element"};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:"forward")}}const on=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",rn=".sticky-top",an="padding-right",ln="margin-right";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn=".bs.modal",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}`,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn="modal-open",An="show",En="modal-static",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=z.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element,vn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle="modal"]',(function(t){const e=z.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne(".modal.show");i&&On.getInstance(i).hide(),On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn=".bs.offcanvas",kn=".data-api",Ln=`load${xn}${kn}`,Sn="show",Dn="showing",$n="hiding",In=".offcanvas.show",Nn=`show${xn}`,Pn=`shown${xn}`,Mn=`hide${xn}`,jn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Dn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,Mn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():N.trigger(this._element,jn)}:null})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,jn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=z.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
      "},Un={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Gn={entry:"(string|element|function|null)",selector:"(string|element)"};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Zn=new Set(["sanitize","allowList","sanitizeFn"]),ts="fade",es="show",is=".modal",ns="hide.bs.modal",ss="hover",os="focus",rs={AUTO:"auto",TOP:"top",RIGHT:p()?"left":"right",BOTTOM:"bottom",LEFT:p()?"right":"left"},as={allowList:Vn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},ls={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName("show")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,"mouseover",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(es),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,"mouseover",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),N.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)N.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ss?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ss?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is),ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=cs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},ds={...cs.DefaultType,content:"(null|string|element|function)"};class us extends cs{static get Default(){return hs}static get DefaultType(){return ds}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}m(us);const fs=".bs.scrollspy",ps=`activate${fs}`,ms=`click${fs}`,gs=`load${fs}.data-api`,_s="active",bs="[href]",vs=".nav-link",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))z.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(_s);else for(const e of z.parents(t,".nav, .list-group"))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s);const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy="scroll"]'))Es.getOrCreateInstance(t)})),m(Es);const Ts=".bs.tab",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}`,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s="ArrowLeft",Is="ArrowRight",Ns="ArrowUp",Ps="ArrowDown",Ms="Home",js="End",Fs="active",Hs="fade",Ws="show",Bs=".dropdown-toggle",zs=`:not(${Bs})`,Rs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',qs=`.nav-link${zs}, .list-group-item${zs}, [role="tab"]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle="tab"], .${Fs}[data-bs-toggle="pill"], .${Fs}[data-bs-toggle="list"]`;class Ks extends W{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),N.trigger(t,Os,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,Ms,js].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([Ms,js].includes(t.key))i=e[t.key===Ms?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io="hide",no="show",so="showing",oo={animation:"boolean",autohide:"boolean",delay:"number"},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ro}static get DefaultType(){return oo}static get NAME(){return"toast"}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); +//# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/js/components/commentSection.mjs b/js/components/commentSection.mjs new file mode 100644 index 000000000..fe43f235e --- /dev/null +++ b/js/components/commentSection.mjs @@ -0,0 +1,112 @@ +import { timePassed } from "../utils/timeUtils.mjs"; +import { apiCall } from "../services/apiServices.mjs"; +/** + * @description Creates and returns a container for displaying comments and reactions. + * @param {Array} comments + * @param {Array} reactions + * @returns {HTMLDivElement} + */ +export function commentSection(comments, reactions) { + const div = document.createElement("div"); + div.className = "comment-container"; + const reactionElement = createReactElements(reactions); + const commentElements = createComments(comments); + div.append(reactionElement); + div.append(commentElements); + return div; +} + +/** + * @description Creates and returns a container for displaying reaction buttons based on the provided reaction data. + * @param {Array} reactions + * @returns {HTMLDivElement} + */ +function createReactElements(reactions) { + const div = document.createElement("div"); + div.classList.add("bg-primary-subtle"); + let hearth = 0; + let smile = 0; + let frown = 0; + reactions.filter((reaction) => { + if (reaction.symbol === "💗") { + hearth += reaction.count; + } + if (reaction.symbol === "😀") { + smile += reaction.count; + } + if (reaction.symbol === "🙁") { + frown += reaction.count; + } + }); + div.innerHTML = ` + + + + + `; + return div; +} + +/** + * @description Creates and returns a container for displaying comments based on the provided comment data. + * @param {Array} comments + * @returns {HTMLDivElement} + */ +function createComments(comments) { + const div = document.createElement("div"); + div.classList.add("bg-body", "p-3"); + div.innerHTML = ` +
      +

      Comments (${comments.length})

      +
      +
      +
      + +
      + +
      +
      +
      + `; + Array.from(comments).reverse().forEach(comment => { + const com = document.createElement("div"); + com.dataset.commentId = comment.id; + const commentHeader = document.createElement("div"); + commentHeader.innerHTML = ` +
      + ${comment.author.avatar.alt} +

      ${comment.author.name}

      +
      `; + const time = timePassed(comment.created); + const body = document.createElement("p"); + body.textContent = comment.body; + if (comment.author.name === localStorage["name"]) { + const deleteBtn = document.createElement("button"); + deleteBtn.classList.add("btn", "btn-danger", "btn-sm", "delete-comment-btn", "float-end"); + deleteBtn.textContent = "delete"; + com.append(deleteBtn); + } + com.append(commentHeader); + com.append(time); + com.append(body); + div.append(com); + }); + return div; +} + +/** + * @description Updates the reaction buttons within a specified container with new reaction data. + * @param {HTMLElement} container + * @param {Array} reactions + */ +export function updateReactions(container, reactions) { + const item = container.querySelector(".bg-primary-subtle"); + const newItem = createReactElements(reactions); + item.replaceWith(newItem); +} +export async function updateComments(container) { + const api = await apiCall(`/social/posts/${container.dataset.id}?_author=true&_reactions=true&_comments=true`); + const item = container.querySelector(".bg-body"); + const newItem = createComments(api.data.comments); + item.replaceWith(newItem); +} diff --git a/js/components/postHandler.mjs b/js/components/postHandler.mjs new file mode 100644 index 000000000..4f887de24 --- /dev/null +++ b/js/components/postHandler.mjs @@ -0,0 +1,269 @@ +import { postApiData, apiCall, putApiData, deleteApiData} from "../services/apiServices.mjs"; +import { updateReactions, updateComments } from "./commentSection.mjs"; +/** + * @description Creates a new post with the provided data and sends it to the server. + * @param {Event} event + * @returns {void} + */ +export async function createNewPost(event) { + event.preventDefault(); + const postForm = document.querySelector("#post-form"); + const formData = new FormData(postForm); + const { title, body, url, alt } = Object.fromEntries(formData.entries()); + const dataPackage = {}; + if (title) { + dataPackage.title = title; + } + if (body) { + dataPackage.body = body; + } + if (url) { + dataPackage.media = {}; + dataPackage.media.url = url; + dataPackage.media.alt = alt; + } + const tags = document.querySelector("#added-tags"); + const currentTags = Array.from(tags.querySelectorAll("span")).map(currentTag => currentTag.textContent); + if (currentTags.length > 0) { + dataPackage.tags = currentTags; + } + + try { + const response = await postApiData("/social/posts", dataPackage); + if (response) { + location.reload(); + } else { + const postError = document.querySelector("#post-error"); + if (body.length > 160) { + postError.textContent = "body to long"; + } + postError.style.display = "block"; + } + + } catch (error) { + console.log(error); + } +} + +/** + * @description Opens the post editor modal and populates it with the data of the specified post. + * @param {string} id + * @returns {void} + */ +export async function openPostEditor(id) { + const myModal = new bootstrap.Modal(document.getElementById("myModal")) + const response = await apiCall(`/social/posts/${id}`); + const data = response.data; + document.querySelector("#myModal").dataset.id = data.id; + document.querySelector("#edit-title-input").value = data.title; + document.querySelector("#edit-body-input").value = data.body; + if (data.media) { + document.querySelector("#edit-media-url-input").value = data.media.url; + document.querySelector("#edit-media-alt-input").value = data.media.alt; + } + const addedTags = document.querySelector("#edit-added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + + data.tags.forEach(tag => { + const tagElement = displayTag(tag); + addedTags.append(tagElement); + }) + myModal.toggle(); + +} + +/** + * @description Handles the editing of a post. + * @param {Event} event + * @returns {void} + */ +export async function editPost(event) { + event.preventDefault(); + const postForm = document.querySelector("#edit-form"); + const formData = new FormData(postForm); + const { title, body, url, alt } = Object.fromEntries(formData.entries()); + const dataPackage = {} + if (title) { + dataPackage.title = title; + } + if (body) { + dataPackage.body = body; + } else { + dataPackage.body = ""; + } + if (url) { + dataPackage.media = {}; + dataPackage.media.url = url; + dataPackage.media.alt = alt; + } else { + // does not work, can not remove media in edit mode + dataPackage.media = null; + } + const tags = document.querySelector("#edit-added-tags"); + const currentTags = Array.from(tags.querySelectorAll("span")).map(currentTag => currentTag.textContent); + if (currentTags.length > 0) { + dataPackage.tags = currentTags; + } else { + dataPackage.tags = []; + } + + const postId = document.querySelector("#myModal").dataset.id; + try { + const response = await putApiData(`/social/posts/${postId}`, dataPackage); + if (response) { + location.reload(); + } else { + const authError = document.querySelector("#auth-error"); + authError.style.display = "block"; + } + + } catch (error) { + + } +} + +/** + * @description Toggles the visibility of media inputs for adding images. + * @returns {void} + */ +export function addMedia() { + const mediaInputs = document.querySelector(".add-media"); + const mediaIcon = document.querySelector("#media-image"); + const closeIcon = document.querySelector("#media-close"); + + if (mediaInputs.classList.contains("d-none")) { + mediaInputs.classList.remove("d-none"); + mediaIcon.classList.add("d-none"); + closeIcon.classList.remove("d-none"); + } else { + mediaInputs.classList.add("d-none"); + mediaIcon.classList.remove("d-none"); + closeIcon.classList.add("d-none"); + } +} + +/** + * @description Adds a tag to the list of tags in the form. + * @returns {void} + */ +export function addTag() { + const tagInput = document.querySelector("#tags-input"); + const tag = tagInput.value.trim().toLowerCase(); + const addedTags = document.querySelector("#added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (tag !== "" && !dublicateTag(tagsByName,tag)) { + const tagElement = displayTag(tag); + addedTags.append(tagElement); + } + tagInput.value = ""; +} + +/** + * @description Adds a tag to the list of tags in the edit form. + * @returns {void} + */ +export function editTag() { + const tagInput = document.querySelector("#edit-tags-input"); + const tag = tagInput.value.trim().toLowerCase(); + const addedTags = document.querySelector("#edit-added-tags"); + const currentTags = addedTags.querySelectorAll("span"); + const tagsByName = Array.from(currentTags).map((currentTag) => { + return currentTag.textContent; + }) + if (tag !== "" && !dublicateTag(tagsByName,tag)) { + const tagElement = displayTag(tag); + addedTags.append(tagElement); + } + tagInput.value = ""; +} +function displayTag(content) { + const tagElement = document.createElement("span"); + tagElement.classList.add("p-1", "bg-secondary-subtle", "tag"); + tagElement.textContent = content; + const removeTagButton = document.createElement("button"); + removeTagButton.classList.add("btn", "btn-close", "btn-sm"); + tagElement.append(removeTagButton); + removeTagButton.onclick = function() { + tagElement.remove(); + } + return tagElement; +} +/** + * @description Check if a tag already exists in a given array of tags. + * @param {Array} tags + * @param {string} tag + * @returns {boolean} + */ +let dublicateTag = (tags, tag) => { + const foundTag = tags.find((currentTag) => { + if (currentTag === tag) { + return true; + } + }) + if (foundTag) { + return true; + } +} +/** + * @description Handles interactions (reactions, comments, deletion) on a post. + * @param {Event} e + * @returns {Promise} + */ +export async function handlePostInteraction(e) { + const postContainer = e.target.closest("[data-id]"); + const postId = postContainer.dataset.id; + if (e.target.classList.contains("react-btn")) { + const emoji = e.target.childNodes[0].nodeValue.trim(); + async function reactToPost() { + const testEmo = await putApiData(`/social/posts/${postId}/react/${emoji}`); + updateReactions(postContainer, testEmo.data.reactions); + } + reactToPost(); + } + if (e.target.classList.contains("toggle-comment-btn")) { + const commentContainer = e.target.closest(".comment-container"); + const collapseElement = commentContainer.querySelector(".collapse"); + collapseElement.classList.toggle("show"); + } + if (e.target.classList.contains("delete-btn")) { + deleteApiData(`/social/posts/${postId}`); + } + if (e.target.classList.contains("edit-btn")) { + openPostEditor(postId); + } + if (e.target.classList.contains("comment-btn")) { + e.preventDefault(); + const textarea = e.target.closest(".comment-form").querySelector("textarea"); + const body = textarea.value; + postApiData(`/social/posts/${postId}/comment`,{body: body}); + triggerDebounce(postContainer); + } + if (e.target.classList.contains("delete-comment-btn")) { + const comment = e.target.closest("[data-comment-id]"); + const commentId = comment.dataset.commentId; + deleteApiData(`/social/posts/${postId}/comment/${commentId}`); + triggerDebounce(postContainer); + } + +} + +function debounce(func, delay) { + let timerId; + return function(...args) { + clearTimeout(timerId); + timerId = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} +const updatetest = debounce(updateComments,300); + +function triggerDebounce(value) { + updatetest(value); +} diff --git a/js/components/postList.mjs b/js/components/postList.mjs new file mode 100644 index 000000000..34c6128e6 --- /dev/null +++ b/js/components/postList.mjs @@ -0,0 +1,122 @@ +import { timePassed } from "../utils/timeUtils.mjs"; +import { showAll } from "../pages/feed.mjs"; + +/** + * @description Creates and displays a post element based on the provided data. + * @param {Object} data + * @returns {HTMLDivElement} + */ +export function displayPost(data) { + const post = document.createElement("div"); + const header = createPostHeader(data); + const body = createPostBody(data); + post.append(header); + post.append(body); + return post; +} + +/** + * @description Creates and returns the header element for a post based on the provided data. + * @param {Object} data + * @returns {HTMLDivElement} + */ +function createPostHeader(data) { + const header = document.createElement("div"); + header.classList.add("border-bottom", "mb-3"); + header.innerHTML = ` +
      + + ${data.author.avatar.alt} + +

      ${data.author.name}

      +
      `; + + const time = timePassed(data.created); + if (data.author.name === localStorage["name"]) { + const div = document.createElement("div"); + div.classList.add("btn-group", "float-end"); + const editBtn = document.createElement("button"); + const deleteBtn = document.createElement("button"); + editBtn.classList.add("btn", "btn-light", "btn-sm", "edit-btn"); + deleteBtn.classList.add("btn", "btn-danger", "btn-sm", "delete-btn"); + editBtn.textContent = "Edit"; + deleteBtn.textContent = "delete"; + div.append(editBtn, deleteBtn); + header.append(div); + } + header.append(time); + return header; +} + +/** + * @description Creates and returns the body element for a post based on the provided data. + * @param {Object} data + * @returns {HTMLDivElement} + */ +function createPostBody(data) { + const body = document.createElement("div"); + body.innerHTML = ` +

      ${data.title}

      `; + + const div = document.createElement("div"); + const buttonContainer = document.createElement("div"); + div.innerHTML = data.body; + div.classList.add("content", "mb-3"); + div.style.maxHeight = "200px"; + div.style.overflow = "hidden"; + const button = document.createElement("button"); + button.style.display = "none"; + button.classList.add("show-more", "btn"); + button.textContent = "show more"; + button.onclick = function () { + showAll(div, button); + }; + buttonContainer.append(button); + body.append(div); + body.append(button); + if (data.media) { + const image = createMediaElement(data.media); + body.append(image); + } + if (Array.isArray(data.tags) && data.tags.length > 0) { + const tags = createTagsElement(data.tags); + body.append(tags); + } + return body; +} + +/** + * @description Creates and returns a media element (such as an image) based on the provided media data. + * @param {Object} media + * @param {string} media.url + * @param {string} media.alt + * @returns {HTMLDivElement} + */ +function createMediaElement(media) { + const div = document.createElement("div"); + div.classList.add("text-center", "mb-3"); + const image = document.createElement("img"); + image.classList.add("img-fluid"); + image.src = media.url; + image.alt = media.alt; + div.append(image); + return div; +} + +/** + * @description Creates and returns a container for displaying tags based on the provided tag data. + * @param {Array} tags + * @returns {HTMLDivElement} + */ +function createTagsElement(tags) { + const div = document.createElement("div"); + div.classList.add("d-flex", "gap-2", "mb-3"); + tags.forEach(tag => { + const container = document.createElement("div"); + container.classList.add("bg-secondary-subtle", "p-1"); + container.innerText = tag; + div.append(container); + }); + return div; +} + diff --git a/js/components/search.mjs b/js/components/search.mjs new file mode 100644 index 000000000..39f911225 --- /dev/null +++ b/js/components/search.mjs @@ -0,0 +1,119 @@ +import { apiCall } from "../services/apiServices.mjs"; + +const searchInput = document.querySelector("#search-input"); + +export function initializeSearch() { + const searchResultContainer = document.querySelector("#search-results"); + if (searchResultContainer) { + displayLiveSearch(); + } + + const searchForm = document.querySelector("#search-form"); + if (searchForm) { + searchForm.addEventListener("submit", executeSearch) + } +} + +/** + * @description Handles input change events and performs API calls to retrieve search results for posts and profiles. + * @param {Event} event + * @returns {Promise} + */ +async function handleInputChange(event) { + const postList = document.querySelector("#post-list"); + postList.innerHTML = ""; + const profileList = document.querySelector("#profile-list"); + profileList.innerHTML = ""; + if (event.target.value.length > 3) { + try { + const postApiData = await apiCall("/social/posts/search?limit=3&q=" + event.target.value); + const posts = postApiData.data; + if (posts.length === 0) { + const item = createListItem("No results"); + postList.append(item); + } else { + posts.forEach(post => { + const item = createListItem(`${post.title}`); + postList.append(item); + }); + } + + } catch (error) { + console.log(error); + const item = createListItem("Cant reach server"); + item.classList.add("list-group-item-danger"); + postList.append(item); + } + try { + const profileApiData = await apiCall("/social/profiles/search?limit=3&q=" + event.target.value); + const profiles = profileApiData.data; + if (profiles.length === 0) { + const item = createListItem("No results"); + profileList.append(item); + } else { + profiles.forEach(profile => { + const item = createListItem(` + + + ${profile.name} + ` + ); + profileList.append(item); + }) + } + } catch (error) { + console.log(error); + const item = createListItem("Cant reach server"); + item.classList.add("list-group-item-danger"); + profileList.append(item); + } + } +} + +/** + * @description Display live search results based on user input. + */ +function displayLiveSearch() { + const searchResults = new bootstrap.Collapse("#search-results", {toggle: false}); + const debouncedHandleInput = debounce(handleInputChange, 300); + searchInput.addEventListener("input", debouncedHandleInput); + searchInput.addEventListener("focusin", () => { + searchResults.show(); + }) + searchInput.addEventListener("focusout", () => { + setTimeout(function() { + searchResults.hide(); + },1000); + }) + +} + +/** + * @description Execute a search operation based on the input value. + * @param {Event} event + */ +function executeSearch(event) { + event.preventDefault(); + if (searchInput.value.length > 0) { + window.location.href = "/search/index.html?search=" + searchInput.value; + } + +} + +function createListItem(content) { + const listItem = document.createElement("li"); + listItem.classList.add("list-group-item"); + listItem.innerHTML = content; + return listItem; +} + +function debounce(func, delay) { + let timerId; + return function(...args) { + clearTimeout(timerId); + timerId = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} + \ No newline at end of file diff --git a/js/components/sort.mjs b/js/components/sort.mjs new file mode 100644 index 000000000..51ceb0dc5 --- /dev/null +++ b/js/components/sort.mjs @@ -0,0 +1,71 @@ +/** + * @description Sorts the given array of posts by their popularity score and filters out posts older than 24 hours. + * @param {Array} data + * @returns {Array} + */ +export function sortByTrending(data) { + data.sort((a,b) => calculatePopularityScore(b) - calculatePopularityScore(a)); + + const filteredAllPosts = data.filter((post) => { + const date = Date.parse(post.created); + const timeElapsed = Date.now() - date; + if (timeElapsed <= 1000*60*60*24) { + return true; + } else { + return false; + } + }) + return filteredAllPosts; +} + +/** + * @description Sorts the given array of data objects by their popularity score in descending order. + * @param {Array} data + * @returns {Array} Returns a sorted array of data objects by popularity score. + */ +export function sortByPopularity(data) { + const newData = data.sort((a,b) => calculatePopularityScore(b) - calculatePopularityScore(a)); + return newData; +} + +/** + * @description Calculates the count of tags in the provided array of posts and returns them sorted by count in descending order. + * @param {Array} posts + * @returns {Array} + */ +export function getTagCount(posts) { + const tagCount = {}; + + posts.forEach(post => { + post.tags.forEach(tag => { + if (tag.length > 1) { + tagCount[tag] = (tagCount[tag] || 0) + 1; + } + }) + }) + + Object.keys(tagCount).forEach(tag => { + if (tagCount[tag] === 1) { + delete tagCount[tag]; + } + }) + + const tagsTest = Object.keys(tagCount).map(tag => ({tag, count: tagCount[tag]})); + + tagsTest.sort((a,b) => b.count - a.count); + const sortedTags = tagsTest.map(item => item.tag); + return sortedTags; +} + +/** + * @description Calculates the popularity score of a post based on the number of comments and reactions it has. + * @param {Object} post + * @param {number} [post._count.comments=0] + * @param {number} [post._count.reactions=0] + * @returns {number} + */ +function calculatePopularityScore(post) { + const comments = post._count.comments || 0; + const reactions = post._count.reactions || 0; + return comments*2 + reactions; +} \ No newline at end of file diff --git a/js/index.js b/js/index.js new file mode 100644 index 000000000..a4425b3c9 --- /dev/null +++ b/js/index.js @@ -0,0 +1,115 @@ +import { initializeFormValidation } from "./services/authService.mjs" +import { apiCall} from "./services/apiServices.mjs"; +import { getFeed } from "./pages/feed.mjs"; +import { getProfile } from "./pages/profilePage.mjs"; +import { initializeSearch} from "./components/search.mjs"; +import { displaySinglePost } from "./pages/post.mjs"; +import { displaySearchResults } from "./pages/search.mjs"; +import { createNewPost, editPost, addMedia, addTag, editTag, handlePostInteraction } from "./components/postHandler.mjs"; + + +if (localStorage["accessToken"]) { + updateLoggedInUserUI(); + const queryString = document.location.search; + const params = new URLSearchParams(queryString); + const userParam = params.get("user"); + const postParam = params.get("id"); + const searchParam = params.get("search"); + if (searchParam) { + displaySearchResults(searchParam); + } + if (postParam) { + displaySinglePost(postParam); + } + const feed = document.querySelector("#feed"); + if (feed) { + if (userParam) { + getProfile(userParam); + } else { + getFeed(); + } + } +} else { + const desiredPagePath = "/index.html"; + const desiredPageURL = window.location.origin + desiredPagePath; + if (window.location.href !== desiredPageURL) { + window.location.href = desiredPageURL; + } +} + +const forms = document.querySelectorAll(".needs-validation"); +initializeFormValidation(forms); + +const createPost = document.querySelector("#create-post"); +const postForm = document.querySelector("#post-form"); +const closePostForm = document.querySelector("#close-postform"); +const editForm = document.querySelector("#edit-form"); +if (editForm) { + editForm.addEventListener("submit", editPost) +} + +if (createPost) { + createPost.addEventListener("click", () => { + postForm.classList.remove("d-none"); + createPost.classList.add("d-none"); + }) + + + closePostForm.addEventListener("click" , () => { + postForm.classList.add("d-none"); + createPost.classList.remove("d-none"); + + }) +} +if (postForm) { + postForm.addEventListener("submit", createNewPost) +} + + + +const editTags = document.querySelector("#edit-add-tag"); +if (editTags) { + editTags.addEventListener("click" , editTag); +} +const addTags = document.querySelector("#add-tag"); +if (addTags) { + addTags.addEventListener("click" , addTag); + + const mediaToggle = document.querySelector("#media-toggle"); + mediaToggle.addEventListener("click", addMedia); + + const mediaUrlTest = document.querySelector("#media-url-input"); + mediaUrlTest.addEventListener("focusout", () => { + const image = document.querySelector("#placeholder-image"); + image.src = mediaUrlTest.value; + }) +} + +const logoutBtn = document.querySelector("#logout-btn"); +if (logoutBtn) { + logoutBtn.addEventListener("click", () => { + localStorage.clear(); + window.location.href = "../index.html"; + }) +} +initializeSearch(); +/** + * @description Updates the UI elements for the logged-in user based on their profile data. + * @returns {Promise} + */ +async function updateLoggedInUserUI() { + try { + const apiName = await apiCall("/social/profiles/" + localStorage["name"]); + const user = document.querySelectorAll(".user-profile"); + Array.from(user).forEach(img => { + img.src = apiName.data.avatar.url; + img.alt = apiName.data.avatar.alt; + }) + const userLink = document.querySelectorAll(".user-profile-link"); + Array.from(userLink).forEach(link => { + link.href = "/profile/index.html?user=" + localStorage["name"]; + }) + } catch (error) { + console.log(error); + } +} diff --git a/js/pages/feed.mjs b/js/pages/feed.mjs new file mode 100644 index 000000000..3957ed7ad --- /dev/null +++ b/js/pages/feed.mjs @@ -0,0 +1,178 @@ +import { commentSection } from "../components/commentSection.mjs"; +import { handlePostInteraction } from "../components/postHandler.mjs"; +import { displayPost } from "../components/postList.mjs"; +import { sortByPopularity, sortByTrending, getTagCount } from "../components/sort.mjs"; +import { apiCall } from "../services/apiServices.mjs"; +import { showLoadingSpinner, hideLoadingSpinner, displayError, displayNoPosts } from "../utils/feedbackUtils.mjs"; + +/** + * @description Retrieves and displays the feed of posts, including options for sorting and filtering. + * @returns {void} + */ +export async function getFeed() { + const postPerPage = 10; + let currentPage = 1; + try { + showLoadingSpinner(); + const data = await getAllPosts(); + const originalData = [...data]; + let dataCopy = sortByTrending(data); + + const recent = document.querySelector("#recent"); + const trending = document.querySelector("#trending"); + const popular = document.querySelector("#popular"); + + if (dataCopy.length === 0) { + dataCopy = originalData; + setActive(recent); + } + + + window.onscroll = function() { + if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 20) { + currentPage += 1; + const startIndex = (currentPage - 1) * postPerPage; + const endIndex = startIndex + postPerPage; + displayFeed(dataCopy.slice(startIndex,endIndex)); + } + } + + trending.addEventListener("click", () => { + dataCopy = sortByTrending(data); + clearFeed(); + if (dataCopy.length === 0) { + displayNoPosts("No new posts the last 24 hours"); + } + displayFeed(dataCopy.slice(0,10)); + setActive(trending); + }) + recent.addEventListener("click", () => { + dataCopy = originalData; + clearFeed(); + displayFeed(dataCopy.slice(0,10)); + setActive(recent); + }) + popular.addEventListener("click", () => { + dataCopy = sortByPopularity(data); + clearFeed(); + displayFeed(dataCopy.slice(0,10)); + setActive(popular); + }) + + const TagSelection = document.querySelector("#tag-selection"); + const popularTags = getTagCount(data); + const bsOffcanvas = new bootstrap.Offcanvas("#offcanvasResponsive"); + for (let i = 0; i < 10; i++) { + const tagElement = document.createElement("li"); + tagElement.classList.add("list-group-item", "list-group-item-action"); + if (i % 2) { + tagElement.classList.add("list-group-item-primary"); + } + const tagButton = document.createElement("button"); + tagButton.classList.add("btn"); + tagButton.textContent = popularTags[i]; + + tagButton.onclick = async function() { + clearFeed(); + bsOffcanvas.hide(); + try { + showLoadingSpinner(); + const apiData = await apiCall("/social/posts?_author=true&_reactions=true&_comments=true&_tag=" + tagButton.textContent); + dataCopy = apiData.data; + displayFeed(dataCopy.slice(0,10)); + hideLoadingSpinner(); + } catch (error) { + console.log(error); + displayError(); + hideLoadingSpinner(); + } + } + + tagElement.append(tagButton); + TagSelection.append(tagElement); + } + + displayFeed(dataCopy.slice(0,10)); + + hideLoadingSpinner(); + + } catch (error) { + console.log(error); + displayError(); + hideLoadingSpinner(); + } +} + +/** + * @description Displays a feed containing posts with the provided data. + * @param {Array} data + * @returns {void} + */ +export function displayFeed(data) { + const feed = document.querySelector("#feed"); + data.forEach(element => { + const post = document.createElement("div"); + post.classList.add("container", "bg-white", "p-3", "mb-3"); + post.dataset.id = element.id; + post.innerHTML= ``; + const postContent = displayPost(element); + const postComments = commentSection(element.comments, element.reactions); + post.append(postContent); + post.append(postComments); + feed.append(post); + + const bodyText = post.querySelector(".content"); + const bodyShowMore = post.querySelector(".show-more"); + if (bodyText.clientHeight < bodyText.scrollHeight) { + bodyShowMore.style.display = "block"; + } + }); + const postTest = feed.querySelectorAll("[data-id]"); + postTest.forEach(post => { + post.addEventListener("click", handlePostInteraction); + }); + +} + +/** + * @description Clear the content of the feed element. + */ +export function clearFeed() { + const feed = document.querySelector("#feed"); + feed.innerHTML = ""; +} + +export function showAll(div, btn) { + div.style.maxHeight = "none"; + btn.style.display = "none"; +} + +/** + * @description Retrieve all posts with associated author, reactions, and comments data. + * @returns {Promise} - A promise that resolves to an array containing all posts data. + */ +async function getAllPosts() { + let datatest = await apiCall("/social/posts?_author=true&_reactions=true&_comments=true"); + const allDatatest = []; + allDatatest.push(...datatest.data); + while (!datatest.meta.isLastPage) { + datatest = await apiCall(`/social/posts?_author=true&_reactions=true&_comments=true&page=${datatest.meta.currentPage + 1}`); + allDatatest.push(...datatest.data); + } + return allDatatest; +} + +/** + * @description Set the active state for a specific button within a group of buttons. + * @param {HTMLElement} button + */ +function setActive(button) { + const myTabs = document.querySelectorAll("#myTab button"); + Array.from(myTabs).forEach(tab => { + if (tab === button) { + tab.classList.add("active"); + } else { + tab.classList.remove("active"); + } + }) +} \ No newline at end of file diff --git a/js/pages/post.mjs b/js/pages/post.mjs new file mode 100644 index 000000000..ae8b52d64 --- /dev/null +++ b/js/pages/post.mjs @@ -0,0 +1,33 @@ +import { displayPost } from "../components/postList.mjs"; +import { commentSection } from "../components/commentSection.mjs"; +import { apiCall } from "../services/apiServices.mjs"; +import { displayError, hideLoadingSpinner, showLoadingSpinner } from "../utils/feedbackUtils.mjs"; +import { handlePostInteraction } from "../components/postHandler.mjs"; +/** + * @description Displays a single post on the page. + * @param {Object} data + */ +export async function displaySinglePost(param) { + try { + showLoadingSpinner(); + const apiData = await apiCall("/social/posts/" + param + "?_author=true&_reactions=true&_comments=true"); + const {data} = apiData; + const container = document.querySelector("#post"); + const post = document.createElement("div"); + post.classList.add("bg-white", "p-3"); + post.dataset.id = data.id; + container.innerHTML = `

      ${data.title}

      `; + const postContent = displayPost(data); + const postComments = commentSection(data.comments, data.reactions); + + post.append(postContent); + post.append(postComments); + post.addEventListener("click", handlePostInteraction); + container.append(post); + hideLoadingSpinner(); + } catch (error) { + console.log(error) + displayError(); + hideLoadingSpinner(); + } +} \ No newline at end of file diff --git a/js/pages/profilePage.mjs b/js/pages/profilePage.mjs new file mode 100644 index 000000000..26853aa19 --- /dev/null +++ b/js/pages/profilePage.mjs @@ -0,0 +1,216 @@ +import { putApiData, apiCall } from "../services/apiServices.mjs"; +import { displayError, displayNoPosts, hideLoadingSpinner, showLoadingSpinner } from "../utils/feedbackUtils.mjs"; +import { displayFeed } from "./feed.mjs"; + +export async function getProfile(param) { + try { + showLoadingSpinner(); + const userProfile = await apiCall("/social/profiles/" + param + "?_following=true&_followers=true"); + displayProfile(userProfile); + const apiData = await apiCall(`/social/profiles/${param}/posts` +"?_author=true&_reactions=true&_comments=true"); + if (apiData.data.length === 0) { + displayNoPosts(`${param} has 0 posts`); + } else { + displayFeed(apiData.data); + } + hideLoadingSpinner(); + } catch (error) { + console.log(error); + displayError(); + hideLoadingSpinner(); + } +} + +/** + * @description Displays profile information on the page. + * @param {Object} data + */ +function displayProfile(data) { + const avatar = document.querySelector("#user-avatar"); + avatar.src = data.data.avatar.url; + avatar.alt = data.data.avatar.alt; + + const banner = document.querySelector("#banner"); + getMeta(data.data.banner.url, (err,img) => { + if (img.naturalHeight > img.naturalWidth) { + banner.style.backgroundSize = "100% auto"; + } else { + banner.style.backgroundSize = "auto 100%"; + + } + banner.style.backgroundImage = `url(${data.data.banner.url})`; + }); + + const user = document.querySelector("h1"); + user.textContent = data.data.name; + user.style.filter = "drop-shadow(4px -4px 12px white)"; + const bio = document.querySelector("#bio"); + if (data.data.bio) { + bio.textContent = data.data.bio; + } + + const followOrEdit = document.querySelector("#followOrEdit"); + if (data.data.name === localStorage["name"]) { + followOrEdit.textContent = "Edit"; + followOrEdit.onclick = function() { + editProfile(data.data.avatar, data.data.banner); + } + const postBtn = document.querySelector("#create-posts"); + postBtn.classList.remove("d-none"); + } else { + if (isFollowing(data.data.followers)) { + followOrEdit.textContent = "Unfollow"; + } + followOrEdit.addEventListener("click", function() { + if (isFollowing(data.data.followers)) { + putApiData(`/social/profiles/${data.data.name}/unfollow`); + followOrEdit.textContent = "Follow"; + } else { + putApiData(`/social/profiles/${data.data.name}/follow`); + followOrEdit.textContent = "Unfollow"; + } + }) + } + const testFollow = document.querySelectorAll(".followers"); + displayFollow(data.data.followers, testFollow); + const testFollowing = document.querySelectorAll(".following"); + displayFollow(data.data.following, testFollowing); +} + + +/** + * @description Displays the list of followers or following users. + * @param {Array} followers - The array of followers or following users. + * @param {NodeList} containers - The containers where the list of followers or following users will be displayed. + */ +function displayFollow(followers, containers) { + Array.from(containers).forEach(container => { + const amount = container.querySelector(".amount"); + const followLength = followers.length; + amount.textContent = followLength; + let list = displayUsers(followers); + const modal = container.querySelector(".modal-body"); + if (modal) { + list.classList.add("row"); + modal.append(list); + } else { + if (followLength >= 3) { + list = displayUsers(followers,3); + } + list.classList.add("d-none", "d-lg-flex", "row"); + container.append(list); + } + }) +} + +/** + * @description Checks if the current user is following a list of users. + * @param {Array} users + * @returns {boolean} - True if the current user is following any user in the list, otherwise false. + */ +function isFollowing(users) { + const following = users.find((user) => { + if (user.name === localStorage["name"]) { + return true; + } + }) + return following; +} + +/** + * @description Displays a list of users with their avatars and names. + * @param {Array} users + * @param {number} [amount=users.length] - The maximum number of users to display (defaults to the length of the users array). + * @returns {HTMLDivElement} + */ +function displayUsers(users, amount = users.length) { + const div = document.createElement("div"); + for (let i = 0 ; i < amount ; i++) { + const user = document.createElement("a"); + user.href = "../profile/index.html?user=" + users[i].name; + user.classList.add("d-flex","flex-column","align-items-center","col-4", "overflow-hidden"); + const userImg = document.createElement("img"); + userImg.src = users[i].avatar.url; + userImg.classList.add("user-icon"); + const userName = document.createElement("h3"); + userName.classList.add("fs-5"); + userName.textContent = users[i].name; + user.append(userImg); + user.append(userName); + div.append(user); + } + return div; +} + + +/** + * @description Displays a modal for editing the user's profile details. + * @param {Object} avatar + * @param {Object} banner + */ +function editProfile(avatar, banner) { + const editModal = new bootstrap.Modal(document.getElementById("edit-modal")) + const modal = document.querySelector("#edit-modal"); + const userImg = modal.querySelector("img"); + const userBanner = modal.querySelector(".test-bg"); + userBanner.style.backgroundImage = `url(${banner.url})`; + userBanner.style.backgroundSize = "100% auto"; + userImg.src = avatar.url; + const avatarUrl = document.querySelector("#avatar-url"); + const avatarAlt = document.querySelector("#avatar-alt"); + const bannerUrl = document.querySelector("#banner-url"); + const bannerAlt = document.querySelector("#banner-alt"); + avatarUrl.value = avatar.url; + avatarAlt.value = avatar.alt; + bannerUrl.value = banner.url; + bannerAlt.value = banner.alt; + editModal.toggle(); + avatarUrl.addEventListener("input", () => { + userImg.src = avatarUrl.value; + }); + bannerUrl.addEventListener("input", () => { + getMeta(bannerUrl.value, (err,img) => { + if (img.naturalHeight > img.naturalWidth) { + userBanner.style.backgroundSize = "100% auto"; + } else { + userBanner.style.backgroundSize = "auto 100%"; + + } + }); + userBanner.style.backgroundImage = `url(${bannerUrl.value})`; + }) + + const form = document.querySelector("#edit-profile-form"); + form.addEventListener("submit", event => { + event.preventDefault(); + const dataPackage = {} + if (bannerUrl) { + dataPackage.banner = {}; + dataPackage.banner.url = bannerUrl.value; + dataPackage.banner.alt = bannerAlt.value; + } + if (avatarUrl) { + dataPackage.avatar = {}; + dataPackage.avatar.url = avatarUrl.value; + dataPackage.avatar.alt = avatarAlt.value; + } + putApiData("/social/profiles/" + localStorage["name"], dataPackage); + editModal.hide() + }) +} + + +/** + * @description Retrieve metadata (such as natural width and height) of an image from its URL. + * @param {string} url - The URL of the image. + * @param {Function} cb - The callback function to be invoked when metadata retrieval is complete. + * The callback should accept two parameters: (err, img). + * - err: An error object if an error occurs during metadata retrieval, or null otherwise. + * - img: An Image object containing the metadata of the image. + */ +function getMeta(url,cb) { + const img = new Image(); + img.onload = () => cb(null, img); + img.onerror = (err) => cb(err); + img.src = url; +} \ No newline at end of file diff --git a/js/pages/search.mjs b/js/pages/search.mjs new file mode 100644 index 000000000..c37ac39e6 --- /dev/null +++ b/js/pages/search.mjs @@ -0,0 +1,144 @@ +import { apiCall } from "../services/apiServices.mjs"; +import { displayError, hideLoadingSpinner, showLoadingSpinner } from "../utils/feedbackUtils.mjs"; + +/** + * @description Displays search results for posts and profiles based on the provided search parameter. + * @param {string} param + */ +export function displaySearchResults(param) { + + const searchPosts = document.querySelector("#search-posts"); + const searchProfiles = document.querySelector("#search-profiles"); + const nextPage = document.querySelector("#next-page"); + const prevPage = document.querySelector("#prev-page"); + const searchTitle = document.querySelector("#search-result-title"); + const title = document.querySelector("title"); + + searchTitle.textContent += " for: " + param; + title.textContent += " - " + param; + + // Initialize + let currentPage = 1; + let currentEndpoint = "/social/posts/search?limit=10&_author=true&q=" + param; + let extractData = item => ({ + title: item.title, + body: item.body, + href: `../post/index.html?id=${item.id}`, + }); + displaySearchAmount(param); + + /** + * @description Fetches search results from the specified endpoint and displays them on the page. + * @param {string} endpoint + * @param {Function} data + * @param {number} page + */ + async function displayResults(endpoint, data, page) { + try { + showLoadingSpinner(); + const searchResult = await apiCall(endpoint + "&page=" + page); + if (searchResult.meta.isLastPage) { + nextPage.disabled = true; + } else { + nextPage.disabled = false; + } + if (searchResult.meta.isFirstPage) { + prevPage.disabled = true; + } else { + prevPage.disabled = false; + } + const searchResults = document.querySelector("#search"); + searchResults.innerHTML = ""; + searchResult.data.forEach(item => { + const extractData = data(item); + const searchItem = document.createElement("div"); + searchItem.classList.add("bg-white", "mb-3"); + const searchText = document.createElement("a"); + searchText.href = extractData.href; + searchText.classList.add("text-black", "link-underline","link-underline-opacity-0", "p-2", "d-flex", "align-items-center", "gap-3"); + let htmlContent = `${extractData.title}` + if (extractData.body) { + htmlContent += `

      ${extractData.body}

      ` + } + if (extractData.avatar) { + const avatarImg = document.createElement("img"); + avatarImg.src = extractData.avatar; + avatarImg.classList.add("user-icon"); + searchText.append(avatarImg); + } + searchText.innerHTML += `
      ${htmlContent}
      `; + searchItem.append(searchText); + searchResults.append(searchItem); + }) + hideLoadingSpinner(); + + } catch (error) { + console.log("error"); + displayError(); + hideLoadingSpinner(); + } + } + + // event listener for search posts button + searchPosts.addEventListener("click", () => { + currentPage = 1; + currentEndpoint = "/social/posts/search?limit=10&_author=true&q=" + param; + extractData = item => ({ + title: item.title, + body: item.body, + href: `../post/index.html?id=${item.id}`, + }); + + displayResults(currentEndpoint, extractData, currentPage); + }); + + // event listener for search profiles button + searchProfiles.addEventListener("click", () => { + currentPage = 1; + currentEndpoint = "/social/profiles/search?limit=10&q=" + param; + extractData = item => ({ + title: item.name, + body: item.bio, + href: `../profile/index.html?user=${item.name}`, + avatar: item.avatar.url + }); + displayResults(currentEndpoint, extractData, currentPage); + }); + + // initial display of search results + displayResults(currentEndpoint, extractData, currentPage); + + nextPage.addEventListener("click", () => { + currentPage++; + displayResults(currentEndpoint, extractData, currentPage); + window.scrollTo({ + top: 0, + behavior: "smooth" + }) + }) + prevPage.addEventListener("click", () => { + if (currentPage > 1) { + currentPage--; + displayResults(currentEndpoint, extractData, currentPage); + window.scrollTo({ + top: 0, + behavior: "smooth" + }) + } + }) +} + +/** + * @description Fetches and displays the total count of search results for posts and profiles based on the provided search parameter. + * @param {string} param + * @throws {Error} + */ +async function displaySearchAmount(param) { + const postAmount = await apiCall("/social/posts/search?q=" + param); + const postBadge = document.querySelector("#search-post-amount"); + postBadge.textContent = postAmount.meta.totalCount; + + const profileAmount = await apiCall("/social/profiles/search?q=" + param); + const profileBadge = document.querySelector("#search-profiles-amount"); + profileBadge.textContent = profileAmount.meta.totalCount; +} \ No newline at end of file diff --git a/js/services/apiServices.mjs b/js/services/apiServices.mjs new file mode 100644 index 000000000..db4ba8b1b --- /dev/null +++ b/js/services/apiServices.mjs @@ -0,0 +1,134 @@ +const NOROFF_API_URL = "https://v2.api.noroff.dev"; +const accessToken = localStorage.getItem("accessToken"); +/** + *Requests and retrieves an API key for a user with the provided authentication token. + * @param {string} token + * @returns {Promise} + */ +async function getApiKey(token) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/create-api-key`, { + method: "POST", + headers: { + "Content-type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: "API KEY" + }) + }) + + const result = await response.json(); + return result.data.key; + } catch (error) { + console.log(error); + } +} +/** + *Makes an authenticated API call to a specified endpoint using the provided access token. + * @param {string} endpoint + */ +export async function apiCall(endpoint) { + const apiKey = await getApiKey(accessToken); + const options = { + headers: { + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + } + } + try { + const response = await fetch(`${NOROFF_API_URL}${endpoint}`, options) + const result = await response.json(); + return result; + } catch (error) { + + } +} + +/** + * @description Sends a POST request to the specified endpoint with the provided data. + * @param {string} endpoint + * @param {Object} [data={}] + * @returns {Promise} + */ +export async function postApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "post", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + try { + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } else { + return response.json(); + } + } catch (error) { + + } +} + +/** + * @description Sends a PUT request to the specified endpoint with the provided data. + * @param {string} endpoint + * @param {Object} [data={}] + * @returns {Promise} + */ +export async function putApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "put", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + try { + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } else { + return response.json(); + } + + } catch (error) { + console.log(error); + } +} + +/** + * @description Sends a DELETE request to the specified endpoint with the provided data. + * @param {string} endpoint + * @param {Object} [data={}] + * @returns {Promise} + */ +export async function deleteApiData(endpoint, data = {}) { + const apiKey = await getApiKey(accessToken); + const options = { + method: "delete", + headers: { + "Content-Type" : "application/json", + Authorization: `Bearer ${accessToken}`, + "X-Noroff-API-Key": apiKey + }, + body: JSON.stringify(data) + } + try { + const response = await fetch (`${NOROFF_API_URL}${endpoint}`, options) + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } else { + location.reload(); + } + } catch (error) { + + } +} \ No newline at end of file diff --git a/js/services/authService.mjs b/js/services/authService.mjs new file mode 100644 index 000000000..28f62350f --- /dev/null +++ b/js/services/authService.mjs @@ -0,0 +1,149 @@ +const NOROFF_API_URL = "https://v2.api.noroff.dev"; + +/** + *Initializes form validation and submission logic for a collection of forms. + * @param {NodeList} forms + */ +export function initializeFormValidation(forms) { + Array.from(forms).forEach(form => { + const inputs = form.querySelectorAll("input"); + Array.from(inputs).forEach(input => { + input.addEventListener("focusout", () => { + if (input.value.length > 0) { + if(inputValidation(input)) { + input.classList.add("is-valid"); + } else { + input.classList.add("is-invalid"); + } + }; + }); + input.addEventListener("focusin", () => { + input.classList.remove("is-valid", "is-invalid"); + }) + }) + form.addEventListener("submit", event => { + event.preventDefault(); + + loginFormValidation(form); + }) + }) +} +/** + * Handles user authentication by sending a POST request to the specified API endpoint + * @param {object} data + * @param {string} data.email + * @param {string} data.password + */ +async function signIn(data) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/login`, { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const authError = document.querySelector("#auth-error"); + authError.style.display = "block"; + if (response.status === 401) { + authError.textContent = "Incorrect Password"; + } + } else { + const result = await response.json(); + localStorage.setItem("accessToken", result.data.accessToken); + localStorage.setItem("name", result.data.name); + window.location.href = "/profile/index.html?user=" + result.data.name; + } + + } catch (error) { + console.log(error); + } +} + +/** + *Registers a new user by sending a POST request to the specified API endpoint. + * @param {Object} data + * @param {string} data.name + * @param {string} data.email + * @param {string} data.password + */ +async function registerUser(data) { + try { + const response = await fetch(`${NOROFF_API_URL}/auth/register`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(data) + }); + if (!response.ok) { + const authError = document.querySelector("#auth-error"); + authError.style.display = "block"; + } else { + const signInData = (({email, password}) => ({email, password}))(data); + signIn(signInData); + } + } catch (error) { + console.log(error); + } +} +/** + *Validates the input fields in a given form and performs user authentication or registration based on the form data. + * @param {HTMLFormElement} form + */ +function loginFormValidation(form) { + const dataObject = {} + const inputs = form.querySelectorAll("input"); + + Array.from(inputs).forEach(input => { + if(inputValidation(input)) { + const key = input.name; + dataObject[key] = input.value; + } else { + input.classList.add("is-invalid"); + } + }) + if ("email" in dataObject && "password" in dataObject) { + if ("name" in dataObject) { + registerUser(dataObject); + } else { + signIn(dataObject); + } + } +} +/** + *Validates the value of an input element based on its type. + * @param {HTMLInputElement} input + * @returns {boolean} + */ +function inputValidation(input) { + if (input.type === "text") { + return (userNameValidation(input.value)); + } + + if (input.type === "email") { + return (emailValidation(input.value)) + } + + if (input.type === "password") { + if (input.value.length >= 8) { + if (input.id ==="registerPasswordRepeat") { + const pass = document.querySelector("#registerPassword"); + return (pass.value === input.value); + } else { + return true; + } + } + } +} + +let emailValidation = (email) => { + const regEx = /\S+@(?:stud\.)?noroff\.no$/; + return regEx.test(email); +} +let userNameValidation = (username) => { + const regEx = /^[a-zA-Z0-9_]+$/; + return regEx.test(username); +} \ No newline at end of file diff --git a/js/utils/feedbackUtils.mjs b/js/utils/feedbackUtils.mjs new file mode 100644 index 000000000..2f949166c --- /dev/null +++ b/js/utils/feedbackUtils.mjs @@ -0,0 +1,18 @@ + +export function showLoadingSpinner() { + const spinner = document.querySelector("#loading-spinner"); + spinner.classList.remove("d-none"); +} +export function hideLoadingSpinner() { + const spinner = document.querySelector("#loading-spinner"); + spinner.classList.add("d-none"); +} +export function displayError() { + const errorMessage = document.querySelector("#display-error"); + errorMessage.classList.remove("d-none"); +} +export function displayNoPosts(content) { + const feed = document.querySelector("#feed"); + feed.classList.add("text-center"); + feed.innerHTML = content; +} \ No newline at end of file diff --git a/js/utils/timeUtils.mjs b/js/utils/timeUtils.mjs new file mode 100644 index 000000000..edc562f1b --- /dev/null +++ b/js/utils/timeUtils.mjs @@ -0,0 +1,36 @@ + +/** + * @description Calculate the time elapsed since a given timestamp and format it as a human-readable string. + * @param {string} created + * @returns {HTMLDivElement} + */ +export function timePassed(created) { + const div = document.createElement("p"); + div.classList.add("text-black-50"); + const date = Date.parse(created); + const timeElapsed = Date.now() - date; + const weeks = Math.floor(timeElapsed / (1000 * 60 * 60 * 24 * 7)); + const days = Math.floor(timeElapsed / (1000 * 60 * 60 * 24)); + const hours = Math.floor(timeElapsed / (1000 * 60 * 60)); + const minutes = Math.floor(timeElapsed / (1000 * 60)); + let time = ""; + switch (true) { + case weeks > 0: + time = weeks === 1 ? "1 week ago" : `${weeks} weeks ago`; + break; + case days > 0: + time = days === 1 ? "1 day ago" : `${days} days ago`; + break; + case hours > 0: + time = hours === 1 ? "1 hour ago" : `${hours} hours ago`; + break; + case minutes > 0: + time = minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`; + break; + default: + time = "now" + break; + } + div.textContent = `Posted ${time}`; + return div; +} diff --git a/post/index.html b/post/index.html new file mode 100644 index 000000000..110c091d2 --- /dev/null +++ b/post/index.html @@ -0,0 +1,83 @@ + + + + + + FriendLink | Post + + + + + + + + +
      + +
      +
      +
      +
      +
      +
      + Loading... +
      + +
      +
      + + +
      + + + + + \ No newline at end of file diff --git a/profile/index.html b/profile/index.html index c5e05acb8..c6d7b59ca 100644 --- a/profile/index.html +++ b/profile/index.html @@ -18,15 +18,12 @@ FriendLink - - - Profile - +
      + + +
      @@ -34,320 +31,208 @@
      -
      +
      -
      -
      +
      +
      +
      -
      -
      -
      -
      -
      -
      -
      +
      -
      -
      -
      -
      -
      -
      - +
      -
      -
      -
      - -
      - -
      +
      + + + -
      -
      -
      -
      -
      -
      -

      Random User

      -
      -

      Posted 2 days ago

      -
      -
      -

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

      -
      -
      - - - - -
      -
      -
      -

      Comments

      -
      -
      -
      -
      - -
      - -
      -
      -
      -
      -
      -
      -
      -

      Random User

      -
      -

      Posted 2 days ago

      -
      -
      -

      Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

      -
      -
      -
      -
      -
      -
      -

      Random User

      -
      -

      Posted 2 days ago

      -
      -
      -

      Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

      -
      -
      -
      +
      + Posts +
        + Profiles +
          -
          -
          -
          -
          -

          Random User

          -
          -

          Posted 2 days ago

          +
          +
          +
          +
          + +
          -
          -

          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

          +
          +
          -
          - - - - -
          -
          -
          -

          Comments

          -
          -
          - -
          - -
          - -
          - -
          -
          -
          -
          -
          -

          Random User

          -
          -

          Posted 2 days ago

          -
          -
          -

          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

          -
          -
          -
          -
          -
          -
          -

          Random User

          -
          -

          Posted 2 days ago

          +
          + + +
          +
          -
          -

          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

          -
          -
          -
          -
          -
          -
          -
          -
          -

          Random User

          + +
          -

          Posted 2 days ago

          -
          -

          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

          -
          -
          - - - - +
          + + + +
          -
          -
          -

          Comments

          -
          -
          -
          -
          - -
          - -
          -
          -
          -
          -
          -
          -
          -

          Random User

          -
          -

          Posted 2 days ago

          -
          -
          -

          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

          -
          -
          -
          -
          -
          -
          -

          Random User

          -
          -

          Posted 2 days ago

          -
          -
          -

          Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis aut, cumque perspiciatis, possimus illum excepturi vitae hic recusandae natus, porro iusto ab veniam id atque praesentium dolores velit. Maiores, quibusdam!

          -
          -
          +
          +
          -
          +
          Something went wrong!
          +
          -
          +
          +
          + Loading... +
          +
          +