/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- <meta http-equiv="Content-Security-Policy" content="default-src * 'self' 'unsafe-inline' 'unsafe-eval' data: content:"> -->
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#fff">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<title>DriveLife</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="assets/icons/apple-touch-icon.png">
<link rel="icon" href="assets/icons/favicon.png">
<link rel="stylesheet" href="framework7/framework7-bundle.min.css">
<link rel="stylesheet" href="css/icons.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/custom.css?v=1.111">
<link rel="stylesheet" href="css/custom-kesh.css">
<link rel="preload" as="image" href="assets/img/icon-add-post.svg">
<link rel="preload" as="image" href="assets/img/icon-qr-code.svg">
<link rel="preload" as="image" href="assets/img/icon-vehicle-add.svg">
<script defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDqDMSFVfl-tOgqaj4ZqA5I3HnobrIK6jg&loading=async&libraries=places&v=weekly">
</script>
<script src="//cdn.jsdelivr.net/npm/hls.js@latest"></script>
</head>
<body>
<div id="app">
<div class="views tabs safe-areas app-landing-page">
<div class="init-loader">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="init-loader light">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="toolbar toolbar-bottom tabbar-icons footer">
<div class="toolbar-inner">
<a href="#view-social" class="footer-links tab-link tab-link-active start-link">
<i class="icon f7-icons">house</i>
<span class="tabbar-label">Home</span>
</a>
<a href="#view-discover" class="footer-links tab-link">
<i class="icon f7-icons">search</i>
<span class="tabbar-label">Discover</span>
</a>
<a class="tab-link" id="open-action-sheet">
<i class="icon f7-icons">plus_app</i>
<span class="tabbar-label">Add</span>
</a>
<a href="#view-store" class="footer-links tab-link">
<i class="icon f7-icons">cart</i>
<span class="tabbar-label">Store</span>
</a>
<a href="#view-profile" class="footer-links tab-link view-profile-link">
<i class="icon f7-icons">person_circle</i>
<span class="tabbar-label">Profile</span>
</a>
</div>
</div>
<!-- Left Menu panel -->
<div class="panel panel-left panel-push">
<div class="block">
<img src="assets/img/ce-logo-dark.png" />
<p>Welcome to the DriveLife App by CarEvents.com</p>
<p>This is currently an early access app.</p>
</p>Over the coming months, we will be adding all your favourite features from the CarEvents.com website,
including
adding new events, venues, car clubs and more.</p>
<p>To report any bugs or request any features, please email: <a
href="mailto:app@carevents.com">app@carevents.com</a></p>
</div>
<a class="logout-button"><i class="icon f7-icons">arrow_left_square</i> Logout</a>
</div>
<div id="view-home" class="view view-init view-main tab tab-active">
<div class="page" data-name="home">
<div class="page-content">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
</div>
</div>
</div>
<div id="view-social" class="view view-init tab" data-name="social" data-url="/social/">
</div>
<div id="view-discover" class="view view-init tab" data-name="discover" data-url="/discover/">
</div>
<div id="view-store" class="view view-init tab" data-name="store" data-url="/store/">
</div>
<div id="view-profile" class="view view-init tab" data-name="profile" data-url="/profile/">
</div>
<div id="view-profile-edit" class="view view-init tab" data-name="profile-edit" data-url="/profile-edit/">
</div>
<!-- <div id="view-auth" class="view view-init tab" data-name="auth" data-url="/auth/">
</div> -->
<div id="view-notifications" class="view view-init tab" data-name="notifications" data-url="/notifications/">
</div>
<div id="view-profile-garage-edit" class="view view-init tab" data-name="profile-garage-edit"
data-url="/profile-garage-edit/">
</div>
<div id="profile-view" class="view view-init tab" data-name="profile-view" data-url="/profile-view/1">
</div>
<div id="post-view" class="view view-init tab" data-name="post-view" data-url="/post-view/-1">
</div>
<div id="profile-garage-edit" class="view view-init tab" data-name="profile-garage-edit"
data-url="/profile-garage-edit/">
</div>
<div id="profile-garage-vehicle-view" class="view view-init tab" data-name="profile-garage-vehicle-view"
data-url="/profile-garage-vehicle-view/-1">
</div>
<div id="discover-view-venue" class="view view-init tab" data-name="discover-view-venue"
data-url="/discover-view-venue/-1">
</div>
<div id="discover-view-event" class="view view-init tab" data-name="discover-view-event"
data-url="/discover-view-event/-1">
</div>
<div id="search" class="view view-init tab" data-name="search" data-url="/search/">
</div>
<!-- <div id="view-profile-edit-images" class="view view-init tab" data-name="profile-edit-images"
data-url="/profile-edit-images/">
</div>
<div id="view-profile-edit-mydetails" class="view view-init tab" data-name="profile-edit-mydetails"
data-url="/profile-edit-mydetails/">
</div>
<div id="view-profile-edit-socials" class="view view-init tab" data-name="profile-edit-socials"
data-url="/profile-edit-socials/">
</div>
<div id="view-profile-edit-username" class="view view-init tab" data-name="profile-edit-username"
data-url="/profile-edit-username/">
</div>
<div id="view-profile-garage-vehicle-add" class="view view-init tab" data-name="profile-garage-vehicle-add"
data-url="/profile-garage-vehicle-add/">
</div> -->
</div>
<!-- Comments Slider -->
<div class="popup comments-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title" style="left: 145.5px;">Comments</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="comments-list" id="comments-list">
<div class="preloader"></div>
</div>
<form id="comment-form">
<!-- replying to -->
<span class="replying-to hidden"></span>
<div class="toolbar messagebar">
<div class="toolbar-inner">
<div class="messagebar-area">
<textarea class="resizable" name="comment" placeholder="Message"></textarea>
</div>
<button type="submit" class="link icon-only demo-send-message-link"><i
class="icon f7-icons">arrow_up_circle_fill</i></button>
<!-- <a class="link icon-only demo-send-message-link"><i class="icon f7-icons">arrow_up_circle_fill</i></a> -->
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="popup share-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Share This</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li>
<a href="#" id="copy-link" target="_blank"><i class="icon f7-icons">link</i>
Copy
Link
</a>
</li>
<li>
<a href="#" id="share-post-email" target="_blank"><i class="icon f7-icons">envelope</i>
Share via Email
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Post Slider -->
<div class="popup edit-post-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Post Options</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li>
<a href="#" target="_blank" id="edit-post">
<i class="icon f7-icons">pencil</i>
Edit Post
</a>
</li>
<li><a href="#" target="_blank" id="delete-post"><i class="icon f7-icons">delete_right</i> Delete
Post</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Post Slider -->
<!-- Add Link Slider -->
<div class="popup add-link-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Add Link</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<form>
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Link Title</div>
<div class="item-input-wrap">
<input type="text" name="custom_link_title" placeholder="E.g. My Website" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Link URL</div>
<div class="item-input-wrap">
<input type="text" name="custom_link_url" placeholder="E.g. https://www.mylink.com" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
<div class="button-add-link">
<div class="button button-large button-fill margin-bottom" id="add-link-btn">
Save</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Framework7 library -->
<script src="framework7/framework7-bundle.min.js"></script>
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
<!-- App routes -->
<script src="js/routes.js" type="module"></script>
<!-- App store -->
<script src="js/store.js" type="module"></script>
<!-- App scripts -->
<script src="js/app.js" type="module"></script>
<script src="js/qr-scanner.js" type="module"></script>
<!-- Page modules -->
<script src="js/view-user-profile.js" type="module"></script>
<script src="js/homepage.js" type="module"></script>
<script src="js/profile.js" type="module"></script>
<script src="js/profile-edit.js" type="module"></script>
<script src="js/view-post.js" type="module"></script>
<script src="js/notifications.js" type="module"></script>
<script src="js/discoverpage.js" type="module"></script>
<script src="js/search.js" type="module"></script>
<script src="js/qr.js" type="module"></script>
<script src="js/event-view.js" type="module"></script>
<script src="js/venue-view.js" type="module"></script>
<script src="js/edit-post.js" type="module"></script>
</body>
</html>
/pages/404.html
<div class="page">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner sliding">
<div class="left">
<a href="#" class="link back">
<i class="icon icon-back"></i>
<span class="if-not-md">Back</span>
</a>
</div>
<div class="title">Not found</div>
</div>
</div>
<div class="page-content">
<div class="block block-strong inset">
<p>Sorry</p>
<p>Requested content not found.</p>
</div>
</div>
</div>
/pages/discover-view-event.html
<template>
<div class="page" data-name="discover-view-event">
<!-- Top Navbar -->
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="#" class="link icon-only">
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<!-- Page content-->
<div class="page-content">
<div class="loading-fullscreen">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="discover-view-event view-event">
<div class="event-detail-img-box">
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide">
<div class="swiper-image" style="background-color: gray;"></div>
</div>
<div class="swiper-slide">
<div class="swiper-image" style="background-color: gray;"></div>
</div>
</div>
</div>
</div>
<div class="container">
<div class="event-detail">
<div class="event-detail-title"></div>
<div class="event-time-address-wrap mt-3 mb-2">
<div class="event-time-address">
<i class="f7-icons">calendar</i>
<span>
<div class="skeleton-block" style="width: 100px"></div>
</span>
</div>
<div class="event-time-address">
<i class="f7-icons">clock</i>
<span>
<div class="skeleton-block" style="width: 100px"></div>
</span>
</div>
<div class="event-time-address">
<i class="f7-icons">placemark_fill</i>
<span>
<div class="skeleton-block" style="width: 300px"></div>
</span>
</div>
</div>
<div class="event-list-btn mt-1">
<div class="btn w-100 bg-dark"><i class="f7-icons">tickets</i> Buy tickets</div>
</div>
<div class="event-list-btn d-flex">
<div class="btn btn-primary w-100" id="favourite_event" style="display: none;"><i
class="f7-icons">heart_fill</i> Favourite</div>
<div class="btn btn-primary w-100 popup-open" data-popup=".share-listing-popup">
<i class="f7-icons">paperplane_fill</i> Share
</div>
</div>
</div>
<div class="listing-tabs">
<a href="#tab-about" class="tab-link tab-link-active">About</a>
<a href="#tab-entry-details" class="tab-link">Entry & Tickets</a>
</div>
<swiper-container class="tabs">
<swiper-slide id="tab-about" class="tab tab-active">
<div class="swiper-inner-container">
<div class="event-des-wrap">
<p>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
</p>
</div>
</div>
</swiper-slide>
<swiper-slide id="tab-entry-details" class="tab">
<div class="swiper-inner-container">
<div class="event-des-wrap entry-details">
<p>
<div class="skeleton-block" style="width: 200px"></div>
</p>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
</div>
</div>
<!-- Page content-->
<!-- Share Slider -->
<div class="popup share-listing-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Share This</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li><a href="#" target="_blank" id="copy-event-link"><i class="icon f7-icons">link</i> Copy Link</a>
</li>
<li><a href="#" target="_blank" id="share-email-event-link"><i class="icon f7-icons">envelope</i>
Share via Email</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- * Share Slider -->
</div>
</template>
/pages/discover-view-venue.html
<template>
<div class="page" data-name="discover-view-venue">
<!-- Top Navbar -->
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="#" class="link icon-only">
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<!-- Page content-->
<div class="page-content">
<div class="loading-fullscreen">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="discover-view-event">
<div class="event-detail-img-box">
<div class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide">
<div class="swiper-image" style="background-color: gray;"></div>
</div>
<div class="swiper-slide">
<div class="swiper-image" style="background-color: gray;"></div>
</div>
</div>
</div>
</div>
<div class="container">
<div class="event-detail">
<div class="event-detail-title">
<div class="skeleton-block" style="width: 200px"></div>
</div>
<div class="event-time-address-wrap mt-3 mb-2">
<div class="event-time-address">
<i class="f7-icons">placemark_fill</i>
<span>
<div class="skeleton-block" style="width: 200px"></div>
</span>
</div>
</div>
<div class="event-list-btn mt-1">
<div class="btn w-100 bg-dark venue-follow-btn">Follow</div>
</div>
<div class="event-list-btn d-flex">
<div class="btn btn-primary w-100 popup-open" data-popup=".share-listing-popup"><i
class="f7-icons">paperplane_fill</i> Share</div>
</div>
</div>
<div class="listing-tabs">
<a href="#tab-events" class="tab-link tab-link-active">Upcoming Events</a>
<a href="#tab-about" class="tab-link">About</a>
</div>
<swiper-container class="tabs">
<swiper-slide id="tab-events" class="tab tab-active">
<div class="swiper-inner-container">
<div class="grid grid-cols-2 event-listing mt-2">
<a href="#" class="card event-item">
<div class="event-image position-relative">
<div class="image-rectangle" style="background-image: url('assets/img/start01.jpg');"></div>
<div class="event-dates">
<div class="event-date-item">
<p>Feb</p>
<h5>18</h5>
</div>
<div class="event-date-item">
<p>Feb</p>
<h5>21</h5>
</div>
</div>
</div>
<div class="card-content">
<h3 class="event-title">Power Maxed MotoFest Coventry</h3>
<p class="event-info">Starts Thu, 18 Feb 2023</p>
<div class="event-info">NEC Birmingham</div>
</div>
</a>
</div>
</div>
</swiper-slide>
<swiper-slide id="tab-about" class="tab">
<div class="swiper-inner-container">
<div class="event-des-wrap">
<p>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
<div class="skeleton-block" style="width: 95%;padding: .5rem; margin-bottom: 5px;"></div>
</p>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
</div>
</div>
<!-- Page content-->
<!-- Share Slider -->
<div class="popup share-listing-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Share This</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li><a href="#" target="_blank" id="copy-venue-link"><i class="icon f7-icons">link</i> Copy Link</a>
</li>
<li><a href="#" target="_blank" id="share-email-venue-link"><i class="icon f7-icons">envelope</i>
Share via Email</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- * Share Slider -->
</div>
</template>
/pages/discover.html
<template>
<div class="page" data-name="discover">
<!-- Top Navbar -->
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a href="#" class="link icon-only panel-open" data-panel="left">
<i class="icon f7-icons">bars</i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<!-- Page content-->
<div class="page-content discover-page infinite-scroll-content ptr-content ptr-watch-scrollable"
data-ptr-distance="130" data-ptr-mousewheel="true">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
<div class="discovery-wrap">
<form class="searchbar">
<div class="searchbar-inner">
<div class="searchbar-input-wrap">
<input type="search" placeholder="Search" class="discover-search" />
<i class="searchbar-icon"></i>
<!-- <span class="input-clear-button"></span> -->
</div>
<span class="searchbar-disable-button if-not-aurora">Cancel</span>
</div>
</form>
<div class="tabbar-nav pt-1">
<ul>
<li><a href="#featured" class="tab-link tab-link-active" data-id="featured">Featured</a></li>
<li><a href="#events" class="tab-link" data-id="events">Events</a></li>
<li><a href="#venues" class="tab-link" data-id="venues">Venues</a></li>
<li><a href="#users" class="tab-link" data-id="users">Users</a></li>
<li><a href="#vehicles" class="tab-link" data-id="vehicles">Vehicles</a></li>
</ul>
</div>
<div class="container mt-15 mb-15">
<div class="tabs">
<div class="tab tab-active" id="featured">
<div class="featured-section">
<swiper-container slides-per-view="1" loop="true">
<swiper-slide>
<div class="featured-item">
<div class="featured-img"
style="background-image:url('https://www.carevents.com/uk/wp-content/uploads/sites/3/2023/12/3d7fe3f3-a748-4e98-b366-bc93bb51e3ed-1024x576.jpg')">
</div>
<div class="featured-content">
<h2>Kwik Fit British Touring Car Championship – Brands Hatch</h2>
<p>5th-6th Oct 24</p>
<a href="/discover-view-event/91937" class="btn btn-primary ">See
More</a>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
<div class="trending-section">
<h2 class="section-title mt-3 mb-2">Trending Events</h2>
<swiper-container class="event-listing" slides-per-view="2" loop="true"
id="trending-events">
<div class="infinite-scroll-preloader">
<div class="preloader"></div>
</div>
</swiper-container>
</div>
<div class="trending-section">
<h2 class="section-title mt-3 mb-2">Trending Venues</h2>
<swiper-container class="event-listing" slides-per-view="2" loop="true"
id="trending-venues">
<div class="infinite-scroll-preloader">
<div class="preloader"></div>
</div>
</swiper-container>
</div>
</div>
<div class="tab" id="events">
<div class="filter-bar mb-2">
<div class="filter-item popup-open" data-popup=".filter-bydate-popup">
<span>Date</span>
<i class="icon f7-icons">chevron_down</i>
</div>
<div class="filter-item popup-open" data-popup=".filter-bycategory-popup">
<span>Category </span>
<i class="icon f7-icons">chevron_down</i>
</div>
<div class="filter-item popup-open" data-popup=".filter-bylocation-popup">
<span>Location </span>
<i class="icon f7-icons">chevron_down</i>
</div>
</div>
<div class="trending-section">
<div class="grid grid-cols-2 event-listing" id="filtered-events-tab">
<!-- content populates here -->
</div>
</div>
<br />
<div class="infinite-scroll-preloader events-tab">
<div class="preloader"></div>
</div>
</div>
<div class="tab" id="venues">
<div class="filter-bar mb-2">
<div class="filter-item popup-open" data-popup=".filter-bylocation-popup">
<span>Location </span>
<i class="icon f7-icons">chevron_down</i>
</div>
</div>
<div class="trending-section">
<div class="grid grid-cols-2 event-listing" id="filtered-venues-tab">
</div>
</div>
<br />
<div class="infinite-scroll-preloader venues-tab">
<div class="preloader"></div>
</div>
</div>
<div class="tab" id="users">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul id="users-tab">
</ul>
<br />
<div class="infinite-scroll-preloader users-tab">
<div class="preloader"></div>
</div>
</div>
</div>
<div class="tab" id="vehicles">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul id="vehicles-tab">
</ul>
<br />
<div class="infinite-scroll-preloader vehicles-tab">
<div class="preloader"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filter - Date - Slider -->
<div class="popup filter-bydate-popup three-quarter-popup">
<div class="view view-init tab" data-name="filter-bydate" data-url="/discover/">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Filter by Date</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Quick Options</div>
<div class="list" id="date-filters">
<ul>
<li>
<label class="item-checkbox item-content">
<input type="checkbox" name="anytime" value="anytime" checked />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">Anytime</div>
</div>
</label>
</li>
<li>
<label class="item-checkbox item-content">
<input type="checkbox" name="today" value="today" />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">Today</div>
</div>
</label>
</li>
<li>
<label class="item-checkbox item-content">
<input type="checkbox" name="tomorrow" value="tomorrow" />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">Tomorrow</div>
</div>
</label>
</li>
<li>
<label class="item-checkbox item-content">
<input type="checkbox" name="this_weekend" value="this-weekend" />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">This Weekend</div>
</div>
</label>
</li>
</ul>
</div>
<div class="block-title">Select Date Range</div>
<div class="list">
<ul>
<li>
<div class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Owned From</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly"
id="date_from" name="date_from" />
</div>
</div>
</div>
</li>
<li>
<div class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Owned To</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly"
id="date_to" name="date_to" />
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="popup-lower-button">
<button class="button button-large button-fill margin-bottom apply-filters">Apply
Filters</button>
</div>
</div>
</div>
</div>
</div>
<!-- * Filter - Date - Slider -->
<!-- Filter - Category - Slider -->
<div class="popup filter-bycategory-popup three-quarter-popup">
<div class="view view-init tab" data-name="filter-bycategory" data-url="/discover/">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Filter by Category</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<form class="list mt-2" id="category-filters">
<ul>
<li>
<label class="item-checkbox item-content">
<input type="checkbox" name="all" value="0" checked />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">All</div>
</div>
</label>
</li>
</ul>
</form>
<div class="popup-lower-button">
<button class="button button-large button-fill margin-bottom apply-filters">Apply
Filters</button>
</div>
</div>
</div>
</div>
</div>
<!-- * Filter - Category - Slider -->
<!-- Filter - Location - Slider -->
<div class="popup filter-bylocation-popup three-quarter-popup">
<div class="view view-init tab" data-name="filter-bylocation" data-url="/discover/">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Filter by Location</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block location-field">
<p>Location</p>
<input type="text" placeholder="Enter Location" id="autocomplete" />
<input type="hidden" id="lat" />
<input type="hidden" id="lng" />
</div>
<div class="list mt2" id="location-filters">
<ul>
<li>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="location" value="national" checked />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">National</div>
</div>
</label>
</li>
<li>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="location" value="near-me" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">Nearby</div>
</div>
</label>
</li>
<li>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="location" value="25-miles" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">25 Miles</div>
</div>
</label>
</li>
<li>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="location" value="50-miles" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">50 Miles</div>
</div>
</label>
</li>
<li>
<label class="item-radio item-radio-icon-start item-content">
<input type="radio" name="location" value="100-miles" />
<i class="icon icon-radio"></i>
<div class="item-inner">
<div class="item-title">100 Miles</div>
</div>
</label>
</li>
</ul>
</div>
<div class="popup-lower-button">
<button class="button button-large button-fill margin-bottom apply-filters">Apply
Filters</button>
</div>
</div>
</div>
</div>
</div>
<!-- * Filter - Location - Slider -->
</div>
</div>
</template>
/pages/forgot-password.html
<template>
<div class="page" data-name="forgot-password">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="right">
</div>
</div>
</div>
<div class="page-content">
<div class="login-form">
<div class="section">
<h1>Reset Password</h1>
<h4>Enter your email below</h4>
</div>
<form>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Email</div>
<div class="item-input-wrap">
<input type="email" placeholder="Enter email address" required />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="login-buttons">
<a href="#" class="button button-large button-fill margin-bottom">Send reset email</a>
</div>
</form>
</div>
</div>
</div>
</template>
/pages/home.html
<template>
<div class="page no-swipeback" data-name="social">
<div class="navbar navbar-large">
<div class="navbar-inner">
<div class="left">
<a href="#" class="link icon-only panel-open" data-panel="left">
<i class="icon f7-icons">bars</i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="../assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div class="social-tabs toolbar toolbar-bottom tabbar">
<div class="toolbar-inner">
<a href="#tab-latest" class="tab-link tab-link-active" data-type="latest">Latest</a>
<a href="#tab-following" class="tab-link" data-type="following">Following</a>
</div>
</div>
<div class="page-content ptr-content ptr-watch-scrollable home-page social-content infinite-scroll-content"
data-ptr-distance="130" data-ptr-mousewheel="true">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
<div class="tabs">
<div class="tab tab-active" id="tab-latest">
<div class="data virtual-list list-virtual-latest">
</div>
<div class="infinite-scroll-preloader home-posts">
<div class="preloader"></div>
</div>
</div>
<div class="tab" id="tab-following">
<div class="data virtual-list list-virtual-following"></div>
<div class="infinite-scroll-preloader home-following-posts">
<div class="preloader"></div>
</div>
</div>
</div>
</div>
</div>
</template>
/pages/login.html
<template>
<div class="page no-toolbar no-swipeback" data-name="signin">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<!-- <i class="icon icon-back"></i> -->
</a>
</div>
<div class="right">
<a href="/signup-step1/" class="link">
Sign Up
</a>
</div>
</div>
</div>
<div class="page-content login-screen-content">
<div class="login-form">
<div class="section">
<h1>Login</h1>
<h4>Log into your DriveLife Account</h4>
</div>
<form>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Email</div>
<div class="item-input-wrap">
<input type="email" name="username" placeholder="Enter email address" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Password</div>
<div class="item-input-wrap">
<input type="password" name="password" placeholder="Enter password" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="block">
<p class="text-center"><a href="https://www.carevents.com/reset-password/" id="forgot-password">Forgot
Password?</a></p>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom">
Next
</button>
</div>
</form>
</div>
</div>
</div>
</template>
/pages/notifications.html
<template>
<div class="page no-toolbar" data-name="notifications">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
</div>
</div>
<!-- Page content-->
<div class="page-content notification-page ptr-content ptr-watch-scrollable" data-infinite-distance="50"
data-ptr-distance="120">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
<div class="notification-wrap">
<div class="">
<div class="app-notification-title" data-title="Recent" data-id="recent"></div>
<div class="notification-list" id="recent">
</div>
</div>
<div class="">
<div class="app-notification-title" data-title="Last Week" data-id="last-week"></div>
<div class="notification-list" id="this-week">
</div>
</div>
<div class="">
<div class="app-notification-title" data-title="Last 30 days" data-id="last-30"></div>
<div class="notification-list" id="last-30-days">
</div>
</div>
<div class="load-more-notifications btn btn-primary hidden" data-page="1" data-total-pages="NaN">Load
more
</div>
</div>
</div>
<!-- Page content-->
</div>
</template>
/pages/post-edit.html
<template>
<div class="page no-toolbar" data-name="post-edit">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="update-post">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Edit Post</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap">
<input type="hidden" id="edit_post_id" />
<textarea placeholder="Post content" rows="8" id="post_content"></textarea>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
/pages/post-view.html
<template>
<div class="page" data-name="post-view">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div class="page-content">
<div id="post-view-container">
<div class="loading-fullscreen post-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
/pages/profile-edit-account-settings.html
<template>
<div class="page no-toolbar" data-name="profile-edit-account-settings">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Delete your account</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<p>If you would like to completely remove your account, please click below. Please note, it is not
possible to restore an account once it has been deleted.</p>
<p><button class="button button-fill color-red account-delete">Delete Account</button></p>
</div>
</div>
</div>
</template>
/pages/profile-edit-images.html
<template>
<div class="page no-toolbar" data-name="profile-edit-images">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="save-profile-images">
Save
</a>
</div>
</div>
</div>
<div class="page-content profile-edit-images">
<div class="block-title">Profile Image</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<div class="custom-file-upload profile" id="fileUpload">
<input type="file" id="fileuploadInput" name="profile_image" />
<label for="fileuploadInput">
</label>
<span>
<i class="icon f7-icons">cloud_upload</i>
<small>Tap to Upload</small>
</span>
</div>
</div>
<div class="block-title">Cover Image</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<div class="custom-file-upload cover" id="fileUpload">
<input type="file" id="fileuploadInput1" name="cover_image" />
<label for="fileuploadInput1">
</label>
<span>
<i class="icon f7-icons">cloud_upload</i>
<small>Tap to Upload</small>
</span>
</div>
</div>
</div>
</div>
</template>
/pages/profile-edit-mydetails.html
<template>
<div class="page no-toolbar" data-name="profile-edit-mydetails">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="save-details">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Your Details</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<form>
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">First Name *</div>
<div class="item-input-wrap">
<input type="text" name="first_name" placeholder="Enter first name" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Last Name *</div>
<div class="item-input-wrap">
<input type="text" name="last_name" placeholder="Enter last name" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input" id="email-input">
<div class="item-inner">
<div class="item-title item-label">Email * <span class="error-message" id="email-verify-span">Email not
verified</span></div>
<div class="item-input-wrap">
<input type="text" name="email" placeholder="Enter email" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Tel No</div>
<div class="item-input-wrap">
<input type="text" name="tel_no" placeholder="Enter tel no" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</form>
</div>
<div class="block-title">Password</div>
<div class="list list-strong-ios list-dividers-ios inset-ios change-password">
<form>
<ul>
<li class="accordion-item">
<a class="item-link item-content">
<div class="item-inner">
<div class="item-title password-toggle-title">Change Password</div>
</div>
</a>
<div class="accordion-item-content">
<div>
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Current Password *</div>
<div class="item-input-wrap">
<input type="password" name="current_password" placeholder="Enter Current Password" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Password *</div>
<div class="item-input-wrap">
<input type="password" name="password" placeholder="Enter Password" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Confirm Password *</div>
<div class="item-input-wrap">
<input type="password" name="confirm_password" placeholder="Confirm Password" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="button-add-link">
<div class="button button-large button-fill margin-bottom" id="update_password">Update Password</div>
</div>
</div>
</li>
</ul>
</form>
</div>
</div>
</div>
</template>
/pages/profile-edit-socials.html
<template>
<div class="page no-toolbar" data-name="profile-edit-socials">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="save-profile-socials">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Social Links</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<form>
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Instagram</div>
<div class="item-input-wrap">
<input type="text" name="social_instagram" placeholder="Enter Instagram Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Facebook</div>
<div class="item-input-wrap">
<input type="text" name="social_facebook" placeholder="Enter Facebook Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">TikTok</div>
<div class="item-input-wrap">
<input type="text" name="social_tiktok" placeholder="Enter Tiktok Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">YouTube</div>
<div class="item-input-wrap">
<input type="text" name="social_youtube" placeholder="Enter YouTube Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Mivia</div>
<div class="item-input-wrap">
<input type="text" name="social_mivia" placeholder="Enter Mivia Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Custodian Garage / Car link</div>
<div class="item-input-wrap">
<input type="text" name="social_custodian" placeholder="Enter Custodian Username" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</form>
</div>
<div class="block-title">Other Links</div>
<div class="list list-strong-ios list-dividers-ios inset-ios social-other-links">
<ul></ul>
<div class="button-add-link">
<div class="button button-fill margin-bottom popup-open" data-popup=".add-link-popup">Add Link</div>
</div>
</div>
</div>
</div>
</template>
/pages/profile-edit-username.html
<template>
<div class="page no-toolbar" data-name="profile-edit-username">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="save-username">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block-title">Change Username
<div class="item-input-info change-username-info">Your username can be changed every 30 days</div>
</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<form>
<ul>
<li class="item-content item-input">
<div class="item-inner profile-edit-view">
<div class="item-title item-label">Username</div>
<div class="item-input-wrap">
<input type="text" name="username" placeholder="Enter desired Username" id="lowercaseInput" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
</ul>
</form>
<br />
<div class="item-input-info change-username-info" id="username-editable"></div>
</div>
</div>
</div>
</template>
/pages/profile-edit.html
<template>
<div class="page" data-name="profile-edit">
<div class="navbar navbar-nologo">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="#" class="link icon-only">
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li><a href="/profile-edit-images/">Edit Profile Images</a></li>
<li><a href="/profile-edit-socials/">Manage Social Links</a></li>
<li><a href="/profile-edit-mydetails/">My Details</a></li>
<li><a href="/profile-edit-username/">Username</a></li>
<li><a href="/profile-edit-account-settings/">Account Settings</a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
/pages/profile-garage-edit.html
<template>
<div class="page" data-name="profile-garage-edit">
<div class="navbar navbar-nologo">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="/profile-garage-vehicle-add/" class="top-right-action">
+ Add
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="garage-list profile-landing-page">
<div class="listview-title garage-sub-title">Current Vehicles</div>
<ul class="listview image-listview media transparent flush pt-1" id="garage-edit-current-list">
</ul>
<div class="listview-title garage-sub-title">Past Vehicles</div>
<ul class="listview image-listview media transparent flush pt-1" id="garage-edit-past-list">
</ul>
</div>
</div>
</div>
</template>
/pages/profile-garage-vehicle-add.html
<template>
<div class="page no-toolbar" data-name="profile-garage-vehicle-add">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="submit-add-vehicle-form">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<form id="addVehicleForm">
<div class="block-title">Vehicle Image</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<div class="custom-file-upload" id="fileUpload">
<input type="file" id="fileuploadInput" name="vehicle_image" />
<label for="fileuploadInput">
<span>
<i class="icon f7-icons">cloud_upload</i>
<small>Tap to Upload</small>
</span>
</label>
</div>
</div>
<div class="block-title">Vehicle Details</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Vehicle Make *</div>
<div class="item-input-wrap input-dropdown-wrap">
<select class="input-with-value" name="vehicle_make" required>
<option disabled selected value="0">Please Select</option>
<option value="Acura">Acura</option>
<option value="Alfa Romeo">Alfa Romeo</option>
<option value="Aston Martin">Aston Martin</option>
<option value="Audi">Audi</option>
<option value="Alpine ">Alpine</option>
<option value="Bentley">Bentley</option>
<option value="BMW">BMW</option>
<option value="Bugatti">Bugatti</option>
<option value="Buick">Buick</option>
<option value="Cadillac">Cadillac</option>
<option value="Chevrolet">Chevrolet</option>
<option value="Chrysler">Chrysler</option>
<option value="Citroën">Citroën</option>
<option value="Dodge">Dodge</option>
<option value="Ferrari">Ferrari</option>
<option value="Fiat">Fiat</option>
<option value="Ford">Ford</option>
<option value="GMC">GMC</option>
<option value="Honda">Honda</option>
<option value="Hyundai">Hyundai</option>
<option value="Infiniti">Infiniti</option>
<option value="Jaguar">Jaguar</option>
<option value="Jeep">Jeep</option>
<option value="Kia">Kia</option>
<option value="Lamborghini">Lamborghini</option>
<option value="Land Rover">Land Rover</option>
<option value="Lexus">Lexus</option>
<option value="Lotus">Lotus</option>
<option value="Lincoln">Lincoln</option>
<option value="Maserati">Maserati</option>
<option value="Mazda">Mazda</option>
<option value="McLaren">McLaren</option>
<option value="Mercedes-Benz">Mercedes-Benz</option>
<option value="Mini">Mini</option>
<option value="Mitsubishi">Mitsubishi</option>
<option value="Nissan">Nissan</option>
<option value="Peugeot">Peugeot</option>
<option value="Porsche">Porsche</option>
<option value="Ram">Ram</option>
<option value="Renault">Renault</option>
<option value="Rolls-Royce">Rolls-Royce</option>
<option value="Saab">Saab</option>
<option value="Subaru">Subaru</option>
<option value="Suzuki">Suzuki</option>
<option value="Tesla">Tesla</option>
<option value="TVR">TVR</option>
<option value="Toyota">Toyota</option>
<option value="Volkswagen">Volkswagen</option>
<option value="Volvo">Volvo</option>
</select>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Model *</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle model" name="vehicle_model" required />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Variant</div>
<div class="item-input-wrap">
<input type="text" placeholder="Add any vehicle variant info" name="vehicle_variant" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Registration</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle reg" name="vehicle_reg" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Colour</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle colour" name="vehicle_colour" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Short description -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Short Description</div>
<div class="item-input-wrap">
<textarea name="vehicle_description" placeholder="Add a short description of your vehicle"
maxlength="200"></textarea>
</div>
</div>
</li>
</ul>
</div>
<div class="block-title">Ownership Dates</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Ownership *</div>
<div class="item-input-wrap input-dropdown-wrap">
<select class="input-with-value" name="vehicle_ownership">
<option value="current">Current Vehicle</option>
<option value="past">Past Vehicle</option>
</select>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li>
<div class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Owned From</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly" id="owned-from"
name="vehicle_owned_from" />
</div>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li>
<div class="item-content item-input" id="owned-to-block" style="display: none;">
<div class="item-inner">
<div class="item-title item-label">Owned To</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly" id="owned-to"
name="vehicle_owned_to" />
</div>
</div>
</div>
</li>
<!-- * Input -->
</ul>
</div>
<div class="block-title">Vehicle Tagging</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="toggle-inline">
<div>Allow this vehicle to be discovered & tagged via it's registration</div>
<div>
<label class="toggle">
<input type="checkbox" name="vehicle_tagging" />
<span class="toggle-icon"></span>
</label>
</div>
</li>
</ul>
</div>
</form>
</div>
</div>
</template>
/pages/profile-garage-vehicle-edit.html
<template>
<div class="page no-toolbar" data-name="profile-garage-vehicle-edit">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="top-right-action" id="submit-vehicle-form">
Save
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="loading-fullscreen">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<form id="vehicleForm">
<input type="hidden" name="garage_id" value="" />
<div class="block-title">Vehicle Image</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<div class="custom-file-upload" id="fileUpload">
<input type="file" id="fileuploadInput" name="vehicle_image" />
<label for="fileuploadInput">
</label>
<span>
<i class="icon f7-icons">cloud_upload</i>
<small>Tap to Upload</small>
</span>
</div>
</div>
<div class="block-title">Vehicle Details</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Vehicle Make *</div>
<div class="item-input-wrap input-dropdown-wrap">
<select class="input-with-value" name="vehicle_make" required>
<option disabled selected value="0">Please Select</option>
<option value="Acura">Acura</option>
<option value="Alfa Romeo">Alfa Romeo</option>
<option value="Aston Martin">Aston Martin</option>
<option value="Audi">Audi</option>
<option value="Alpine ">Alpine</option>
<option value="Bentley">Bentley</option>
<option value="BMW">BMW</option>
<option value="Bugatti">Bugatti</option>
<option value="Buick">Buick</option>
<option value="Cadillac">Cadillac</option>
<option value="Chevrolet">Chevrolet</option>
<option value="Chrysler">Chrysler</option>
<option value="Citroën">Citroën</option>
<option value="Dodge">Dodge</option>
<option value="Ferrari">Ferrari</option>
<option value="Fiat">Fiat</option>
<option value="Ford">Ford</option>
<option value="GMC">GMC</option>
<option value="Honda">Honda</option>
<option value="Hyundai">Hyundai</option>
<option value="Infiniti">Infiniti</option>
<option value="Jaguar">Jaguar</option>
<option value="Jeep">Jeep</option>
<option value="Kia">Kia</option>
<option value="Lamborghini">Lamborghini</option>
<option value="Land Rover">Land Rover</option>
<option value="Lexus">Lexus</option>
<option value="Lotus">Lotus</option>
<option value="Lincoln">Lincoln</option>
<option value="Maserati">Maserati</option>
<option value="Mazda">Mazda</option>
<option value="McLaren">McLaren</option>
<option value="Mercedes-Benz">Mercedes-Benz</option>
<option value="Mini">Mini</option>
<option value="Mitsubishi">Mitsubishi</option>
<option value="Nissan">Nissan</option>
<option value="Peugeot">Peugeot</option>
<option value="Porsche">Porsche</option>
<option value="Ram">Ram</option>
<option value="Renault">Renault</option>
<option value="Rolls-Royce">Rolls-Royce</option>
<option value="Saab">Saab</option>
<option value="Subaru">Subaru</option>
<option value="Suzuki">Suzuki</option>
<option value="Tesla">Tesla</option>
<option value="TVR">TVR</option>
<option value="Toyota">Toyota</option>
<option value="Volkswagen">Volkswagen</option>
<option value="Volvo">Volvo</option>
</select>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Model *</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle model" name="vehicle_model" required />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Variant</div>
<div class="item-input-wrap">
<input type="text" placeholder="Add any vehicle variant info" name="vehicle_variant" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Registration</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle reg" name="vehicle_reg" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Colour</div>
<div class="item-input-wrap">
<input type="text" placeholder="Your vehicle colour" name="vehicle_colour" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<!-- * Input -->
<!-- Short description -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Short Description</div>
<div class="item-input-wrap">
<textarea name="vehicle_description" placeholder="Add a short description of your vehicle"
maxlength="200"></textarea>
</div>
</div>
</li>
</ul>
</div>
<div class="block-title">Ownership Dates</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<!-- Input -->
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Ownership *</div>
<div class="item-input-wrap input-dropdown-wrap">
<select class="input-with-value" name="vehicle_ownership">
<option value="current">Current Vehicle</option>
<option value="past">Past Vehicle</option>
</select>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li>
<div class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Owned From</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly" id="owned-from"
name="vehicle_owned_from" />
</div>
</div>
</div>
</li>
<!-- * Input -->
<!-- Input -->
<li>
<div class="item-content item-input" id="owned-to-block">
<div class="item-inner">
<div class="item-title item-label">Owned To</div>
<div class="item-input-wrap">
<input type="text" placeholder="Select date" readonly="readonly" id="owned-to"
name="vehicle_owned_to" />
</div>
</div>
</div>
</li>
<!-- * Input -->
</ul>
</div>
<div class="block-title">Vehicle Tagging</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="toggle-inline">
<div>Allow this vehicle to be discovered & tagged via it's registration</div>
<div>
<label class="toggle">
<input type="checkbox" name="vehicle_tagging" />
<span class="toggle-icon"></span>
</label>
</div>
</li>
</ul>
</div>
<div class="block">
<button class="mt-4 button button-large button-fill color-red" id="delete-vehicle" type="button">Delete
Vehicle</button>
</div>
</form>
</div>
</div>
</template>
/pages/profile-garage-vehicle-view.html
<template>
<div class="page" data-name="profile-garage-vehicle-view">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div class="page-content profile-landing-page infinite-scroll-content" data-infinite-distance="50">
<div class="loading-fullscreen garage">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="vehicle-profile-background" style="background-color:gray;">
<a class="vehicle-profile-image" href="/profile/" style="background-color:gray;"></a>
</div>
<div class="profile-garage-intro text-center">
<h1>
<div class="skeleton-block" style="width: 100px"></div>
</h1>
<p class="garage-owned-information">
<div class="skeleton-block" style="width: 100px"></div>
</p>
<p class="garage-vehicle-description">
<div class="skeleton-block" style="width: 100px"></div>
</p>
<div class="profile-links-edit garage d-flex">
<!-- <div class="profile-link dark-bg garage-add-post">Add Post</div> -->
</div>
</div>
<div class="profile-lower">
<div class="profile-tabs garage-view">
<a href="#tab-1" class="tab-link tab-link-active" id="garage-posts">Posts</a>
<a href="#tab-2" class="tab-link" id="garage-tags">Tags</a>
</div>
<swiper-container class="tabs">
<swiper-slide id="tab-1" class="tab">
<div class="swiper-inner-container">
<div class="profile-grid" id="garage-posts-tab">
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader garage-posts-tab">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
<swiper-slide id="tab-2" class="tab">
<div class="swiper-inner-container">
<div class="profile-grid" id="garage-tags-tab">
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader garage-tags-tab">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
</div>
</div>
</template>
/pages/profile-view.html
<template>
<div class="page" data-name="profile-view">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons open-qr-modal">qrcode</i>
</a>
</div>
</div>
</div>
<div class="page-content profile-landing-page infinite-scroll-content view-page ptr-content ptr-watch-scrollable"
data-infinite-distance="50" data-ptr-distance="130">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
<div class="loading-fullscreen">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
<div class="profile-background" style="background-color:gray;">
</div>
<div class="profile-head">
<div class="profile-image" style="background-color:gray;"></div>
<h3 class="name profile-username">
<div class="skeleton-block" style="width: 100px"></div>
</h3>
<h5 class="subtext profile-name">
<div class="skeleton-block" style="width: 100px"></div>
</h5>
<div class="mt-1">
<div class="btn w-70 bg-dark user-follow-btn">
<div class="skeleton-block" style="width: 80px"></div>
</div>
</div>
</div>
<div class="profile-links">
<div class="profile-links-external d-flex">
<a class="profile-link social-link link external " id="instagram" target="_blank"
href="https://www.instagram.com/"><img src="assets/img/icon-instagram.svg" />
<span>Instagram</span></a>
<a class="profile-link social-link link external " id="facebook" target="_blank"
href="https://www.facebook.com"><img src="assets/img/icon-facebook.svg" />
<span>Facebook</span></a>
<a class="profile-link social-link link external " id="tiktok" target="_blank"
href="https://www.tiktok.com"><img src="assets/img/icon-tiktok.svg" /> <span>Tiktok</span></a>
<a class="profile-link social-link link external " id="youtube" target="_blank"
href="https://www.youtube.com"><img src="assets/img/icon-youtube.svg" />
<span>YouTube</span></a>
<a class="profile-link social-link popup-open" data-popup=".links-popup"><img
src="assets/img/icon-link.svg?v=1.4" />
<span>More</span></a>
</div>
</div>
<div class="profile-lower">
<div class="profile-tabs">
<a href="#profileview-posts-tab" class="tab-link tab-link-active" id="my-posts">Posts</a>
<a href="#profileview-garage-tab" class="tab-link" id="my-garage">Garage</a>
<a href="#profileview-tags-tab" class="tab-link" id="my-tags">Tags</a>
</div>
<swiper-container class="tabs">
<swiper-slide id="profileview-posts-tab" class="tab tab-active">
<div class="swiper-inner-container">
<div class="profile-grid list virtual-list" id="profile-view-grid-posts">
<!-- Content generates here -->
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader posts-tab view-profile">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
<swiper-slide id="profileview-garage-tab" class="tab">
<div class="swiper-inner-container">
<div class="listview-title garage-sub-title">Current Vehicles</div>
<ul
class="listview image-listview media transparent flush pt-1 pview-current-vehicles-list">
<!-- Content generates here -->
</ul>
<div class="listview-title garage-sub-title mt-2">Past Vehicles</div>
<ul class="listview image-listview media transparent flush pt-1 pview-past-vehicles-list">
<!-- Content generates here -->
</ul>
</div>
</swiper-slide>
<swiper-slide id="profileview-tags-tab" class="tab">
<div class="swiper-inner-container">
<div class="profile-grid list virtual-list" id="profile-view-grid-tags">
<!-- Content generates here -->
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader tags-tab view-profile">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
</div>
<div class="popup links-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">More Links</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block profile-external-links">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
/pages/profile.html
<template>
<div class="page" data-name="profile">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a href="#" class="link icon-only panel-open" data-panel="left">
<i class="icon f7-icons">bars</i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div
class="page-content no-swipeback profile-landing-page infinite-scroll-content my-profile ptr-content ptr-watch-scrollable"
data-infinite-distance="50" data-ptr-distance="120">
<div class="ptr-preloader">
<div class="preloader"></div>
<div class="ptr-arrow"></div>
</div>
<div class="profile-background" style="background-color: gray;"></div>
<div class="profile-head">
<div class="profile-image" style="background-color: gray;"></div>
<h3 class="name profile-username">@</h3>
<h5 class="subtext profile-name"></h5>
</div>
<div class="profile-links">
<div class="profile-links-edit d-flex">
<!-- <a class="profile-link popup-open" data-popup=".edit-profile-popup">Edit Profile</a> -->
<a class="profile-link" href="/profile-edit/">Edit Profile</a>
<a class="profile-link dark-bg" href="/profile-garage-edit/">Edit Garage</a>
</div>
<div class="profile-links-external d-flex">
<a class="profile-link social-link link external " id="instagram" target="_blank"
href="https://www.instagram.com/"><img src="assets/img/icon-instagram.svg" /> <span>Instagram</span></a>
<a class="profile-link social-link link external " id="facebook" target="_blank"
href="https://www.facebook.com"><img src="assets/img/icon-facebook.svg" /> <span>Facebook</span></a>
<a class="profile-link social-link link external " id="tiktok" target="_blank"
href="https://www.tiktok.com"><img src="assets/img/icon-tiktok.svg" /> <span>Tiktok</span></a>
<a class="profile-link social-link link external " id="youtube" target="_blank"
href="https://www.youtube.com"><img src="assets/img/icon-youtube.svg" /> <span>YouTube</span></a>
<a class="profile-link social-link popup-open" data-popup=".links-popup"><img
src="assets/img/icon-link.svg?v=1.4" />
<span>More</span></a>
</div>
</div>
<div class="profile-lower">
<div class="profile-tabs">
<a href="#profile-posts-tab" class="tab-link tab-link-active" id="my-posts">Posts</a>
<a href="#profile-garage-tab" class="tab-link" id="my-garage">Garage</a>
<a href="#profile-tags-tab" class="tab-link" id="my-tags">Tags</a>
</div>
<swiper-container class="tabs">
<swiper-slide id="profile-posts-tab" class="tab tab-active">
<div class="swiper-inner-container">
<div class="profile-grid" id="profile-grid-posts">
<!-- Content generates here -->
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader posts-tab">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
<swiper-slide id="profile-garage-tab" class="tab ">
<div class="swiper-inner-container">
<div class="listview-title garage-sub-title">Current Vehicles</div>
<ul class="listview image-listview media transparent flush pt-1 current-vehicles-list">
<!-- Content generates here -->
</ul>
<div class="listview-title garage-sub-title mt-2">Past Vehicles</div>
<ul class="listview image-listview media transparent flush pt-1 past-vehicles-list">
<!-- Content generates here -->
</ul>
</div>
</swiper-slide>
<swiper-slide id="profile-tags-tab" class="tab">
<div class="swiper-inner-container">
<div class="profile-grid" id="profile-grid-tags">
<!-- Content generates here -->
</div>
<!-- Preloader for infinite scroll -->
<div class="infinite-scroll-preloader tags-tab">
<div class="preloader"></div>
</div>
</div>
</swiper-slide>
</swiper-container>
</div>
</div>
<div class="popup links-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">More Links</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block profile-external-links">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Profile Slider -->
<div class="popup edit-profile-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="navbar">
<div class="navbar-inner">
<div class="title">Edit Profile</div>
<div class="right">
<!-- Link to close popup -->
<a class="link popup-close"><i class="icon f7-icons">xmark</i></a>
</div>
</div>
</div>
<div class="page-content">
<div class="block">
<div class="list links-list list-outline-ios list-strong-ios list-dividers-ios">
<ul>
<li><a href="/profile-edit-images/">Edit Profile Images</a></li>
<li><a href="/profile-edit-socials/">Manage Social Links</a></li>
<li><a href="/profile-edit-mydetails/">My Details</a></li>
<li><a href="/profile-edit-username/">Username</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- * Edit Profile Slider -->
</div>
</template>
/pages/search.html
<template>
<div class="page" data-name="search">
<!-- Top Navbar -->
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a class="link back">
<i class="icon icon-back"></i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="#" class="link icon-only">
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<!-- Page content-->
<div class="page-content infinite-scroll-content">
<div class="discovery-wrap">
<div class="searchbar">
<div class="searchbar-inner">
<div class="searchbar-input-wrap">
<input type="search" placeholder="Search" class="discover-search" id="discover-search"
aria-autocomplete="none" />
<i class="searchbar-icon"></i>
<!-- <span class="input-clear-button"></span> -->
</div>
<span class="searchbar-disable-button if-not-aurora">Cancel</span>
</div>
<ul id="search-history" class="search-history"></ul> <!-- Container for the search history -->
</div>
<div class="tabbar-nav pt-1">
<ul>
<li><a href="#top-results" class="tab-link tab-link-active" data-type="all">Top</a></li>
<li><a href="#events-results" class="tab-link" data-type="events">Events</a></li>
<li><a href="#venues-results" class="tab-link" data-type="venues">Venues</a></li>
<li><a href="#users-results" class="tab-link" data-type="users">Users</a></li>
<li><a href="#vehicles-results" class="tab-link" data-type="vehicles">Vehicles</a></li>
</ul>
</div>
<div class="tabs">
<!-- Tab - Top Results -->
<div class="tab tab-active" id="top-results">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</div>
</div>
<!-- Tab - Events -->
<div class="tab" id="events-results">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul>
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</ul>
</div>
</div>
<!-- Tab - Venues -->
<div class="tab" id="venues-results">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul>
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</ul>
</div>
</div>
<!-- Tab - Users -->
<div class="tab" id="users-results">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul>
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</ul>
</div>
</div>
<!-- Tab - Vehicles -->
<div class="tab" id="vehicles-results">
<div class="list list-outline-ios list-strong-ios list-dividers-ios mt-2">
<ul>
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Page content-->
</div>
</template>
/pages/signup-complete.html
<template>
<div class="page no-navbar no-toolbar no-swipeback" data-name="signup-complete">
<div class="page-content env-padding">
<div class="login-form">
<div class="block">
<div class="section"><img src="assets/img/start01.jpg" />
<h1 class="">Welcome to DriveLife</h1>
<h4>Early Access</h4>
</div>
<p>Brought to you be CarEvents.com, DriveLife brings to you a world of car events, venues, media and more.</p>
<p>This is currently an early access app and will be updated with tonnes of new features in the coming weeks,
including video and gallery uploading, our online store and more.</p>
<p>Thank you for joining us on a journey to empower car enthusiasts!</p>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom" id="signup-complete">
Continue
</button>
</div>
</div>
</div>
</div>
</template>
/pages/signup-step1.html
<template>
<div class="page no-toolbar no-swipeback" data-name="signup-step1">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<!-- <i class="icon icon-back"></i> -->
</a>
</div>
<div class="right">
<a href="/login/" class="link">
Login
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="login-form">
<div class="section">
<h1>Register</h1>
<h4>Create your DriveLife Account</h4>
</div>
<form id="sign-up-step1">
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">First Name</div>
<div class="item-input-wrap">
<input type="text" name="first_name" placeholder="Enter first name" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Last Name</div>
<div class="item-input-wrap">
<input type="text" name="last_name" placeholder="Enter last name" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Email</div>
<div class="item-input-wrap">
<input type="email" name="email" placeholder="Enter email address" />
<span class="input-clear-button"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Password</div>
<div class="item-input-wrap">
<input type="password" name="password" placeholder="Enter password" />
<span class="toggle-password"></span>
</div>
</div>
</li>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-title item-label">Confirm Password</div>
<div class="item-input-wrap">
<input type="password" name="confirm_password" placeholder="Enter password" />
<span class="toggle-password"></span>
</div>
</div>
</li>
</ul>
</div>
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="agree_terms" />
<i class="icon icon-checkbox"></i>
<div class="item-title">I agree to the <a class="popup-open" data-popup=".terms-popup">Terms &
Conditions</a></div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="agree_privacy" />
<i class="icon icon-checkbox"></i>
<div class="item-title">I agree to the <a class="popup-open" data-popup=".privacy-popup">Privacy
Policy</a></div>
</label>
</li>
</ul>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom">
Next
</button>
</div>
</form>
<!-- Popups - Terms -->
<div class="popup terms-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="page-content">
<div class="privacy-statement text-center">
<h3>Terms & Conditions</h3>
<p><strong>INTRODUCTION</strong></p>
<p>1.1 Car Events Ltd. (“CarEvents.com”, “we”, “us”, “our”) is a company registered in England and
Wales under company
number 12698619, with registered offices at The Motorist Lennerton Lane, Sherburn In Elmet, Leeds,
England, LS25
6JE.</p>
<p>1.2 We operate an online and mobile App service where you can search for and purchase car show and
related event
tickets. No tickets are sold by us, we simply facilitate the sale process via a direct link to the
show organiser’s
Stripe and/or Paypal accounts.</p>
<p>1.3 This “Purchase Policy” sets out the terms and conditions applicable to purchases of Tickets via
any of the
CarEvents.com platforms. If you are making a purchase online, this Purchase Policy also incorporates
our website
Terms of Use.</p>
<p>1.4 We promote the sale of Tickets and associated products and services on behalf of event
organisers, promoters,
venues, car clubs and other persons involved in the organisation of events (“Event Partner(s)”).
Please note that we
are not responsible for organising or delivering the events themselves.</p>
<p>1.5 It is the responsibility of the Event Organisers to set the number and type of Tickets
available via the
CarEvents.com online organiser dashboard tool on an event by event basis.</p>
<p>1.6 We promote the sale of Tickets from numerous online interfaces (desktop, mobile website and
app). All of our
interfaces access the same ticketing system and ticket inventory, therefore Tickets for popular
events may sell-out
quickly. Occasionally, additional Tickets for sold-out events may become available prior to events.
However, we do
not control Ticket inventory or its availability.</p>
<p>1.7 We can’t offer any exchanges or refunds if the event is going ahead on the date originally
planned. If the date
of an event is changed, your tickets will be transferred to the new date. If the date does change,
you may request a
refund by emailing the event organiser directly (within a minimum of 14 days before the event takes
place). Refunds
are not guaranteed and are at the event organiser’s discretion.</p>
<p>1.8 Nothing in this Purchase Policy affects your statutory rights as a consumer. For further
information about your
statutory rights, please contact Citizens Advice.</p>
<p>1.9 We act solely as the event orgnaiser’s fulfilment partner and are not responsible for any
aspects of your
purchase from the event partner, save for delivery of tickets to you. If you have any queries or
complaints
regarding your purchase of tickets from any of our event partners or other third parties, please
contact them
directly.</p>
<p><strong>YOUR ACCOUNT AND REGISTRATION</strong></p>
<p>2.1 In order to set up a CarEvents.com account to purchase or sell Tickets you must:</p>
<p>(a) be at least 18 years old (or the age of legal capacity in the country of purchase) and able to
enter into
legally binding contracts; and</p>
<p>(b) follow the instructions to set up a password-protected account providing your correct full name
and email
address (all your details must be kept up to date at all times).</p>
<p>2.2 In order to setup a CarEvents.com account to purchase a ticket, you must be at least 16 years
old, however
please note that the contract (and associated rights and obligations) for the purchase of that
Ticket remains
between you and the Event Partner.</p>
<p>2.3 If you are making purchases on behalf of a company or other legal entity, you represent and
warrant that you
have the authority to bind that company or other legal entity (and this Purchase Policy and
references to “you”
refer and apply to that company or other legal entity).</p>
<p>2.4 Please refer to our Privacy Notice and Cookies Policy for more details on how we use and
protect your personal
data. If we are investigating your account, or if we are investigated ourselves, you agree to comply
fully with our
requests for information about you and your purchases.</p>
<p>2.5 You must not create or use multiple accounts with the purpose or intention of circumventing any
of the terms of
this Purchase Policy or concealing your identity or other personal details.</p>
<p>2.6 You must not use our website or app, your account and/or any of our related services (together
the “CE
Services”) for any unlawful purpose or in any unlawful manner. If we discover or suspect that you
have used or are
using or attempting to use the CE Services in such a way that a criminal offence has been, is being
or might be
committed, we are required by law to report your identity and details of such activity to the
relevant authorities
(and any relevant Event Partner).</p>
<p>2.7 We reserve the right to terminate your account and/or cancel any of your orders and/or prohibit
you from making
future orders or using the CE Services in future if:</p>
<p>(a) any abusive or threatening behaviour is carried out by you or on your behalf or via your
account;</p>
<p>(b) we suspect any fraudulent activity or other illegal activity is carried out by you or on your
behalf or via
your account;</p>
<p>(c) we suspect any unauthorised use of your account or other unauthorised activity is carried out
by you or on your
behalf or via your account;</p>
<p>(d) we are ordered to do so by any legal or regulatory authority; and/or</p>
<p>(e) you otherwise breach the terms of this Purchase Policy or any other applicable policies or
terms and conditions
(including any applicable Event Partner’s terms and conditions).</p>
<p>2.8 You may close your account by emailing members@carevents.com. However, please note that such
closure shall not
take effect until after any events that you have purchased Tickets for have taken place.</p>
<p>2.9 Termination of your account and/or cancellations of any purchases under this Purchase Policy
shall not affect
our or your rights and liabilities which have accrued prior to and including the date of such
termination or
cancellation.</p>
<p><strong>LEGALLY BINDING CONTRACT</strong></p>
<p>3.1 In order to make a purchase from us or any of our event partners, you must be at least 18 years
old (or the age
of legal capacity in the country of purchase) and able to enter into legally binding contracts. If
you are
purchasing online, you must also have a CarEvents.com account and a valid credit or debit card
issued in your name.
</p>
<p>3.2 Any purchase from us or our event partners forms a legally binding contract that is subject to:
(i) this
Purchase Policy; (ii) any special terms and conditions stated to be applicable to a ticket and/or
event; (iii) other
terms and conditions of the Event Partner(s) and/or event; and (iv) any venue terms and conditions
(including
conditions of entry). You should read this Purchase Policy carefully before you make a purchase.</p>
<p>3.3 By purchasing one or more Items from us or our event partners, you acknowledge that you have
read, understood
and agree to be bound by the terms and conditions of this Purchase Policy. If you do not agree with
this Purchase
Policy or any other applicable terms and conditions, or if you cannot comply with any of them, then
you must not
make a purchase.</p>
<p>3.4 We reserve the right from time to time to make changes to this Purchase Policy. Where we make
any such changes,
we shall post the updated version of this Purchase Policy on our website. Therefore, we recommend
you check this
Purchase Policy regularly to stay informed of its current terms and conditions. All purchases are
subject to the
applicable version of this Purchase Policy that was published at the time of purchase. If you do not
agree with any
revised version of this Purchase Policy, or if you cannot comply with it, then you must not make a
purchase.</p>
<p><strong>PRICES, PAYMENT AND PLACING ORDERS</strong></p>
<p>4.1 Accepted methods of payment include Paypal, Visa and MasterCard debit or credit cards (via
Stripe)</p>
<p>4.2 Your contract for purchase starts once we have confirmed your order and ends immediately after
completion of
the event for which you have purchased Item(s).</p>
<p>4.3 If you do not receive an order confirmation after submitting payment information, or if you
experience an error
message or service interruption after submitting payment information, it is your responsibility to
confirm via your
CarEvents.com account or contacting the Event Partner whether or not your order has been placed.
Only you may be
aware of any problems that may occur during the purchase process. We will not be responsible for any
costs or losses
you incur if you assume that an order was or was not placed because you failed to receive an order
confirmation.</p>
<p>4.4 All purchases are subject to credit or debit card verification (if applicable), other security
checks. Your
order may be cancelled if it has not passed the relevant verification process or if payment is not
received in full.
In rare circumstances, if your payment is recalled by the associated bank or payment provider, we
reserve the right
to cancel and refund any order for which an order confirmation has been sent. We accept no
responsibility or
liability for such cancellations, as these are outside our control.</p>
<p>4.5 It is prohibited to obtain or attempt to obtain any Items through unauthorised use of any
robot, spider or
other automated device or software, or through unauthorised framing or linking to any website, or
through any other
illegal or unauthorised activity. We reserve the right to cancel any orders that we reasonably
suspect to have been
made in breach of this Purchase Policy, without any notice to you, and any and all Items obtained as
part of such
orders will be void.</p>
<p>4.6 To prevent fraud and protect us, we reserve the right to carry out checks and/or request that
additional
information be provided in order to verify purchases. We reserve the right to cancel any orders that
we reasonably
suspect to have been made fraudulently, without any notice to you, and any and all Items obtained as
part of such
orders will be void.</p>
<p>4.7 Please ensure that you read the full Item description details and are happy with your selection
prior to
purchase as we may be unable to rectify issues arising as a result of your mistake.</p>
<p><strong>DELIVERY</strong></p>
<p>6.1 All CarEvents.com tickets are supplied as an e-delivery, you are responsible for either (i)
ensuring that you
log in to your CarEvents.com account and download your Tickets in good time prior to the event; or
(ii) providing a
valid email address for e-delivery of Tickets and ensuring that you are able to receive delivery of
the Tickets by
email (for example by ensuring that your email mailbox does not reject, bounce or otherwise prevent
any relevant
emails from being delivered, and by checking your email mailbox regularly (including junk or spam
folders).</p>
<p><strong>TICKET RIGHTS AND OBLIGATIONS</strong></p>
<p>8.1 Any Ticket you purchase via CE Services remains the property of the relevant Event Partner and
is a personal
revocable licence which may be withdrawn, and admission refused, at any time.</p>
<p><strong>TICKET RESTRICTIONS</strong></p>
<p>9.1 When purchasing Tickets from an Event Partner, you are limited to a specified number of Tickets
for each event.
This number is included on the first purchase page and is verified with every order. This policy is
in effect to
discourage and prevent unfair ticket buying practices. Tickets may be restricted to a maximum number
per person (or
business, as applicable), per credit or debit card and, for some events or tours, a restriction may
apply per
household as well. We reserve the right to cancel any order(s) for Tickets purchased in excess of
the relevant
limits without notice.</p>
<p>9.2 Tickets may be sold subject to certain restrictions on entry or use, such as a minimum age for
entry. Any such
restrictions will be displayed or otherwise notified to you before or at the time of booking. It is
your
responsibility to ensure that you read all notifications and other important information displayed
or notified to
you as part of the purchase process. We will not be responsible if you or any guests under your
booking are refused
admission because of a failure to meet or prove that you/they meet any restrictions (e.g. a minimum
age
requirement).</p>
<p>9.3 You are not entitled to purchase any Tickets as a trader acting in the course of business with
the intention of
reselling your Tickets for profit unless formal written permission is given by us and the relevant
Event Partner in
advance. If we discover or have reason to suspect that you have purchased and intend to resell, or
have sold Tickets
in breach of this clause, we reserve the right to cancel your Tickets without notice.</p>
<p>9.4 You may not resell or transfer your Tickets if prohibited by law. In addition, Event Partners
may restrict or
prohibit the resale or transfer of tickets for some events. Any resale or transfer (or attempted
resale or transfer)
of a ticket in breach of the applicable law or any restrictions or prohibition imposed by an Event
Partner is
grounds for seizure or cancellation of that Ticket.</p>
<p>9.5 Tickets purchased from us may not:</p>
<p>(a) be used for advertising, promotions, contests or sweepstakes (or for other such similar
commercial gain);
and/or</p>
<p>(b) be combined with any hospitality, travel or accommodation service and/or any other merchandise,
product or
service to create a package for sale or other distribution,</p>
<p>unless formal written permission is given by us and the relevant Event Partner in advance and
provided that even if
such permission is granted, use of our or any Event Partner’s trade marks and other intellectual
property is subject
to the express prior written consent of the owner.</p>
<p><strong>EVENT TIMINGS AND ADMISSIONS</strong></p>
<p>10.1 Please note that advertised start times of events are subject to change.</p>
<p>10.2 Tickets are sold subject to the Event Partner’s right to alter or vary the programme of an
event due to events
or circumstances beyond its reasonable control without being obliged to refund monies or exchange
tickets, unless
such change is a material alteration as described in clause 11, in which case the provisions of that
clause shall
apply.</p>
<p>10.3 Generally, every effort to admit latecomers will be made at a suitable break in the event, but
admission
cannot always be guaranteed.</p>
<p>10.4 The event venue may conduct security searches of you and other patrons for safety and security
purposes and/or
may refuse admission to patrons (including you) breaching or suspected of breaching any terms and
conditions of the
event or any Event Partner.</p>
<p>10.5 Admission to all events is subject to the terms of admission of the relevant venue, and
certain items (e.g.
laser pens, mobile phones, dogs (except guide dogs) and patrons’ own food and drink) may be
prohibited. Please check
with the venue directly. The unauthorised use of photographic and/or recording equipment at events
may also be
prohibited. The use of drones or similar equipment for any reason in, at or near the event venue
without written
permission from the event partner is strictly prohibited.</p>
<p>10.6 Breach of any of venue terms and conditions or any unacceptable behaviour likely to cause
damage, nuisance or
injury shall entitle the Event Partner to eject you from the venue.</p>
<p>10.7 Event Partners reserve the right to refuse admission to the venue, or to remove any person
from the venue for
reasons of public safety, any unacceptable behaviour likely to cause damage, nuisance or injury, or
for any breach
of the Event Partners´ terms and conditions.</p>
<p>10.8 Unless expressly authorised by the relevant Event Partner, there will be no pass-outs or
re-admissions of any
kind.</p>
<p>10.9 By attending an event, you and other patrons understand and agree to being photographed,
filmed and/or
recorded in relation to the event and/or for safety and security, including filming by the police.
You and other
patrons understand and agree that resulting photographs, videos, audio recordings and/or audiovisual
recordings may
be used in any and all media for any purpose at any time throughout the world (however, you may
object to such use
by specific request to privacy@CarEvents.com).</p>
<p>10.10 Prolonged exposure to loud music or noise may damage your hearing and we advise you and all
patrons to wear
adequate ear protection at events.</p>
<p>10.11 Special effects, which may include sound, audio-visual, pyrotechnic effects or lighting
effects may be
featured at an event, which may not be suitable for those with photosensitive epilepsy, or similar
conditions.</p>
<p><strong>EVENT CANCELLATIONS AND ALTERATIONS</strong></p>
<p>11.1 If an event is cancelled, rescheduled or materially altered, we will use reasonable endeavours
to notify you
once we have received the relevant information and authorisation from our Event Partner. However, we
cannot
guarantee that you will be informed of such cancellation, rescheduling or alteration before the date
of the event.
It is your responsibility to ascertain whether an event has been cancelled, rescheduled or altered
and the date and
time of any rescheduled event.</p>
<p>11.2 Cancellation: If an event is cancelled altogether, we’ll usually just refund your tickets
automatically. We
refund the face value for each ticket – you’ll see a credit onto your card after we’ve emailed you
about the
cancellation with a timescale.</p>
<p>11.3 Rescheduling: If an event for which you have purchased Tickets or Packages is rescheduled,
Tickets and
Packages will usually be valid for the new date (or you will be offered Tickets or Packages of a
value corresponding
with your original Tickets or Packages for the rescheduled event, subject to availability). If the
date of an event
is changed, you may request a refund by emailing info@CarEvents.com. You must email at least 5 days
before the
event.</p>
<p>11.4 Material Alteration: If an event for which you have purchased Tickets or Packages is
“materially altered” (as
defined in clause 11.5 below), Tickets and Packages will usually be valid for the altered event (or
you will be
offered Tickets or Packages of a value corresponding with your original Tickets or Packages for the
altered event,
subject to availability). If you notify the event partner within the specified deadline that you do
not wish to
attend the altered event, you should be able to cancel your order and obtain a refund of the Sale
Price of your
Tickets or Packages (Service Charges and Order Processing Fees are non-refundable).</p>
<p>11.5 For the purposes of this Purchase Policy, a “material alteration” is a change (other than a
rescheduling)
which, in our and the relevant Event Partner’s reasonable opinion, makes the event materially
different to the event
that purchasers of Tickets, taken generally, could reasonably expect. In particular, please note
that the following
are not deemed to be “material alterations”: adverse weather conditions; changes of any advertised
event
contributor; changes to the line-up of any multi-performer event (such as a festival); curtailment
of the event
where the majority of an event is performed in full; and delays to the starting of the performance
of an event.</p>
<p>11.6 To claim a refund under clause 11.2, 11.3 or 11.4, please contact the event partner directly.
</p>
<p>11.7 Refunds will be made using the same means of payment as you used for the initial purchase.</p>
<p><strong>STATUTORY RIGHT TO CANCEL</strong></p>
<p>13.1 Tickets and Packages cannot be cancelled, exchanged or refunded after purchase, save in the
circumstances set
out in clause 11 or 12.</p>
<p>13.2 To meet the relevant cancellation deadline, it is sufficient for you to send your
communication concerning
your exercise of the right to cancel before the cancellation period, as set out by the event partner
has expired.
</p>
<p>13.3 If you cancel a purchase under this clause 13, the event partner will generally reimburse to
you all payments
received from you in relation to the relevant cancelled event.</p>
<p><strong>INTERPRETATION</strong></p>
<p>14.1 The terms “including”, “include”, “in particular”, “e.g.” or any similar expression shall be
construed as
illustrative and shall not limit the sense of the words, description, definition, phrase or term
preceding those
terms.</p>
<p>14.2 The headings used within this Purchase Policy are for reference purposes only and do not
affect its
interpretation. Clauses references in these terms and conditions are references to the clauses of
these terms and
conditions of this Purchase Policy.</p>
<p>14.3 Capitalised terms in this Purchase Policy shall have the special meaning ascribed to them as
set out within
this Purchase Policy.</p>
<p><strong>WARRANTIES AND INDEMNITIES</strong></p>
<p>15.1 You represent and warrant that the information that you submit to us in relation to your
account and in your
use of the CarEvents.com services is true, accurate and complete and you will not use any false
information,
including contact information. You further warrant and represent that you are at least 18 years old
(or the age of
legal capacity in the country of purchase) and can enter into legally binding contracts for the
purchase of Tickets.
</p>
<p>15.2 You represent and warrant that in using our website, you shall comply with all applicable laws
and
regulations, along with the terms of this Purchase Policy and any other applicable terms and
conditions.</p>
<p>15.3 You hereby indemnify and hold harmless us and our affiliates along with their respective
officers, directors,
employees and agents (the “Indemnified Parties”) against any losses, damages, expenses (including
reasonable legal
fees), liabilities, claims and/or demands suffered by any Indemnified Parties arising out of or in
connection with
your breach of this Purchase Policy or any other applicable terms and conditions, breach of any
applicable laws or
regulations, or breach of any third party rights.</p>
<p><strong>LIMITATION OF LIABILITY</strong></p>
<p>16.1 To the maximum extent permitted by law, we (including our affiliates, parent undertakings,
subsidiaries, and
their respective officers, directors, employees, agents, legal representatives and sub-contractors)
and our relevant
Event Partners shall not be liable for any loss, injury or damage to any person (including you) or
property
howsoever caused (including by us and/or by the Event Partner):</p>
<p>(a) in any circumstances where there is no breach of contract or a legal duty of care owed by us or
the relevant
Event Partner;</p>
<p>(b) in circumstances where such loss or damage is not directly as a result of any such breach (save
for death or
personal injury resulting from our or an Event Partner’s negligence); or</p>
<p>(c) to the extent that any increase in any loss or damage results from your negligence or breach by
you of any of
the terms of this Purchase Policy and/or any other applicable terms and conditions and/or any
applicable laws or
regulations.</p>
<p>16.2 To the maximum extent permitted by law, we (including our affiliates, parent undertakings,
subsidiaries, and
their respective officers, directors, employees, agents, legal representatives and sub-contractors)
and our relevant
Event Partners, shall not be liable for any indirect or consequential losses or loss of data,
profits, revenue,
earnings, goodwill, reputation, enjoyment or opportunity, or for distress, or any exemplary, special
or punitive
damages, arising directly or indirectly from your use of the CarEvents.com services and/or any
purchases made under
this Purchase Policy. In particular please note that:</p>
<p>(a) personal arrangements and expenditure, including travel, accommodation, hospitality and other
costs and
expenses incurred by you relating to an event which have been arranged by you are at your own risk,
and neither we
nor the relevant Event Partners shall be responsible or liable to you for any wasted or
unrecoverable costs or
expenditure in relation to such personal arrangements, even if caused as a result of the
cancellation, rescheduling
or alteration of an event for which you have purchased tickets under this Purchase Policy; and</p>
<p>(b) neither we nor any relevant Event Partner shall be responsible or liable to you for any loss of
enjoyment or
amenity, including where an event has been cancelled, rescheduled or altered; and</p>
<p>(c) neither we nor any relevant Event Partner shall be responsible or liable to you (and you will
not be entitled
to any refund) if admission to a venue or event is refused or revoked at any time as a result of
your breach of any
Event Partner’s terms and conditions.</p>
<p>16.3 Unless otherwise stated in this clause 16, our and any relevant Event Partner’s liability to
you in connection
with an event (including, but not limited to, for any cancellation, rescheduling or alteration of an
event) and any
Items you have purchased shall be limited to the price paid by you for the Items, including any
Service Charges but
excluding any Order Processing Fees.</p>
<p>16.4 We are not responsible for any internet connection errors experienced while using the
CarEvents.com services.
</p>
<p>16.5 We are not responsible for the actions or failures of any Venue, promoter or other Event
Partner. Under no
circumstances shall we be liable for death or personal injury suffered by you or your guests arising
out of
attendance at an event, unless caused by our negligence. Neither shall we be liable for any loss or
damage sustained
to your property or belongings, or those of any guests under your booking, attending an event.</p>
<p>16.6 We will not be liable to you for failure to perform any of our obligations under this Purchase
Policy to the
extent that the failure is caused by a force majeure event (meaning any cause beyond our reasonable
control
including without limitation, acts of God, war, insurrection, riot, civil disturbances, acts of
terrorism, fire,
explosion, flood, theft of essential equipment, malicious damage, strike, lock out, weather, third
party injunction,
national defence requirements, acts or regulations of national or local governments). This clause
does not affect
the terms of any clauses specifically providing for a right of refund.</p>
<p>16.7 Nothing in this Purchase Policy seeks to exclude or limit our or any Event Partner’s liability
for death or
personal injury caused by our or any Event Partner’s negligence, fraud or other type of liability
which cannot by
law be excluded or limited.</p>
<p><strong>QUERIES, COMPLAINTS AND DISPUTE RESOLUTION</strong></p>
<p>17.1 If we need to contact you, we will use your CarEvents.com account contact details. It is your
responsibility
to inform us immediately of any changes to your contact details, whether before or after receipt of
Items. In
particular, please ensure that you provide us with a valid email address as this is our preferred
method of
contacting you. You should also be aware that your email mailbox settings may treat our emails as
junk, so remember
to check your junk and/or spam folders.</p>
<p>17.2 If you have any queries or complaints regarding your purchase, please contact the event
partner directly,
quoting any order reference numbers.</p>
<p>17.3 Please note that we do not tolerate aggressive or abusive behaviour towards our staff or
representatives, or
unreasonable demands or persistence being used (including any threat, abuse or harassment towards
our staff or
representatives in any form or any media). We reserve the right to take such action we deem
reasonably necessary in
the circumstances to address any such behaviour towards our staff or representatives.</p>
<p><strong>GENERAL</strong></p>
<p>18.1 If we delay or fail to enforce any of the provisions of this Purchase Policy, it shall not
mean that we have
waived our right to do so.</p>
<p>18.2 We shall be entitled to assign our rights and obligations under this Purchase Policy provided
that your rights
are not adversely affected.</p>
<p>18.3 If any provision of this Purchase Policy is found by a competent court to be invalid or
unenforceable, that
provision shall be deemed to be omitted from this Purchase Policy and this shall not prevent the
other provisions
from continuing to remain in full force and operate separately.</p>
<p>18.4 If any provision of this Purchase Policy is or becomes illegal, invalid or unenforceable
pursuant to the law
of any applicable jurisdiction, this shall not affect or impair the legality, validity or
enforceability in that
jurisdiction of any other provision of this Purchase Policy.</p>
<p>18.5 Any of our and our Event Partners’ affiliates, successors, or assigns may enforce these terms
in accordance
with the provisions of the Contracts (Rights of Third Parties) Act 1999. Except as provided in the
previous
sentence, this Purchase Policy does not create any right enforceable by any person who is not a
party to it but does
not affect any right or remedy that a third party has which exists or is available apart from the
Contracts (Rights
of Third Parties) Act 1999.</p>
<p>18.6 Nothing contained within this Purchase Policy and no action taken by you or us pursuant to
this Purchase
Policy shall create, or be deemed to create, a partnership, joint venture, or establish a
relationship of principal
and agent.</p>
<p>18.7 Any notice provided under this Purchase Policy shall be delivered upon receipt and shall be
deemed to have
been received at the time of delivery (if delivered by hand, registered post or courier) or at the
time of
transmission (if delivered by email).</p>
<p>18.8 This Purchase Policy shall be governed by and construed in all respects in accordance with
English law and
both you and we agree to submit to the non-exclusive jurisdiction of the English courts in relation
to any dispute
arising out of or in connection with this Purchase Policy.</p>
<p><em>Publication Date: 16 January 2024.</em></p>
</div>
</div>
</div>
</div>
</div>
<!-- Popups - Privacy -->
<div class="popup privacy-popup three-quarter-popup">
<div class="view">
<div class="page">
<div class="page-content">
<div class="privacy-statement text-center">
<h2>Privacy Policy</h2>
<p>This Privacy Policy describes Our policies and procedures on the collection, use and disclosure
of Your information
when You use the Service and tells You about Your privacy rights and how the law protects You.</p>
<p>We use Your Personal data to provide and improve the Service. By using the Service, You agree to
the collection and
use of information in accordance with this Privacy Policy.</p>
<h2>Interpretation and Definitions</h2>
<h2>Interpretation</h2>
<p>The words of which the initial letter is capitalized have meanings defined under the following
conditions. The
following definitions shall have the same meaning regardless of whether they appear in singular or
in plural.</p>
<h2>Definitions</h2>
<p>For the purposes of this Privacy Policy:</p>
<ul>
<li><strong>Account</strong> means a unique account created for You to access our Service or parts
of our Service.
</li>
<li><strong>Company</strong> (referred to as either “CarEvents.com”, “the Company”, “We”, “Us” or
“Our” in this
Agreement) refers to Car Events Ltd, The Motorist Lennerton Lane, Sherburn In Elmet, Leeds,
England, LS25 6JE.
</li>
<li><strong>Cookies</strong> are small files that are placed on Your computer, mobile device or
any other device by
a website, containing the details of Your browsing history on that website among its many uses.
</li>
<li><strong>Country</strong> refers to: United Kingdom</li>
<li><strong>Device</strong> means any device that can access the Service such as a computer, a
cellphone or a
digital tablet.</li>
<li><strong>Personal Data</strong> is any information that relates to an identified or
identifiable individual.</li>
<li><strong>Service</strong> refers to the Website.</li>
<li><strong>Service Provider</strong> means any natural or legal person who processes the data on
behalf of the
Company. It refers to third-party companies or individuals employed by the Company to facilitate
the Service, to
provide the Service on behalf of the Company, to perform services related to the Service or to
assist the Company
in analyzing how the Service is used.</li>
<li><strong>Usage Data</strong> refers to data collected automatically, either generated by the
use of the Service
or from the Service infrastructure itself (for example, the duration of a page visit).</li>
<li><strong>Website</strong> refers to CarEvents.com, accessible from <a
href="https://www.carevents.com" target="_blank"
rel="nofollow noopener">https://www.carevents.com</a></li>
<li><strong>You</strong> means the individual accessing or using the Service, or the company, or
other legal entity
on behalf of which such individual is accessing or using the Service, as applicable.</li>
</ul>
<h2>Collecting and Using Your Personal Data</h2>
<h2>Types of Data Collected</h2>
<h3>Personal Data</h3>
<p>While using Our Service, We may ask You to provide Us with certain personally identifiable
information that can be
used to contact or identify You. Personally identifiable information may include, but is not
limited to:</p>
<ul>
<li>Email address</li>
<li>First name and last name</li>
<li>Address, State, Province, ZIP/Postal code, City</li>
<li>Usage Data</li>
</ul>
<h3>Usage Data</h3>
<p>Usage Data is collected automatically when using the Service.</p>
<p>Usage Data may include information such as Your Device’s Internet Protocol address (e.g. IP
address), browser type,
browser version, the pages of our Service that You visit, the time and date of Your visit, the
time spent on those
pages, unique device identifiers and other diagnostic data.</p>
<p>When You access the Service by or through a mobile device, We may collect certain information
automatically,
including, but not limited to, the type of mobile device You use, Your mobile device unique ID,
the IP address of
Your mobile device, Your mobile operating system, the type of mobile Internet browser You use,
unique device
identifiers and other diagnostic data.</p>
<p>We may also collect information that Your browser sends whenever You visit our Service or when
You access the
Service by or through a mobile device.</p>
<h3>Tracking Technologies and Cookies</h3>
<p>We use Cookies and similar tracking technologies to track the activity on Our Service and store
certain
information. Tracking technologies used are beacons, tags, and scripts to collect and track
information and to
improve and analyze Our Service. The technologies We use may include:</p>
<ul>
<li><strong>Cookies or Browser Cookies.</strong> A cookie is a small file placed on Your Device.
You can instruct
Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You
do not accept
Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your
browser setting so
that it will refuse Cookies, our Service may use Cookies.</li>
<li><strong>Flash Cookies.</strong> Certain features of our Service may use local stored objects
(or Flash Cookies)
to collect and store information about Your preferences or Your activity on our Service. Flash
Cookies are not
managed by the same browser settings as those used for Browser Cookies. For more information on
how You can delete
Flash Cookies, please read “Where can I change the settings for disabling, or deleting local
shared objects?”
available at <a
href="https://helpx.adobe.com/flash-player/kb/disable-local-shared-objects-flash.html#main_Where_can_I_change_the_settings_for_disabling__or_deleting_local_shared_objects_"
target="_blank"
rel="external nofollow noopener">https://helpx.adobe.com/flash-player/kb/disable-local-shared-objects-flash.html#main_Where_can_I_change_the_settings_for_disabling__or_deleting_local_shared_objects_</a>
</li>
<li><strong>Web Beacons.</strong> Certain sections of our Service and our emails may contain small
electronic files
known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that
permit the Company,
for example, to count users who have visited those pages or opened an email and for other
related website
statistics (for example, recording the popularity of a certain section and verifying system and
server integrity).
</li>
</ul>
<p>Cookies can be “Persistent” or “Session” Cookies. Persistent Cookies remain on Your personal
computer or mobile
device when You go offline, while Session Cookies are deleted as soon as You close Your web
browser. You can learn
more about cookies here: <a href="https://www.termsfeed.com/blog/cookies/" target="_blank"
rel="noopener">All About
Cookies by TermsFeed</a>.</p>
<p>We use both Session and Persistent Cookies for the purposes set out below:</p>
<ul>
<li><strong>Necessary / Essential Cookies</strong>Type: Session CookiesAdministered by: UsPurpose:
These Cookies are
essential to provide You with services available through the Website and to enable You to use
some of its
features. They help to authenticate users and prevent fraudulent use of user accounts. Without
these Cookies, the
services that You have asked for cannot be provided, and We only use these Cookies to provide
You with those
services.</li>
<li><strong>Cookies Policy / Notice Acceptance Cookies</strong>Type: Persistent
CookiesAdministered by: UsPurpose:
These Cookies identify if users have accepted the use of cookies on the Website.</li>
<li><strong>Functionality Cookies</strong>Type: Persistent CookiesAdministered by: UsPurpose:
These Cookies allow us
to remember choices You make when You use the Website, such as remembering your login details or
language
preference. The purpose of these Cookies is to provide You with a more personal experience and
to avoid You having
to re-enter your preferences every time You use the Website.</li>
</ul>
<p>For more information about the cookies we use and your choices regarding cookies, please visit
our Cookies Policy
or the Cookies section of our Privacy Policy.</p>
<h2>Use of Your Personal Data</h2>
<p>The Company may use Personal Data for the following purposes:</p>
<ul>
<li><strong>To provide and maintain our Service</strong>, including to monitor the usage of our
Service.</li>
<li><strong>To manage Your Account:</strong> to manage Your registration as a user of the Service.
The Personal Data
You provide can give You access to different functionalities of the Service that are available
to You as a
registered user.</li>
<li><strong>For the performance of a contract:</strong> the development, compliance and
undertaking of the purchase
contract for the products, items or services You have purchased or of any other contract with Us
through the
Service.</li>
<li><strong>To contact You:</strong> To contact You by email, telephone calls, SMS, or other
equivalent forms of
electronic communication, such as a mobile application’s push notifications regarding updates or
informative
communications related to the functionalities, products or contracted services, including the
security updates,
when necessary or reasonable for their implementation.</li>
<li><strong>To provide You</strong> with news, special offers and general information about other
goods, services
and events which we offer that are similar to those that you have already purchased or enquired
about unless You
have opted not to receive such information.</li>
<li><strong>To manage Your requests:</strong> To attend and manage Your requests to Us.</li>
<li><strong>For business transfers:</strong> We may use Your information to evaluate or conduct a
merger,
divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or
all of Our assets,
whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in
which Personal Data
held by Us about our Service users is among the assets transferred.</li>
<li><strong>For other purposes</strong>: We may use Your information for other purposes, such as
data analysis,
identifying usage trends, determining the effectiveness of our promotional campaigns and to
evaluate and improve
our Service, products, services, marketing and your experience.</li>
</ul>
<p>We may share Your personal information in the following situations:</p>
<ul>
<li><strong>With Service Providers:</strong> We may share Your personal information with Service
Providers to
monitor and analyze the use of our Service, to contact You.</li>
<li><strong>For business transfers:</strong> We may share or transfer Your personal information in
connection with,
or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all
or a portion of
Our business to another company.</li>
<li><strong>With Affiliates:</strong> We may share Your information with Our affiliates, in which
case we will
require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and
any other
subsidiaries, joint venture partners or other companies that We control or that are under common
control with Us.
</li>
<li><strong>With business partners:</strong> We may share Your information with Our business
partners to offer You
certain products, services or promotions.</li>
<li><strong>With other users:</strong> when You share personal information or otherwise interact
in the public areas
with other users, such information may be viewed by all users and may be publicly distributed
outside.</li>
<li><strong>With Your consent</strong>: We may disclose Your personal information for any other
purpose with Your
consent.</li>
</ul>
<h2>Retention of Your Personal Data</h2>
<p>The Company will retain Your Personal Data only for as long as is necessary for the purposes set
out in this
Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with
our legal
obligations (for example, if we are required to retain your data to comply with applicable laws),
resolve disputes,
and enforce our legal agreements and policies.</p>
<p>The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally
retained for a
shorter period of time, except when this data is used to strengthen the security or to improve the
functionality of
Our Service, or We are legally obligated to retain this data for longer time periods.</p>
<h2>Transfer of Your Personal Data</h2>
<p>Your information, including Personal Data, is processed at the Company’s operating offices and in
any other places
where the parties involved in the processing are located. It means that this information may be
transferred to — and
maintained on — computers located outside of Your state, province, country or other governmental
jurisdiction where
the data protection laws may differ than those from Your jurisdiction.</p>
<p>Your consent to this Privacy Policy followed by Your submission of such information represents
Your agreement to
that transfer.</p>
<p>The Company will take all steps reasonably necessary to ensure that Your data is treated securely
and in accordance
with this Privacy Policy and no transfer of Your Personal Data will take place to an organization
or a country
unless there are adequate controls in place including the security of Your data and other personal
information.</p>
<h2>Disclosure of Your Personal Data</h2>
<h3>Business Transactions</h3>
<p>If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be
transferred. We will
provide notice before Your Personal Data is transferred and becomes subject to a different Privacy
Policy.</p>
<h3>Law enforcement</h3>
<p>Under certain circumstances, the Company may be required to disclose Your Personal Data if
required to do so by law
or in response to valid requests by public authorities (e.g. a court or a government agency).</p>
<h3>Other legal requirements</h3>
<p>The Company may disclose Your Personal Data in the good faith belief that such action is
necessary to:</p>
<ul>
<li>Comply with a legal obligation</li>
<li>Protect and defend the rights or property of the Company</li>
<li>Prevent or investigate possible wrongdoing in connection with the Service</li>
<li>Protect the personal safety of Users of the Service or the public</li>
<li>Protect against legal liability</li>
</ul>
<h2>Security of Your Personal Data</h2>
<p>The security of Your Personal Data is important to Us, but remember that no method of
transmission over the
Internet, or method of electronic storage is 100% secure. While We strive to use commercially
acceptable means to
protect Your Personal Data, We canno
/pages/signup-step2.html
<template>
<div class="page no-toolbar no-swipeback" data-name="signup-step2">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<!-- <i class="icon icon-back"></i> -->
</a>
</div>
<div class="right">
</div>
</div>
</div>
<div class="page-content">
<div class="login-form">
<div class="section">
<h1>Username</h1>
<h4>Create your DriveLife username</h4>
</div>
<form id="sign-up-step2">
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li class="item-content item-input">
<div class="item-inner">
<div class="item-input-wrap item-input-center">
<input type="text" name="username" value="" placeholder="Username" />
</div>
</div>
</li>
</ul>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom">
Next
</button>
</div>
</form>
</div>
</div>
</div>
</template>
/pages/signup-step3.html
<template>
<div class="page no-toolbar no-swipeback" data-name="signup-step3">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<!-- <i class="icon icon-back"></i> -->
</a>
</div>
<div class="right">
</div>
</div>
</div>
<div class="page-content">
<div class="login-form">
<div class="section">
<h1>About You</h1>
<h4>What type of content revs your engine?<br /><em>Tick all that apply</em></h4>
</div>
<form id="car-selection-form">
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="0" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Sports Cars / Supercars / Exotics</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="1" />
<i class="icon icon-checkbox"></i>
<div class="item-title">JDM & Modded</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="2" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Classic Cars</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="3" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Electric Cars</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="4" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Hotrods & Dragsters</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="5" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Race Cars</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="car_type" value="6" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Motorbikes</div>
</label>
</li>
</ul>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom">Next</button>
</div>
</form>
</div>
</div>
</div>
</template>
/pages/signup-step4.html
<template>
<div class="page no-toolbar no-swipeback" data-name="signup-step4">
<div class="navbar">
<div class="navbar-inner">
<div class="left">
<a class="link back">
<!-- <i class="icon icon-back"></i> -->
</a>
</div>
<div class="right">
</div>
</div>
</div>
<div class="page-content">
<div class="login-form">
<div class="section">
<h1>About You</h1>
<h4>Which best describes you?<br /><em>Tick all that apply</em></h4>
</div>
<form id="interest-selection-form">
<div class="list list-strong-ios list-dividers-ios inset-ios">
<ul>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="0" checked />
<i class="icon icon-checkbox"></i>
<div class="item-title">I just love cars</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="1" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Photographer / Videographer</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="2" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Club Owner / Admin</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="3" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Event Organiser</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="4" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Automotive Venue</div>
</label>
</li>
<li>
<label class="item-checkbox item-checkbox-icon-start item-content">
<input type="checkbox" name="interest" value="5" />
<i class="icon icon-checkbox"></i>
<div class="item-title">Automotive Business Owner</div>
</label>
</li>
</ul>
</div>
<div class="login-buttons">
<button type="submit" class="button button-large button-fill margin-bottom">Next</button>
</div>
</form>
</div>
</div>
</div>
</template>
/pages/store.html
<template>
<div class="page" data-name="store">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="left">
<a href="#" class="link icon-only panel-open" data-panel="left">
<i class="icon f7-icons">bars</i>
</a>
</div>
<div class="middle">
<div class="header-logo"><img src="assets/img/logo-dark.png" /></div>
</div>
<div class="right">
<a href="#" class="link icon-only open-qr-modal">
<i class="icon f7-icons">qrcode</i>
</a>
<a href="/notifications/" class="link icon-only">
<div class="notification-count"></div>
<i class="icon f7-icons">bell</i>
</a>
</div>
</div>
</div>
<div class="page-content">
<div class="store-coming-soon text-center">
<i class="icon f7-icons">cart</i>
<h2>Coming Soon</h2>
<p>We're just putting the finishing touches on our store - Check back soon for a world of automotive merch,
clothing and more!</p>
</div>
</div>
</div>
</template>
/js/api/auth.js
import {
API_URL,
TIMEOUT_MS_HIGHER
} from "./consts.js"
import store from "../store.js"
export const getSessionUser = async () => {
// get session from somewhere
if (store.state.user) {
return store.state.user
}
// check in local storage
const session = window.localStorage.getItem('token')
if (session) {
return session
}
return null
}
export const getUserDetails = async (token) => {
try {
let url = `${API_URL}/wp-json/app/v1/get-user-profile/`
let response = await fetch(url, {
method: "POST",
mode: 'cors',
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
})
const data = await response.json()
if (response.status !== 200) throw new Error(data.message)
return data
} catch (error) {
console.error('Error fetching user details:', error)
return null
}
}
export const verifyUser = async (credentials) => {
try {
// Convert the credentials object to a URL query string
const queryParams = new URLSearchParams(credentials).toString()
// Make a GET request with the query parameters
const response = await fetch(`${API_URL}/wp-json/ticket_scanner/v1/verify_user/?${queryParams}`, {
method: "GET",
mode: 'cors',
headers: {
"Content-Type": "application/json",
},
})
if (response.ok) {
return await response.json()
} else {
console.error('Failed to verify user:', response.statusText)
return null
}
} catch (error) {
console.error(error)
return error
}
}
export const verifyEmail = async (token) => {
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/verify-email`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token
}),
})
const data = await response.json()
return data
} catch (error) {
console.error('Error verifying email:', error)
return null
}
}
export const sendEmailVerification = async () => {
try {
const user = await getSessionUser()
if (!user) return
const response = await fetch(`${API_URL}/wp-json/app/v1/resend-verification-email`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
}),
})
const data = await response.json()
return data
} catch (error) {
console.error('Error sending email verification:', error)
return null
}
}
export const handleSignUp = async (user) => {
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/register-user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(user),
})
const data = await response.json()
if (response.status !== 201) {
return {
success: false,
message: data.message,
code: data.code,
}
}
return data
} catch (error) {
return error
}
}
export const updateUsername = async (username, user_id = null) => {
const controller = new AbortController()
const signal = controller.signal
try {
if (!user_id) {
const user = await getSessionUser();
if (!user) return {
success: false,
message: 'User id not provided'
};
user_id = user.id
}
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-username`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user_id,
username
}),
signal
})
const data = await response.json()
return data
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your username, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const updateContentIds = async (content_ids, user_id) => {
const response = await fetch(`${API_URL}/wp-json/app/v1/update-selected-content`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id,
content_ids
}),
})
const data = await response.json()
return data
}
export const updateAboutUserIds = async (content_ids, user_id) => {
const response = await fetch(`${API_URL}/wp-json/app/v1/update-about-content`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id,
content_ids
}),
})
const data = await response.json()
return data
}
export const updatePassword = async (new_password, old_password) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
new_password,
old_password
}),
signal
})
const data = await response.json()
return data
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your password, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const updateUserDetails = async (details, email_changed) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser();
if (!user) return;
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-user-details`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
...details,
email_changed
}),
signal
});
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your details, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
};
export const getUserById = async (id) => {
try {
let url = `${API_URL}/wp-json/app/v1/get-user-profile-next`
let response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: id
}),
})
const data = await response.json()
if (response.status !== 200) throw new Error(data.message)
return data
} catch (error) {
console.error('Error fetching user details:', error)
return null
}
}
export const getUserNotifications = async (load_old_notifications = false) => {
try {
const user = await getSessionUser()
if (!user) {
throw new Error('Session user not found')
}
const response = await fetch(`${API_URL}/wp-json/app/v1/get-notifications`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
load_old_notifications
}),
})
const data = await response.json()
return data
} catch (error) {
console.error('Error fetching user notifications:', error)
return null
}
}
export const getNotificationCount = async () => {
const user = await getSessionUser()
if (!user || !user.id) return
const response = await fetch(`${API_URL}/wp-json/app/v1/get-new-notifications-count`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id
}),
});
const data = await response.json();
return data;
}
export const markMultipleNotificationsAsRead = async (notificationIds) => {
const user = await getSessionUser();
if (!user) return null;
const response = await fetch(`${API_URL}/wp-json/app/v1/bulk-notifications-read`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id
}),
});
const data = await response.json();
return data;
};
export const deleteUserAccount = async (password) => {
const user = await getSessionUser()
if (!user || !user.id) return
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/delete_account`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
password
}),
})
const data = await response.json()
return data
} catch (error) {
return {
success: false,
message: 'Oops, unable to delete your account. Please try again later.'
}
}
}
/js/api/consts.js
export const API_URL = 'https://www.carevents.com/uk'
// export const API_URL = 'https://wordpress-889362-4267074.cloudwaysapps.com/uk'
export const TIMEOUT_MS_LOW = 15 * 1000 // 15 seconds
export const TIMEOUT_MS_HIGH = 30 * 1000 // 30 seconds
export const TIMEOUT_MS_HIGHER = 60 * 1000 // 60 seconds
export const sendRNMessage = ({
page,
type,
user_id,
association_id,
association_type
}) => {
if (typeof window.ReactNativeWebView === 'undefined') {
console.warn(`This is not a react native webview, failed to send message: ${type} - ${user_id}`)
}
try {
if (window !== undefined && window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type,
page,
user_id,
association_id,
association_type
}))
} else {
console.warn(`Failed to send message: ${type} - ${user_id}`)
}
} catch (e) {
console.error(e)
}
}
/js/api/discover.js
import {
getSessionUser
} from "./auth.js";
import {
API_URL
} from "./consts.js";
/**
* Fetches the discover data
*
* @param {string} search
* @param {'users' | 'events' | 'venues' | 'all'} type
* @param {number} page default 1
*
*/
export const getDiscoverData = async (search, type, page = 1, signal) => {
const user = await getSessionUser();
const response = await fetch(`${API_URL}/wp-json/app/v1/discover-search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
search,
user_id: user?.id,
page,
type,
per_page: 10
}),
signal
});
const data = await response.json();
return data;
};
export const fetchEvent = async (eventId) => {
let user;
try {
user = await getSessionUser();
} catch (e) {
console.error("Error fetching user no session");
}
const response = await fetch(`${API_URL}/wp-json/app/v1/get-event`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
event_id: eventId,
user_id: user?.id
}),
});
const data = await response.json();
if (response.status !== 200) {
throw new Error(data.message);
}
return data;
};
export const fetchTrendingEvents = async (page, paginate = false, filters = null) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser();
if (!user) {
throw new Error('Session user not found');
}
const response = await fetch(`${API_URL}/wp-json/app/v1/get-events-trending`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
page,
per_page: 10,
paginate,
filters
}),
signal
});
const data = await response.json();
if (!data) {
throw new Error('Failed to fetch trending events');
}
return data;
} catch (error) {
console.error(error);
return null;
}
};
export const fetchTrendingVenues = async (page, paginate = false, filters = '{}') => {
try {
const user = await getSessionUser();
if (!user) {
throw new Error('Session user not found');
}
const response = await fetch(`${API_URL}/wp-json/app/v1/get-venues-trending`, {
method: "POST",
cache: "force-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
page,
per_page: 10,
paginate,
filters
}),
});
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return null;
}
};
export const fetchEventCats = async () => {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-event-categories`, {
method: "GET",
cache: "force-cache",
});
const data = await response.json();
return data;
};
export const maybeFavoriteEvent = async (eventId) => {
const user = await getSessionUser();
if (!user || !user.id) {
return null;
}
const response = await fetch(`${API_URL}/wp-json/app/v1/favourite-event`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
event_id: eventId,
user_id: user.id
}),
});
const data = await response.json();
return data;
};
export const maybeFollowVenue = async (venueId) => {
const user = await getSessionUser();
if (!user || !user.id) {
return null;
}
const response = await fetch(`${API_URL}/wp-json/app/v1/follow-venue`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
venue_id: venueId,
user_id: user.id
}),
});
const data = await response.json();
return data;
}
export const fetchVenue = async (venueId) => {
const user = await getSessionUser();
const response = await fetch(`${API_URL}/wp-json/app/v1/get-venue`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
venue_id: venueId,
user_id: user?.id
}),
});
const data = await response.json();
if (response.status !== 200) {
throw new Error(data.message);
}
return data;
}
export const fetchTrendingUsers = async (page, is_vehicle = false) => {
try {
const user = await getSessionUser();
if (!user) {
throw new Error('Session user not found');
}
const response = await fetch(`${API_URL}/wp-json/app/v1/popular-users-cars`, {
method: "POST",
cache: "force-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
page,
per_page: 10,
is_vehicle
}),
});
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return null;
}
};
/js/api/garage.js
import {
getSessionUser
} from './auth.js'
import {
API_URL,
TIMEOUT_MS_HIGH,
TIMEOUT_MS_HIGHER,
} from './consts.js'
export const getUserGarage = async (profileId) => {
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-user-garage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: profileId
}),
})
const data = await response.json()
if (response.status !== 200) {
return []
}
return data
} catch (error) {
return []
}
}
export const getGargeById = async (garageId) => {
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-garage`, {
method: "POST",
cache: "force-cache",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
garage_id: garageId
}),
})
const data = await response.json()
if (response.status !== 200) {
throw new Error('Failed to fetch users posts')
}
return data
} catch (error) {
console.error(error)
return null
}
}
export const getPostsForGarage = async (garageId, page = 1, tagged = false) => {
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-garage-posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
garage_id: garageId,
page,
limit: 10,
tagged
}),
})
const data = await response.json()
if (response.status !== 200) {
return null
}
return data
} catch (error) {
return null
}
}
export const addVehicleToGarage = async (data) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGH)
const response = await fetch(`${API_URL}/wp-json/app/v1/add-vehicle-to-garage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...data,
user_id: user.id,
}),
signal
})
const res = await response.json()
return res
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to add your vehicle, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const updateVehicleInGarage = async (data, garageId) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGH)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-garage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...data,
user_id: user.id,
garage_id: garageId,
}),
signal
})
const res = await response.json()
return res
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your vehicle, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const deleteVehicleFromGarage = async (garageId) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGH)
const response = await fetch(`${API_URL}/wp-json/app/v1/delete-garage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
garage_id: garageId,
}),
signal
})
const res = await response.json()
return res
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to delete your vehicle, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
/js/api/posts.js
import {
API_URL,
TIMEOUT_MS_LOW
} from './consts.js'
import {
getSessionUser
} from './auth.js'
export async function fetchPosts(page, following = false) {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
// setTimeout(() => {
// controller.abort()
// }, 5)
const response = await fetch(`${API_URL}/wp-json/app/v1/get-posts?page=${page}&limit=10`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
following_only: following,
}),
signal // Abort signal
})
const data = await response.json()
return data
} catch (error) {
console.log(error);
return {}
}
}
export async function fetchComments(postId) {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_LOW)
const response = await fetch(`${API_URL}/wp-json/app/v1/get-post-comments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
post_id: postId
}),
signal
})
const data = await response.json()
return data
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to fetch comments, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const maybeLikePost = async (postId) => {
const user = await getSessionUser()
if (!user) return
const response = await fetch(`${API_URL}/wp-json/app/v1/toggle-like-post`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
post_id: postId
}),
})
const data = await response.json()
return data
}
export const addComment = async (postId, comment, comment_id = null) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_LOW)
const response = await fetch(`${API_URL}/wp-json/app/v1/add-post-comment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
post_id: postId,
comment,
parent_id: comment_id
}),
signal
})
const data = await response.json()
return data
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to add comment, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
export const deleteComment = async (commentId) => {
const user = await getSessionUser()
if (!user) return
const response = await fetch(`${API_URL}/wp-json/app/v1/delete-post-comment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
comment_id: commentId
}),
})
const data = await response.json()
return data
}
export const maybeLikeComment = async (commentId, ownerId) => {
const user = await getSessionUser()
if (!user) return
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/toggle-like-comment`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
comment_id: commentId,
owner_id: ownerId
}),
})
const data = await response.json()
if (!response.ok || response.status !== 200) {
throw new Error(data.message)
}
return data
} catch (e) {
console.error("Error liking comment")
throw new Error(e.message)
}
}
export const getPostsForUser = async (profileId, page = 1, tagged = false, limit = 10) => {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-user-posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: profileId,
page,
limit,
tagged
}),
})
const data = await response.json()
if (response.status !== 200) {
return {
data: [],
total_pages: 0,
page: 1,
limit
}
}
return data
}
export const getPostById = async (post_id) => {
const user = await getSessionUser()
if (!user) return
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-post`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
post_id
}),
})
const data = await response.json()
return data
} catch (error) {
return null
}
}
export const deletePost = async (post_id) => {
const user = await getSessionUser()
if (!user) return
try {
const response = await fetch(`${API_URL}/wp-json/app/v1/delete-post`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
post_id
}),
})
if (response.status !== 200) {
throw new Error("Error deleting post")
}
return true
} catch (error) {
return false
}
}
/**
* @param {Object} data {
post_id: string | number;
new_tags: PostTag[];
removed_tags: number[];
caption ? : string;
location ? : string;
}
*/
export const updatePost = async (data) => {
try {
const user = await getSessionUser();
if (!user || !user.id) return null;
const response = await fetch(`${API_URL}/wp-json/app/v1/edit-post`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
...data
}),
});
const res = await response.json();
return res;
} catch (e) {
console.error("Error updating post", e.message);
return null;
}
};
/js/api/profile.js
import {
API_URL,
TIMEOUT_MS_HIGHER
} from "./consts.js"
import {
getSessionUser
} from "./auth.js";
export const addUserProfileLinks = async ({
link,
type,
}) => {
const user = await getSessionUser();
if (!user) return null;
const response = await fetch(`${API_URL}/wp-json/app/v1/add-profile-links`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
link,
type
}),
});
const data = await response.json();
return data;
};
export const updateSocialLinks = async (links) => {
const user = await getSessionUser();
if (!user) return;
const response = await fetch(`${API_URL}/wp-json/app/v1/update-social-links`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
links
}),
});
const data = await response.json();
return data;
};
export const removeProfileLink = async (linkId) => {
const user = await getSessionUser();
if (!user) return;
const response = await fetch(`${API_URL}/wp-json/app/v1/remove-profile-link`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
link_id: linkId
}),
});
const data = await response.json();
console.log(response);
if (response.status !== 200 || data.error) {
return false
}
return true;
};
export const updateUserDetails = async (details, email_changed) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser();
if (!user) return;
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-user-details`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
...details,
email_changed
}),
signal
});
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your details, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
};
export const updateProfileImage = async (image) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser();
if (!user) return;
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-profile-image`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
image
}),
signal
});
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your profile image, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
};
export const updateCoverImage = async (image) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser();
if (!user) return;
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_HIGHER)
const response = await fetch(`${API_URL}/wp-json/app/v1/update-cover-image`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user.id,
image
}),
signal
});
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: "Failed to update your cover image, your connection timed out",
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
};
export const maybeFollowUser = async (profileId) => {
const user = await getSessionUser();
if (!user) return;
const response = await fetch(`${API_URL}/wp-json/app/v1/follow-user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
following_id: profileId,
follower_id: user.id
}),
});
const data = await response.json();
return data;
};
export const removeTagFromPost = async (tagId) => {
const user = await getSessionUser();
if (!user) return;
const response = await fetch(`${API_URL}/wp-json/app/v1/remove-post-tag`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_id: tagId,
user_id: user.id
}),
});
const data = await response.json();
return data;
}
export const approvePostTag = async (tagId) => {
const user = await getSessionUser();
if (!user) return;
const response = await fetch(`${API_URL}/wp-json/app/v1/approve-post-tag`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
tag_id: tagId,
user_id: user.id
}),
});
const data = await response.json();
return data;
}
/js/api/scanner.js
import {
getSessionUser
} from "./auth.js"
import {
API_URL
} from "./consts.js"
export const verifyScan = async (decodedText) => {
const user = await getSessionUser()
const response = await fetch(`${API_URL}/wp-json/app/v1/verify-qr-code`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_id: user?.id,
qr_code: decodedText
}),
})
const data = await response.json()
return data
}
const linkProfile = async (decodedText) => {
const user = await getSessionUser()
const response = await fetch(`${API_URL}/wp-json/app/v1/link-qr-code-entity`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
entity_id: user?.id,
qr_code: decodedText,
entity_type: "profile"
}),
})
const data = await response.json()
return data
}
const unlinkProfile = async (decodedText) => {
const user = await getSessionUser()
const response = await fetch(`${API_URL}/wp-json/app/v1/unlink-qr-code-entity`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
qr_code: decodedText,
entity_id: user?.id,
entity_type: "profile"
}),
})
const data = await response.json()
return data
}
export const getIDFromQrCode = async (decodedText) => {
const response = await fetch(`${API_URL}/wp-json/app/v1/get-linked-entity`, {
cache: "no-cache",
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
qr_code: decodedText
}),
})
const data = await response.json()
if ((data && data.error) || response.status === 404) {
throw new Error(data.error)
}
return data
}
export const handleLink = async (result) => {
if (!result) {
return
}
try {
const response = await linkProfile(result?.qr_code)
return response
} catch (e) {
console.error("Error linking profile", e)
return {
status: 'error',
text: 'Error linking profile'
}
}
}
export const handleUnlink = async (result) => {
if (!result) {
return
}
try {
const response = await unlinkProfile(result?.qr_code)
if (response.status === 'error') {
return {
type: 'error',
text: response.message
}
} else {
return {
type: 'success',
text: response.message
}
}
} catch (e) {
console.error("Error unlinking profile", e)
return {
type: 'error',
text: 'Error unlinking profile'
}
}
}
/js/app.js
//------------------------------------------ CORE ------------------------------------------//
import {
getSessionUser,
handleSignUp,
updateAboutUserIds,
updateContentIds,
updateUsername,
verifyEmail,
verifyUser
} from './api/auth.js'
import store from './store.js'
import routes from './routes.js'
import {
displayProfile
} from './profile.js'
import {
getIDFromQrCode
} from './api/scanner.js'
import {
openModal,
openQRModal
} from './qr.js'
import {
sendRNMessage
} from './api/consts.js'
var $ = Dom7
var userStore = store.getters.user
var notificationCountStore = store.getters.getNotifCount
var networkErrors = store.getters.checkPoorNetworkError
var toolbarEl = $('.footer')[0]
var app = new Framework7({
initOnDeviceReady: true,
view: {
pushState: false,
stackPages: true,
xhrCache: true,
preloadPreviousPage: true,
// browserHistory: true,
},
notification: {
title: 'DriveLife',
closeTimeout: 10000,
closeOnClick: true,
icon: '<img src="assets/icons/favicon.png"/>',
},
toast: {
closeTimeout: 3000,
closeButton: true,
},
name: 'DriveLife',
theme: 'ios',
smartSelect: {
closeOnSelect: true,
},
cache: true,
el: '#app', // App root element
on: {
init: async function () {
const verifyToken = getQueryParameter('verifyToken')
if (verifyToken) {
await verifyUserEmail(verifyToken)
return;
}
await handleSSOSignIn() // SSO with CarEvents
await store.dispatch('checkAuth')
const isAuthenticated = store.getters.isAuthenticated.value
if (!isAuthenticated) {
this.views.main.router.navigate('/auth/')
} else {
$('.init-loader').hide()
$('.start-link').click();
}
await handleQRCode()
const deeplink = getQueryParameter('deeplink')
if (deeplink) {
// get the page from the deeplink and navigate to it
// ex; http://localhost:3000/post-view/308
// get the /post-view/308 and navigate to it
let path = deeplink.split('/').slice(3).join('/')
if (!path.startsWith('/')) {
path = `/${path}`
}
this.views.main.router.navigate(path)
// remove the query parameter from the URL
window.history.pushState({}, document.title, window.location.pathname)
}
},
pageInit: function (page) {
if (page.name === 'profile') {
userStore.onUpdated((data) => {
if (data && data.id) {
const isEmailVerified = data.email_verified ?? false;
if (!isEmailVerified) {
const profileHead = $('.page[data-name="profile"] .profile-head')
if (profileHead.length) {
// Add email verification message before the element
$(`
<div class="email-verification-message">
<p>Your email is not verified. Please verify your email address to access all features.</p>
</div>
`).insertBefore(profileHead);
profileHead.addClass('email-not-verified');
}
}
}
if (data && data.id && !data.external_refresh) {
displayProfile(data, 'profile')
store.dispatch('getMyGarage')
}
if (data && data.id && !data.refreshed) {
store.dispatch('getMyPosts', {
page: 1,
clear: true
})
store.dispatch('getMyTags', {
page: 1,
clear: true
})
}
})
}
if (page.name === 'discover') {
userStore.onUpdated((data) => {
if (data && data.id && !data.refreshed) {
store.dispatch('getTrendingEvents')
store.dispatch('getTrendingVenues')
store.dispatch('filterTrendingUsers')
store.dispatch('filterTrendingVehicles')
store.dispatch('fetchEventCategories')
}
})
}
if (page.name === 'signup-step2') {
const registerData = store.getters.getRegisterData.value
const userNameEl = document.getElementsByName('username')[0]
userNameEl.value = registerData.username
}
},
},
store: store,
routes: routes,
})
async function handleSSOSignIn() {
$('.init-loader').show()
// SSO with CarEvents
const ceToken = getQueryParameter('token')
if (ceToken) {
// remove the query parameter from the URL
window.history.pushState({}, document.title, window.location.pathname)
window.localStorage.setItem('token', ceToken)
}
// check if deeplink url has ?token= query parameter
// if it does, save the token in the local storage
const deeplink = getQueryParameter('deeplink')
if (deeplink) {
const token = deeplink.split('?token=')[1]
if (token) {
window.localStorage.setItem('token', token)
// remove the query parameter from the URL
window.history.pushState({}, document.title, window.location.pathname)
}
}
}
async function handleQRCode() {
const deeplink = getQueryParameter('deeplink')
if (deeplink) {
// check if deeplink has ?qr= query parameter
// if it does, get the value of the qr parameter and redirect to the profile
// ex; http://localhost:3000/?qr=123456
const maybeQr = deeplink.split('?qr=')[1]
const deepqrCode = maybeQr ? maybeQr : null
if (deepqrCode) {
maybeRedirectToProfile(deepqrCode)
return;
}
// check if url looks like https://mydrivelife.com/qr/8700279E
// if it does, get the qr code and redirect to the profile
const isDriveLifeUrl = deeplink.includes('mydrivelife.com/qr/')
if (isDriveLifeUrl) {
const qrCode = deeplink.split('/').slice(-1)[0]
maybeRedirectToProfile(qrCode)
return;
}
}
const qrCode = getQueryParameter('qr')
if (qrCode) {
maybeRedirectToProfile(qrCode)
}
}
async function verifyUserEmail(token) {
// remove the query parameter from the URL
// window.history.pushState({}, document.title, window.location.pathname)
try {
// Clear the app landing page content
$('.app-landing-page').html('')
// Add content to show email verification in progress
$('.app-landing-page').html(`
<div class="verification-content">
<div class="block">
<img src="assets/img/ce-logo-dark.png" />
<div class="verification-header">
<h1>Email Verification</h1>
</div>
<div class="verification-body">
<div class="verification-loader">
<div class="preloader color-white"></div>
</div>
<div class="verification-message">
<p>Verifying your email address...</p>
</div>
</div>
</div>
</div>
`)
const response = await verifyEmail(token)
// Check if there's an error in the response
if (!response || response.status === 'error') {
// Display an error message
$('.verification-body').html(`
<div class="verification-message">
<p class="verification-error">An error occurred: ${response.message || 'Please try again.'}</p>
<p class="verification-error">If you continue to experience issues, please contact support.</p>
<div class="button button-fill button-large" id="goto-app">Go Back</div>
</div>
`)
return
}
if (response.status === 'success') {
// Show a success message in the DOM
$('.verification-body').html(`
<div class="verification-message">
<p class="verification-success">Your email has been successfully verified! You can now proceed.</p>
<div class="button button-fill button-large" id="goto-app">Continue</div>
</div>
`)
return
}
} catch (error) {
// Display a generic error message in case of exceptions
$('.verification-body').html(`
<div class="verification-message">
<p class="verification-error">An unexpected error occurred. Please try again.</p>
<p class="verification-error">If you continue to experience issues, please contact support.</p>
<div class="button button-fill button-large" id="goto-app">Go Back</div>
</div>
`)
}
}
$(document).on('click', '#goto-app', function (e) {
// remove the query parameter from the URL
window.history.pushState({}, document.title, window.location.pathname)
// reload the page
window.location.reload()
})
$(document).on('click', '.start-link', function (e) {
toolbarEl.style.display = 'block'
})
$(document).on('mousedown', '.toolbar-bottom a', async function (e) {
var targetHref = $(this).attr('href');
var validTabs = ['#view-social', '#view-discover', '#view-store', '#view-profile'];
if ($(this).hasClass('tab-link-active') && validTabs.includes(targetHref)) {
var view = app.views.current;
if (view.history.length > 1) {
view.router.back(view.history[0], {
force: true
});
}
}
if (!view || !view.history) {
return
}
if (targetHref == '#view-social' && view.history.length <= 1) {
$('.page-current .page-content').scrollTop(0, 200);
const ptrContent = app.ptr.get('.ptr-content.home-page')
ptrContent.refresh()
}
});
export function showToast(message, type = 'Message', position = 'bottom') {
app.toast.create({
text: message,
position: position,
closeTimeout: 3000,
}).open()
}
async function maybeRedirectToProfile(qrCode) {
var view = app.views.current
try {
$('.init-loader').show()
const response = await getIDFromQrCode(qrCode)
if (response && response.status === 'error') {
throw new Error(response.message || 'Oops, Unable to find the profile linked to this QR code.')
}
const user = store.getters.user.value
const id = response?.data?.linked_to;
if (id) {
if (user && user.id == id) {
$('.view-profile-link').click()
} else {
view.router.navigate(`/profile-view/${id}`)
}
} else {
openModal()
setTimeout(() => {
store.dispatch('setScannedData', {
status: 'success',
qr_code: qrCode,
message: 'QR Code is not linked to any profile',
available: true
})
}, 1000)
}
// remove the query parameter from the URL
window.history.pushState({}, document.title, window.location.pathname)
$('.init-loader').hide()
} catch (error) {
console.log(error);
window.history.pushState({}, document.title, window.location.pathname)
app.dialog.alert(error.message || 'Oops, Unable to find the profile linked to this QR code.')
$('.init-loader').hide()
}
}
// Function to parse query parameters from the URL
function getQueryParameter(name, url) {
const urlParams = new URLSearchParams(url || window.location.search)
return urlParams.get(name)
}
function isAndroid() {
const toMatch = [
/Android/i,
// /webOS/i,
// /iPhone/i,
// /iPad/i,
// /iPod/i,
/BlackBerry/i,
/Windows Phone/i,
];
return toMatch.some((toMatchItem) => {
return navigator.userAgent.match(toMatchItem);
});
}
export function onBackKeyDown() {
// check if the device is an android device
if (!isAndroid()) {
return
}
var view = app.views.current
var leftp = app.panel.left && app.panel.left.opened
var rightp = app.panel.right && app.panel.right.opened
window.ReactNativeWebView.postMessage(JSON.stringify({
his: view.history,
url: app.views.main.router.url,
leftp,
rightp
}))
if (leftp || rightp) {
app.panel.close()
return false
} else if ($('.modal-in').length > 0) {
app.dialog.close()
app.popup.close()
return false
} else if (view.history[0] == '/social/') {
window.ReactNativeWebView.postMessage('exit_app')
return true
} else {
if (view.history.length < 2) {
$('.tab-link[href="#view-home"]').click()
return
}
view.router.back()
return false
}
}
function onPostUpload() {
store.dispatch('getMyPosts', {
page: 1,
clear: true
})
store.dispatch('getPosts', {
page: 1,
reset: true
})
}
window.onPostUpload = onPostUpload
window.onAppBackKey = onBackKeyDown
userStore.onUpdated((data) => {
if (data && data.id && !data.external_refresh && !data.refreshed) {
store.dispatch('getPosts', {
page: 1,
reset: true
})
store.dispatch('getFollowingPosts', {
page: 1,
reset: true
})
}
})
notificationCountStore.onUpdated((data) => {
document.querySelectorAll('.notification-count').forEach((el) => {
el.innerHTML = data
el.style.display = data > 0 ? 'flex' : 'none'
})
})
networkErrors.onUpdated((data) => {
if (data) {
app.dialog.alert('Poor network connection. Please check your internet connection and try again.')
}
})
// Action Sheet with Grid Layout
var actionSheet = app.actions.create({
grid: true,
buttons: [
[{
text: '<div class="actions-grid-item">Add Post</div>',
icon: '<img src="assets/img/icon-add-post.svg" width="48" style="max-width: 100%"/>',
onClick: async function () {
const user = await getSessionUser()
if (user) {
sendRNMessage({
type: "createPost",
user_id: user.id,
page: 'create-post',
})
}
}
},
{
text: '<div class="actions-grid-item">Scan QR Code</div>',
icon: '<img src="assets/img/icon-qr-code.svg" width="48" style="max-width: 100%;"/>',
onClick: function () {
openQRModal()
}
},
{
text: '<div class="actions-grid-item">My Vehicles</div>',
icon: '<img src="assets/img/icon-vehicle-add.svg" width="48" style="max-width: 100%;"/>',
onClick: function () {
var view = app.views.current
view.router.navigate('/profile-garage-edit/');
}
}
],
]
});
// Init slider
new Swiper('.swiper-container', {
pagination: {
el: '.swiper-pagination',
clickable: true,
},
})
app.popup.create({
el: '.share-popup',
swipeToClose: 'to-bottom'
})
app.popup.create({
el: '.edit-post-popup',
swipeToClose: 'to-bottom'
})
$(document).on('click', '#open-action-sheet', function () {
actionSheet.open()
})
// Handle login form submission
$(document).on('submit', '.login-screen-content form', async function (e) {
e.preventDefault()
var username = $(this).find('input[name="username"]').val()
var password = $(this).find('input[name="password"]').val()
if (!username) {
showToast('Username is required')
return
}
if (!password) {
showToast('Password is required')
return
}
try {
app.preloader.show()
const response = await verifyUser({
email: username,
password
})
app.preloader.hide()
if (!response || response.error) {
app.dialog.alert(response.error || 'Login failed, please try again')
return
}
if (response.success) {
showToast('Login successful')
await store.dispatch('login', {
token: response.token
})
app.views.main.router.navigate('/')
$('.start-link').click();
toolbarEl.style.display = 'block'
return
}
} catch (error) {
app.dialog.alert('Login failed, please try again')
}
})
$(document).on('click', '.toggle-password', function () {
var input = $(this).prev('input')
if (input.attr('type') === 'password') {
input.attr('type', 'text')
$(this).html('<i class="fa fa-eye-slash"></i>')
} else {
input.attr('type', 'password')
$(this).html('<i class="fa fa-eye"></i>')
}
})
// Register forms
// Step 1
$(document).on('submit', 'form#sign-up-step1', async function (e) {
e.preventDefault()
var firstName = $(this).find('input[name="first_name"]').val().trim()
var lastName = $(this).find('input[name="last_name"]').val().trim()
var email = $(this).find('input[name="email"]').val().trim()
var password = $(this).find('input[name="password"]').val().trim()
var confirmPassword = $(this).find('input[name="confirm_password"]').val().trim()
var agreeTerms = $(this).find('input[name="agree_terms"]').is(':checked')
var agreePrivacy = $(this).find('input[name="agree_privacy"]').is(':checked')
if (!firstName) {
showToast('First name is required')
return
}
if (!lastName) {
showToast('Last name is required')
return
}
if (!email) {
showToast('Email is required')
return
}
var emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailPattern.test(email)) {
showToast('Please enter a valid email address')
return
}
if (!password) {
showToast('Password is required')
return
}
// Check if password has at least 8 characters
if (password.length < 8) {
showToast('Password must be at least 8 characters long.')
return
}
// Check if password contains at least one lowercase letter
if (!/[a-z]/.test(password)) {
showToast('Password must contain at least one lowercase letter.')
return
}
// Check if password contains at least one uppercase letter
if (!/[A-Z]/.test(password)) {
showToast('Password must contain at least one uppercase letter.')
return
}
// Check if password contains at least one number
if (!/\d/.test(password)) {
showToast('Password must contain at least one number.')
return
}
if (password.length < 8) {
showToast('Password must be at least 8 characters long')
return
}
if (!confirmPassword) {
showToast('Please confirm your password')
return
}
if (password !== confirmPassword) {
showToast('Passwords do not match')
return
}
if (!agreeTerms) {
showToast('You must agree to the Terms & Conditions')
return
}
if (!agreePrivacy) {
showToast('You must agree to the Privacy Policy')
return
}
// add a loader to the login button
var loginButton = $(this).find('button[type="submit"]')[0]
loginButton.innerHTML = '<div class="preloader color-white"></div>'
try {
app.preloader.show()
const response = await handleSignUp({
full_name: `${firstName} ${lastName}`,
email,
password
})
app.preloader.hide()
if (!response || !response.success) {
app.dialog.alert(response.message || 'An error occurred, please try again')
loginButton.innerHTML = 'Next'
return
}
store.dispatch('setRegisterData', {
email,
password,
user_id: response.user_id,
username: response.username
})
app.views.main.router.navigate('/signup-step2/')
} catch (error) {
console.log(error)
app.dialog.alert(error.message || 'An error occurred, please try again')
loginButton.innerHTML = 'Next'
return
}
})
// Step 2
$(document).on('submit', 'form#sign-up-step2', async function (e) {
e.preventDefault()
var username = $(this).find('input[name="username"]').val().trim()
if (!username) {
showToast('Username is required')
return
}
// username can only have letters, numbers, and underscores
var usernamePattern = /^[a-zA-Z0-9_]+$/
if (!usernamePattern.test(username)) {
showToast('Username can only contain letters, numbers, and underscores')
return
}
// username must be at least 3 characters long
if (username.length < 3) {
showToast('Username must be at least 3 characters long')
return
}
let registerData = store.getters.getRegisterData.value
try {
if (registerData.username !== username) {
app.preloader.show()
const response = await updateUsername(username, registerData.user_id)
app.preloader.hide()
if (!response || !response.success) {
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update username',
}).open()
// app.dialog.alert(response.message || 'An error occurred, please try again')
return
}
store.dispatch('setRegisterData', {
...registerData,
username
})
}
app.views.main.router.navigate('/signup-step3/')
} catch (error) {
console.log(error);
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update username',
}).open()
return
}
})
// Step 3
$(document).on('submit', '#car-selection-form', async function (e) {
e.preventDefault()
// Get all checked checkboxes' values
var selectedCarTypes = []
$(this).find('input[name="car_type"]:checked').each(function () {
selectedCarTypes.push($(this).val())
})
// Check if at least one checkbox is selected
if (selectedCarTypes.length === 0) {
showToast('Please select at least one car type')
return
}
// For demonstration, log the selected values to the console
let registerData = store.getters.getRegisterData.value
try {
app.preloader.show()
const response = await updateContentIds(selectedCarTypes, registerData.user_id)
if (!response || !response.success) {
app.dialog.alert(response.message || 'Oops, Unable to save your selection.')
}
app.preloader.hide()
app.views.main.router.navigate('/signup-step4/')
} catch (error) {
console.log(error)
app.dialog.alert('An error occurred, please try again')
return
}
// Redirect to the next step (this can be customized as needed)
app.views.main.router.navigate('/signup-step4/')
})
// Step 4
$(document).on('submit', '#interest-selection-form', async function (e) {
e.preventDefault()
// Get all checked checkboxes' values
var selectedInterests = []
$(this).find('input[name="interest"]:checked').each(function () {
selectedInterests.push($(this).val())
})
// Check if at least one checkbox is selected
if (selectedInterests.length === 0) {
showToast('Please select at least one interest')
return
}
let registerData = store.getters.getRegisterData.value
try {
app.preloader.show()
const response = await updateAboutUserIds(selectedInterests, registerData.user_id)
if (!response || !response.success) {
app.dialog.alert(response.message || 'Oops, Unable to save your selection.')
}
app.preloader.hide()
app.views.main.router.navigate('/signup-complete/')
} catch (error) {
console.log(error)
app.dialog.alert('An error occurred, please try again')
return
}
})
// Signup complete
$(document).on('click', '#signup-complete', async function (e) {
const registerData = store.getters.getRegisterData.value
if (!registerData || !registerData.user_id || !registerData.email || !registerData.password) {
app.dialog.alert('An error occurred, please try again')
return
}
try {
app.preloader.show()
const response = await verifyUser({
email: registerData.email,
password: registerData.password
})
app.preloader.hide()
if (!response || response.error) {
app.dialog.alert(response.error || 'Login failed, please try again')
app.views.main.router.navigate('/auth/')
loginButton.innerHTML = 'Next'
return
}
if (response.success) {
await store.dispatch('login', {
token: response.token
})
app.views.main.router.navigate('/')
$('.start-link').click();
toolbarEl.style.display = 'block'
return
}
} catch (error) {
app.dialog.alert('Login failed, please try again')
}
})
$(document).on('page:afterin', '.page[data-name="auth"]', function (e) {
toolbarEl.style.display = 'none'
setTimeout(() => {
$('.init-loader').hide()
}, 300)
});
$(document).on('page:afterin', '.page[data-name="signup-step1"]', function (e) {
app.popup.create({
el: '.privacy-popup',
swipeToClose: 'to-bottom'
})
});
// logout-button
$(document).on('click', '.logout-button', async function (e) {
app.dialog.close()
app.popup.close()
app.panel.close()
await store.dispatch('logout')
// reload page
window.location.reload()
// app.views.current.router.navigate('/auth/')
})
$(document).on('click', '.view-profile', function (e) {
$('.view-profile-link').click()
})
$(document).on('click', '#forgot-password', function (e) {
// open the url in a new tab
window.open($(this).attr('href'), '_blank')
})
// SSO with CarEvents
$(document).on('click', '#sso-ce-button', function (e) {
// URL encode the redirect URI (the app URL)
const appRedirectUri = encodeURIComponent('https://app.mydrivelife.com/'); // Replace with your app's redirect URL
// Build the CarEvents login URL with the state and app_redirect
const loginUrl = `https://www.carevents.com/uk/login?app_redirect=${appRedirectUri}`;
// Store the state in localStorage or sessionStorage for validation later
window.open(loginUrl, '_blank')
})
export default app
/js/discoverpage.js
import app from "./app.js";
import store from "./store.js";
var $ = Dom7;
var trendingEventsStore = store.getters.getTrendingEvents;
var trendingVenuesStore = store.getters.getTrendingVenues;
var eventCategories = store.getters.getEventCategories;
var filteredEventsStore = store.getters.getFilteredEvents;
var filteredVenuesStore = store.getters.getFilteredVenues;
var trendingUsersStore = store.getters.getTrendingUsers;
var trendingVehiclesStore = store.getters.getTrendingVehicles;
var isFetchingPosts = false
var currentEventsPage = 1
var currentVenuesPage = 1
var currentUsersPage = 1
var refreshed = false
var totalEventPages = 1
var totalVenuesPages = 1
var totalUsersPages = 1
var totalVehiclePages = 1
var autocomplete;
var filters = {};
//AUTOCOMPLETE FUNCTIONS
function initAutocomplete() {
autocomplete = new google.maps.places.Autocomplete(
document.getElementById("autocomplete"), {
types: ["establishment", "geocode"],
componentRestrictions: {
country: 'GB'
}
}
);
autocomplete.setFields(["geometry", "address_component"]);
autocomplete.addListener("place_changed", fillInAddress);
}
function fillInAddress() {
const place = autocomplete.getPlace();
console.log(place);
document.getElementById('lat').value = place.geometry.location.lat();
document.getElementById('lng').value = place.geometry.location.lng();
}
function populateEventCard(data = [], isSwiper = true) {
const swiperContainer = document.querySelector('#trending-events');
if (isSwiper) swiperContainer.innerHTML = '';
const eventsTabContainer = document.querySelector('#filtered-events-tab');
if (refreshed) {
eventsTabContainer.innerHTML = '';
}
data.forEach(event => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
// const startDate = new Date(event.dates[0].start_date);
// const endDate = new Date(event.dates[0].end_date);
let endDateString = '';
if (startDate.getDate() !== endDate.getDate()) {
endDateString = `
<div class="event-date-item">
<p>${endDate.toLocaleString('default', { month: 'short' })}</p>
<h5>${endDate.getDate()}</h5>
</div>
`
}
const card = `
<a href="/discover-view-event/${event.id}">
<div class="card event-item">
<div class="event-image position-relative">
<div class="image-rectangle" style="background-image: url('${event.thumbnail}');"></div>
<div class="event-dates">
<div class="event-date-item">
<p>${startDate.toLocaleString('default', { month: 'short' })}</p>
<h5>${startDate.getDate()}</h5>
</div>
${endDateString}
</div>
</div>
<div class="card-content">
<h3 class="event-title">${event.title}</h3>
<p class="event-info">Starts ${startDate.toLocaleString('default', { weekday: 'short' })}, ${startDate.getDate()} ${startDate.toLocaleString('default', { month: 'short' })} ${startDate.getFullYear()}</p>
<div class="event-info">
${event.location}
</div>
</div>
</div>
</a>
`;
if (isSwiper) {
const swiperSlide = document.createElement('swiper-slide');
swiperSlide.innerHTML = card;
swiperContainer.appendChild(swiperSlide);
}
eventsTabContainer.innerHTML += card;
});
}
function populateVenueCard(data = [], isSwiper = true) {
const swiperContainer = document.querySelector('#trending-venues');
if (isSwiper) swiperContainer.innerHTML = '';
const eventsTabContainer = document.querySelector('#filtered-venues-tab');
data.forEach(event => {
const swiperSlide = document.createElement('swiper-slide');
const card = `
<a href="/discover-view-venue/${event.ID}">
<div class="card event-item">
<div class="event-image position-relative">
<div class="image-rectangle" style="background-image: url('${event.cover_image}');"></div>
</div>
<div class="card-content">
<h3 class="event-title">${event.title}</h3>
<div class="event-info">
${event.venue_location}
</div>
<div class="event-info">
Apprx. ${event.distance} miles away
</div>
</div>
</div>
</a>
`;
if (isSwiper) {
swiperSlide.innerHTML = card;
swiperContainer.appendChild(swiperSlide);
}
eventsTabContainer.innerHTML += card;
});
}
function populateUsersCard(data = []) {
const tabContainer = document.querySelector('#users-tab');
data.forEach(user => {
let linkTo = user.type === 'user' ? `/profile-view/${user.id}` : `/profile-garage-vehicle-view/${user.id}`;
let title;
if (user.type === 'user') {
title = user.name;
}
if (user.type === 'vehicle') {
const userName = user.owner.username;
const vehicleName = user.title;
title = `${vehicleName} <br/> Owner ${userName}`;
}
const card = `
<li>
<a class="item-link search-result item-content" href="${linkTo}">
<div class="item-media">
<div class="image-square image-rounded"
style="background-image:url('${user.thumbnail || 'assets/img/profile-placeholder.jpg'}')">
</div>
</div>
<div class="item-inner">
<div class="item-title">${title}</div>
</div>
</a>
</li>
`;
tabContainer.innerHTML += card;
});
}
function populateVehiclesCard(data = []) {
const tabContainer = document.querySelector('#vehicles-tab');
data.forEach(user => {
let linkTo = `/profile-garage-vehicle-view/${user.id}`;
let title;
const userName = user.owner.username;
const vehicleName = user.title;
title = `${vehicleName} <br/> Owner @${userName}`;
const card = `
<li>
<a class="item-link search-result item-content" href="${linkTo}">
<div class="item-media">
<div class="image-square image-rounded"
style="background-image:url('${user.thumbnail}')">
</div>
</div>
<div class="item-inner">
<div class="item-title">${title}</div>
</div>
</a>
</li>
`;
tabContainer.innerHTML += card;
});
}
function addCategoryOptions(categories) {
const categoryFilters = document.querySelector('#category-filters ul');
categories.forEach(category => {
const listItem = document.createElement('li');
listItem.innerHTML = `
<label class="item-checkbox item-content">
<input type="checkbox" name="${category.slug}" value="${category.id}" />
<i class="icon icon-checkbox"></i>
<div class="item-inner">
<div class="item-title">${category.name}</div>
</div>
</label>
`;
categoryFilters.appendChild(listItem);
});
}
// Event listener for the submit button
$(document).on('click', '.apply-filters', function (e) {
const dateFilters = document.querySelector('#date-filters ul');
const locationFilters = document.querySelector('#location-filters ul');
const categoryFilters = document.querySelector('#category-filters ul');
e.preventDefault(); // Prevent form submission if you're handling it via JavaScript
const selectedCats = [...categoryFilters.querySelectorAll('input[type="checkbox"]:checked')].map(cb => cb.value);
const selectedLocation = [...locationFilters.querySelectorAll('input[type="radio"]:checked')].map(cb => cb.value);
const dateFilter = [...dateFilters.querySelectorAll('input[type="checkbox"]:checked')].map(cb => cb.value);
const latitude = document.getElementById('lat').value;
const longitude = document.getElementById('lng').value;
let location = null;
if (latitude && longitude) {
location = {
latitude,
longitude
}
}
filters = {
'event_location': selectedLocation,
'custom_location': location,
'event_date': dateFilter,
// 'event_start': customDateRange?.start,
// 'event_end': customDateRange?.end,
'event_category': !selectedCats?.includes(0) ? selectedCats : undefined,
};
// get active tab
const activeTab = document.querySelector('.tabbar-nav .tab-link-active').getAttribute('data-id');
if (activeTab === 'events') {
store.dispatch('filterEvents', {
page: 1,
filters
})
currentEventsPage = 1
isFetchingPosts = false
const eventsTabContainer = document.querySelector('#filtered-events-tab');
eventsTabContainer.innerHTML = '';
}
if (activeTab === 'venues') {
store.dispatch('filterVenues', {
page: 1,
filters
})
currentVenuesPage = 1
isFetchingPosts = false
const venuesTabContainer = document.querySelector('#filtered-venues-tab');
venuesTabContainer.innerHTML = '';
}
// close popup
app.popup.close()
});
$(document).on('change', '#category-filters ul', function (e) {
const categoryFilters = document.querySelector('#category-filters ul');
var allCheckbox = categoryFilters.querySelector('input[name="all"]');
const targetCheckbox = e.target;
if (targetCheckbox.name !== "all") {
// If any other checkbox is selected, uncheck "All"
if (targetCheckbox.checked) {
allCheckbox.checked = false;
} else {
// If all other checkboxes are unchecked, check "All"
const allUnchecked = [...categoryFilters.querySelectorAll('input[type="checkbox"]')]
.filter(cb => cb.name !== "all")
.every(cb => !cb.checked);
if (allUnchecked) {
allCheckbox.checked = true;
}
}
} else {
// If "All" is selected, uncheck all other checkboxes
if (targetCheckbox.checked) {
[...categoryFilters.querySelectorAll('input[type="checkbox"]')]
.filter(cb => cb.name !== "all")
.forEach(cb => cb.checked = false);
}
}
});
eventCategories.onUpdated((data) => {
addCategoryOptions(data);
})
trendingVenuesStore.onUpdated((data) => {
totalVenuesPages = data.total_pages
populateVenueCard(data.data);
});
trendingEventsStore.onUpdated((data) => {
totalEventPages = data.total_pages
populateEventCard(data.data);
});
trendingUsersStore.onUpdated((data) => {
const tabContainer = document.querySelector('#users-tab');
if (!data || data.data.length === 0) {
tabContainer.innerHTML = `
<div class="no-events">
<h3>No trending users found for you</h3>
</div>
`;
return
}
totalUsersPages = data.total_pages
if ((totalUsersPages == data.page) || (totalUsersPages == 0) || (data.new_data.length < 10)) {
$('.infinite-scroll-preloader.users-tab').hide()
} else {
$('.infinite-scroll-preloader.users-tab').show()
}
populateUsersCard(data.new_data);
});
trendingVehiclesStore.onUpdated((data) => {
const tabContainer = document.querySelector('#vehicles-tab');
if (!data || data.data.length === 0) {
tabContainer.innerHTML = `
<div class="no-events">
<h3>No trending vehicles found for you</h3>
</div>
`;
return
}
totalVehiclePages = data.total_pages
if ((totalVehiclePages == data.page) || (totalVehiclePages == 0) || (data.new_data.length < 10)) {
$('.infinite-scroll-preloader.vehicles-tab').hide()
} else {
$('.infinite-scroll-preloader.vehicles-tab').show()
}
populateVehiclesCard(data.new_data);
});
// Filtered views
filteredEventsStore.onUpdated((data) => {
const eventsTabContainer = document.querySelector('#filtered-events-tab');
if (!data || data.data.length === 0) {
eventsTabContainer.innerHTML = `
<div class="no-events">
<h3>No events found</h3>
</div>
`;
return
}
totalEventPages = data.total_pages
if ((totalEventPages == data.page) || (totalEventPages == 0) || (data.new_data.length < 10)) {
$('.infinite-scroll-preloader.events-tab').hide()
} else {
$('.infinite-scroll-preloader.events-tab').show()
}
populateEventCard(data.new_data, false);
});
filteredVenuesStore.onUpdated((data) => {
const tabContainer = document.querySelector('#filtered-venues-tab');
if (!data || data.data.length === 0) {
tabContainer.innerHTML = `
<div class="no-venues">
<h3>No venues found</h3>
</div>
`;
totalVenuesPages = 0
return
}
if ((totalVenuesPages == data.page) || (totalVenuesPages == 0) || (data.new_data.length == 0)) {
$('.infinite-scroll-preloader.venues-tab').hide()
totalVenuesPages = 0
} else {
$('.infinite-scroll-preloader.venues-tab').show()
totalVenuesPages = data.total_pages
populateVenueCard(data.new_data, false);
}
});
$(document).on('page:init', '.page[data-name="discover"]', function (e) {
//Date Filters
app.calendar.create({
inputEl: '#date-from',
openIn: 'customModal',
header: true,
footer: true,
});
app.calendar.create({
inputEl: '#date-to',
openIn: 'customModal',
header: true,
footer: true,
});
//Filter Date Popup
app.popup.create({
el: '.filter-bydate-popup',
swipeToClose: 'to-bottom'
});
//Filter Category Popup
app.popup.create({
el: '.filter-bycategory-popup',
swipeToClose: 'to-bottom'
});
//Filter Location Popup
app.popup.create({
el: '.filter-bylocation-popup',
swipeToClose: 'to-bottom'
});
//SEARCH BAR
$('.discover-search').on('mousedown', function (event) {
event.preventDefault();
app.views.discover.router.navigate('/search/');
});
const dateFilters = document.querySelector('#date-filters ul');
const locationFilters = document.querySelector('#location-filters ul');
// Event listener for checkbox selection
dateFilters.addEventListener('change', function (e) {
const targetCheckbox = e.target;
// Uncheck all checkboxes except the one that was clicked
if (targetCheckbox.type === "checkbox") {
[...dateFilters.querySelectorAll('input[type="checkbox"]')].forEach(checkbox => {
if (checkbox !== targetCheckbox) {
checkbox.checked = false;
}
});
}
});
locationFilters.addEventListener('change', function (e) {
const targetCheckbox = e.target;
// Uncheck all checkboxes except the one that was clicked
if (targetCheckbox.type === "checkbox") {
[...locationFilters.querySelectorAll('input[type="checkbox"]')].forEach(checkbox => {
if (checkbox !== targetCheckbox) {
checkbox.checked = false;
}
});
}
});
initAutocomplete();
});
$(document).on('infinite', '.discover-page.infinite-scroll-content', async function (e) {
if (isFetchingPosts) return
// get active tab
const activeTabId = $('.tabbar-nav .tab-link-active').attr('data-id');
if (!activeTabId) return
isFetchingPosts = true
if (activeTabId == 'events') {
currentEventsPage++
if (currentEventsPage <= totalEventPages) {
await store.dispatch('filterEvents', {
page: currentEventsPage,
filters
})
isFetchingPosts = false
}
} else if (activeTabId == 'venues') {
currentVenuesPage++
if (currentVenuesPage <= totalVenuesPages) {
await store.dispatch('filterVenues', {
page: currentVenuesPage,
filters
})
isFetchingPosts = false
}
}
isFetchingPosts = false
})
$(document).on('page:init', '.page[data-name="discover-view-event"]', function (e) {
// Init slider
new Swiper('.swiper-container', {
pagination: {
el: '.swiper-pagination',
clickable: true,
},
})
app.popup.create({
el: '.share-listing-popup',
swipeToClose: 'to-bottom'
});
});
$(document).on('ptr:refresh', '.discover-page.ptr-content', async function (e) {
refreshed = true
currentEventsPage = 1
currentVenuesPage = 1
try {
await store.dispatch('getTrendingEvents')
await store.dispatch('getTrendingVenues')
await store.dispatch('filterTrendingUsers')
await store.dispatch('fetchEventCategories')
} catch (error) {
console.log(error);
}
refreshed = false
app.ptr.get('.discover-page.ptr-content').done()
})
$(document).on('click', '#featured-event-link', function (e) {
e.preventDefault();
window.open(e.target.href, '_blank');
});
/js/edit-post.js
import {
updatePost
} from "./api/posts.js";
import app, {
showToast
} from "./app.js";
import store from "./store.js";
var $ = Dom7;
$(document).on('page:beforein', '.page[data-name="post-edit"]', async function (e) {
var posts = store.getters.myPosts.value
var postId = e.detail.route.params.id
if (!postId || postId == -1) {
return;
}
console.log(posts);
const post = posts.data.find(p => p.id == postId)
$('#edit_post_id').val(postId);
$('#post_content').val(post.caption);
});
$(document).on('click', '#update-post', async function (e) {
var view = app.views.current
const description = $('#post_content').val();
const postId = $('#edit_post_id').val();
const data = {
post_id: postId,
caption: description
}
try {
app.preloader.show()
const response = await updatePost(data)
app.preloader.hide()
if (!response || response.error) {
throw new Error(response.error);
}
showToast('Post updated successfully')
// fine elem with data-post-id="52" and update the .media-post-description .post-caption text
var postElem = $(`[data-post-id="${postId}"]`).find('.media-post-description')
const maxDescriptionLength = 200; // Set your character limit here
const isLongDescription = description.length > maxDescriptionLength;
const shortDescription = isLongDescription ? description.slice(0, maxDescriptionLength) : description;
// for each postElem, loop through and update the .post-caption and .full-description hidden input
postElem.each(function () {
var postCaption = $(this).find('.post-caption');
var fullDescription = $(this).find('.full-description');
postCaption.text(shortDescription);
fullDescription.val(description);
});
store.dispatch('updatePost', {
post_id: postId,
caption: description
})
view.router.back()
} catch (error) {
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update post',
}).open()
app.preloader.hide()
}
})
/js/event-view.js
import {
fetchEvent,
maybeFavoriteEvent
} from "./api/discover.js";
import app from "./app.js";
var $ = Dom7;
//DISCOVER - VIEW EVENT
$(document).on('page:afterin', '.page[data-name="discover-view-event"]', async function (e) {
var eventId = e.detail.route.params.id
if (!eventId || eventId === '-1') {
return;
}
$('.loading-fullscreen').show()
const eventData = await fetchEvent(eventId)
$('.loading-fullscreen').hide()
const mainContainer = $('.discover-view-event.view-event');
if (!eventData) {
mainContainer.html('<div class="text-align-center">No event found</div>');
return;
}
// Populating the Event Title
mainContainer.find('.event-detail-title').text(eventData.title);
// Populating the Event Date
// const dateText = `Sun, Aug 25th 2024`; // You can format this dynamically if needed
const startDate = new Date(eventData.dates[0].start_date);
const endDate = new Date(eventData.dates[0].end_date);
// format as "Sun, Aug 25th 2024" or "Sun, Aug 25th 2024 - Mon, Aug 26th 2024"
if (startDate.toDateString() === endDate.toDateString()) {
var dateText = startDate.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
if (startDate.toDateString() !== endDate.toDateString()) {
var dateText = startDate.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric'
}) + ' - ' + endDate.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
mainContainer.find('.event-time-address:nth-child(1) span').text(dateText);
// Populating the Event Time
const timeText = `${eventData.dates[0].start_time} - ${eventData.dates[0].end_time}`;
mainContainer.find('.event-time-address:nth-child(2) span').text(timeText);
// Populating the Event Location
mainContainer.find('.event-time-address:nth-child(3) span').text(eventData.location);
// Populating the Event Description in "About" Tab
mainContainer.find('#tab-about .event-des-wrap').html(`<p>${eventData.description}</p>`);
mainContainer.find('#tab-entry-details .event-des-wrap').html(`<p>${eventData.entry_details}</p>`);
if (eventData.has_tickets) {
// Populating the Ticket Button URL
mainContainer.find('.event-list-btn .btn.bg-dark').on('click', function () {
window.location.href = eventData.ticket_url;
});
} else {
// Hiding the Ticket Button
mainContainer.find('.event-list-btn').hide();
}
// Populating the Swiper Images
const swiperWrapper = mainContainer.find('.swiper-wrapper');
swiperWrapper.empty(); // Clear existing placeholders
const gallery = eventData.gallery || [];
gallery.push({
url: eventData.cover_photo.url
});
gallery.forEach(image => {
const slide = `
<div class="swiper-slide">
<div class="swiper-image" style="background-image: url('${image.url}');"></div>
</div>
`;
swiperWrapper.append(slide);
});
mainContainer.find('.event-des-wrap a').on('click', function (e) {
console.log('clicked');
e.preventDefault();
window.open($(this).attr('href'), '_blank');
});
$('#copy-event-link').attr('data-event-id', eventId)
$('#share-email-event-link').attr('data-event-id', eventId)
// set the event id to the favourite button
const faveBtn = $('#favourite_event')
faveBtn.attr('data-event-id', eventId)
if (eventData.is_liked) {
faveBtn.addClass('favourite')
faveBtn.innerHTML = `<i class="f7-icons">heart_fill</i> Favourite`
} else {
faveBtn.removeClass('favourite')
faveBtn.innerHTML = `<i class="f7-icons">heart</i> Favourite`
}
});
$(document).on('click', '#favourite_event', async function () {
// get the event id from the button
const eventId = $(this).attr('data-event-id');
const isFavourite = $(this).hasClass('favourite');
// optmisitically update the UI
if (isFavourite) {
$(this).removeClass('favourite')
$(this).innerHTML = `<i class="f7-icons">heart</i> Favourite`
} else {
$(this).addClass('favourite')
$(this).innerHTML = `<i class="f7-icons">heart_fill</i> Favourite`
}
// call the API to favourite the event
await maybeFavoriteEvent(eventId);
})
$(document).on('click', '#copy-event-link', function () {
const eventId = $(this).attr('data-event-id');
const eventLink = `${window.location.origin}/discover-view-event/${eventId}`;
navigator.clipboard.writeText(eventLink);
app.toast.create({
text: 'Link copied to clipboard',
closeTimeout: 2000
}).open()
});
$(document).on('click', '#share-email-event-link', function () {
const eventId = $(this).attr('data-event-id');
const eventLink = `${window.location.origin}/discover-view-event/${eventId}`;
window.open(`mailto:?subject=Event Link&body=${eventLink}`, '_blank');
});
/js/homepage.js
import app, {
showToast
} from "./app.js"
import store from "./store.js"
import {
formatPostDate
} from './utils.js'
import {
fetchComments,
maybeLikePost,
maybeLikeComment,
addComment,
deletePost,
deleteComment
} from './api/posts.js'
import {
getSessionUser
} from "./api/auth.js"
var $ = Dom7
var currentPostsPage = 1
var currentFollowingPostsPage = 1
var postsStore = store.getters.posts
var followingPostsStore = store.getters.followingPosts
var totalPostPages = 0
var totalFPostPages = 0
// Infinite Scroll Event
var isFetchingPosts = false
var activeTab = 'latest'
var refreshed = false
//screen width
var containerWidth = window.innerWidth
// Function to pause all videos
function pauseAllVideos() {
var videos = document.querySelectorAll('video.video-js');
videos.forEach(function (video) {
video.pause();
});
}
export function loadVideos() {
var videos = document.querySelectorAll('video.video-js');
// Function to resume videos if needed (optional)
function playVisibleVideos() {
videos.forEach(function (video) {
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
video.play();
}
});
}, { threshold: 0.5 });
observer.observe(video);
});
}
// Listen for visibility change
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
// Pause all videos when the user changes tabs or minimizes
pauseAllVideos();
} else {
// Optionally resume videos if visible
playVisibleVideos();
}
});
// Loop through each video element
videos.forEach(function (video) {
var videoSrc = video.getAttribute('data-src');
if (Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(videoSrc);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = videoSrc;
video.addEventListener('loadedmetadata', function () {
});
}
// Set up IntersectionObserver to pause/play videos based on visibility
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
video.play();
} else {
video.pause();
}
});
}, { threshold: 0.5 });
observer.observe(video);
video.addEventListener('play', function () {
video.removeAttribute('controls'); // Hide controls
});
video.addEventListener('click', function () {
video.setAttribute('controls', 'controls');
});
});
}
postsStore.onUpdated(async (data) => {
totalPostPages = data.total_pages
if ((data.page == data.total_pages) || (data.new_data.length == 0)) {
$('.infinite-scroll-preloader.home-posts').hide()
if (data.data.length == 0) {
$('#tab-latest .data').html('<p class="text-center">No posts</p>')
return;
}
}
if (data.reset) {
refreshed = data.reset
}
displayPosts(data.new_data, false)
})
followingPostsStore.onUpdated((data) => {
totalFPostPages = data.total_pages
if ((data.page == data.total_pages) || (data.new_data.length == 0)) {
$('.infinite-scroll-preloader.home-following-posts').hide()
if (data.data.length == 0) {
$('#tab-following .data').html('<p class="text-center">No posts</p>')
return;
}
}
if (data.reset) {
refreshed = data.reset
}
displayPosts(data.new_data, true)
})
$(document).on('infinite', '.infinite-scroll-content.home-page', async function () {
if (isFetchingPosts) return
const totalPages = activeTab === 'following' ? totalFPostPages : totalPostPages
const storeName = activeTab === 'following' ? 'getFollowingPosts' : 'getPosts'
if (activeTab === 'following') {
currentFollowingPostsPage++
} else {
currentPostsPage++
}
const currentPage = activeTab === 'following' ? currentFollowingPostsPage : currentPostsPage
if (currentPage >= totalPages) {
return
}
isFetchingPosts = true
await store.dispatch(storeName, {
page: currentPage
})
isFetchingPosts = false
})
$(document).on('page:beforein', '.page[data-name="social"]', function (e) {
const ptrContent = app.ptr.get('.ptr-content.home-page')
ptrContent.on('refresh', async function () {
refreshed = true
const storeName = activeTab === 'following' ? 'getFollowingPosts' : 'getPosts'
if (isFetchingPosts) return
isFetchingPosts = true
if (activeTab === 'following') {
currentFollowingPostsPage = 1
} else {
currentPostsPage = 1
}
await store.dispatch(storeName, {
page: 1,
clear: true
})
isFetchingPosts = false
app.ptr.done()
})
app.toolbar.show('.toolbar.toolbar-bottom', true)
})
// before page out
$(document).on('page:beforeout', '.page[data-name="social"]', function () {
pauseAllVideos()
})
/* Based on this http://jsfiddle.net/brettwp/J4djY/*/
export function detectDoubleTapClosure(callback) {
let lastTap = 0
let timeout
return function detectDoubleTap(event) {
const curTime = new Date().getTime()
const tapLen = curTime - lastTap
if (tapLen < 500 && tapLen > 0) {
event.preventDefault()
// pass the event target to the callback
callback(event.target)
} else {
timeout = setTimeout(() => {
clearTimeout(timeout)
}, 500)
}
lastTap = curTime
}
}
// event listener for tab change
$(document).on('click', '.social-tabs .tab-link', async function (e) {
const type = this.getAttribute('data-type')
activeTab = type
})
function preloadImage(url) {
const MAX_PRELOADS = 10;
// Get all preloaded images
const preloadedImages = document.querySelectorAll('.post-media-preloader');
// If there are more than 10 preloaded images, remove the first one
if (preloadedImages.length >= MAX_PRELOADS) {
preloadedImages[0].remove();
}
// Preload the image
document.head.insertAdjacentHTML('beforeend', `
<link rel="preload" href="${url}" as="image" class="post-media-preloader" />
`);
}
async function displayPosts(posts, following = false) {
const postsContainer = $(following ? '#tab-following .data' : '#tab-latest .data');
if (refreshed) {
postsContainer.html('')
refreshed = false
}
const user = await getSessionUser()
posts.forEach(post => {
let post_actions = `
<div class="media-post-actions">
<div class="media-post-like" data-post-id="${post.id}">
<i class="icon f7-icons ${post.is_liked ? 'text-red' : ''}" data-post-id="${post.id}">${post.is_liked ? 'heart_fill' : 'heart'}</i>
</div>
<div class="media-post-comment popup-open" data-popup=".comments-popup" data-post-id="${post.id}">
<i class="icon f7-icons">chat_bubble</i>
</div>
<div class="media-post-share popup-open" data-popup=".share-popup">
<i class="icon f7-icons">paperplane</i>
</div>
`;
if (post.user_id == user.id) {
post_actions += `
<div class="media-post-edit popup-open" data-popup=".edit-post-popup" data-post-id="${post.id}">
<i class="icon f7-icons">gear_alt</i>
</div>
`;
}
post_actions += `</div>`;
const date = formatPostDate(post.post_date);
const maxDescriptionLength = 200; // Set your character limit here
const isLongDescription = post.caption.length > maxDescriptionLength;
const shortDescription = isLongDescription ? post.caption.slice(0, maxDescriptionLength) : post.caption;
let imageHeight = 400;
if (post.media.length > 0) {
const intrinsicWidth = post.media[0].media_width;
const intrinsicHeight = post.media[0].media_height;
const media_type = post.media[0].media_type;
// Calculate intrinsic aspect ratio
const intrinsicRatio = intrinsicWidth / intrinsicHeight;
// Calculate the rendered height based on the container width
const renderedHeight = containerWidth / intrinsicRatio;
// Use either the rendered height or the fallback height
if (renderedHeight > 0) {
if (renderedHeight > 500) {
imageHeight = 500
} else {
imageHeight = renderedHeight
}
if (media_type === 'video') {
imageHeight = renderedHeight
}
}
}
let profile_link;
if (post.user_id == user.id) {
profile_link = `
<a href="#" class="view-profile media-post-header">
<div class="media-post-avatar" style="background-image: url('${post.user_profile_image || 'assets/img/profile-placeholder.jpg'}');"></div>
<div class="media-post-user">${post.username}</div>
<div class="media-post-date">${date}</div>
</a>`
} else {
profile_link = `
<a href="/profile-view/${post.user_id}" class="media-post-header">
<div class="media-post-avatar" style="background-image: url('${post.user_profile_image || 'assets/img/profile-placeholder.jpg'}');"></div>
<div class="media-post-user">${post.username}</div>
<div class="media-post-date">${date}</div>
</a>`
}
const postItem = `
<div class="media-post" data-post-id="${post.id}" data-is-liked="${post.is_liked}">
<div class="media-post-content">
${profile_link}
<div class="media-post-content">
<swiper-container pagination class="demo-swiper-multiple" space-between="50">
${post.media.map((mediaItem, index) => {
// Preload the first image in the post
if (index === 0 && mediaItem.media_type !== 'video') {
preloadImage(mediaItem.media_url)
}
// // create a url encoded string for the media url
// const videoThumbnail = mediaItem.media_type === 'video' ?
// encodeURIComponent(`${mediaItem.media_url}/thumbnails/thumbnail.jpg`) : '';
return `
<swiper-slide class="swiper-slide post-media ${mediaItem.media_type === 'video' ? 'video' : ''}" style="height: ${imageHeight}px; ">
${mediaItem.media_type === 'video' ?
`<video
style="height: ${imageHeight}px;"
class="video-js"
data-src="${mediaItem.media_url}/manifest/video.m3u8"
preload="auto"
playsinline
loop
controls
autoplay
poster="${mediaItem.media_url}/thumbnails/thumbnail.jpg" <!-- Add the thumbnail as the poster image -->
></video>`
: `<img src="${mediaItem.media_url}"
alt="${mediaItem.caption || post.username + 's post'}"
style="text-align: center;"
onerror = "this.style.display='none';"
/>`}
</swiper-slide>`}).join('')}
</swiper-container>
</div>
${post_actions}
<div class="media-post-likecount" data-like-count="${post.likes_count}">${post.likes_count} likes</div>
<div class="media-post-description">
<strong>${post.username}</strong> <br/> <span class="post-caption">${shortDescription}</span>
<span class="full-description hidden">${post.caption}</span>
${isLongDescription ? `<span class="media-post-readmore">... more</span>` : ''}
</div>
${post.comments_count > 0 ? `<div class="media-post-commentcount popup-open" data-popup=".comments-popup" data-post-id="${post.id}">View ${post.comments_count} comments</div>` : ''}
</div>
</div>
`;
postsContainer.append(postItem);
});
loadVideos()
}
export function togglePostLike(postId, single = false) {
// Find all post elements with the specified postId
let container = single ? `.media-post.single[data-post-id="${postId}"]` : `.media-post[data-post-id="${postId}"]`
const postElements = document.querySelectorAll(container)
// Iterate through all matching post elements and update them
postElements.forEach(postElement => {
const likeIcon = postElement.querySelector('.media-post-like i')
const isLiked = postElement.getAttribute('data-is-liked') === 'true'
const likeCountElem = postElement.querySelector('.media-post-likecount')
let likeCount = parseInt(likeCountElem.getAttribute('data-like-count'))
// Toggle the like state
if (isLiked) {
likeIcon.classList.remove('text-red')
likeIcon.innerText = 'heart'
likeCount--
postElement.setAttribute('data-is-liked', 'false')
} else {
likeIcon.classList.add('text-red')
likeIcon.innerText = 'heart_fill'
likeCount++
postElement.setAttribute('data-is-liked', 'true')
}
// Update like count
likeCountElem.innerText = `${likeCount} likes`
likeCountElem.setAttribute('data-like-count', likeCount)
if (single) {
var pathStore = store.getters.getPathData
if (pathStore && pathStore.value[`/post/${postId}`]) {
var post = pathStore.value[`/post/${postId}`]
post.is_liked = !isLiked
post.likes_count = likeCount
store.dispatch('setPathData', {
path: `/post/${postId}`,
data: post,
})
}
}
})
// Optionally, make an API call to update the like status on the server
maybeLikePost(postId)
}
function displayComments(comments, postId) {
const user = store.getters.user.value
const commentsContainer = document.getElementById('comments-list')
// reset the comments container
commentsContainer.innerHTML = ''
const commentForm = document.getElementById('comment-form')
commentForm.setAttribute('data-post-id', postId)
if (!comments.length) {
commentsContainer.innerHTML = '<div class="no-comments">No comments found</div>'
return
}
comments.forEach(comment => {
const replyItems = comment.replies.length > 0 ? `
<div class="comment-replies">
<span class="comment-replies-toggle" data-replies-count="${comment.replies.length}">
Show ${comment.replies.length} ${comment.replies.length > 1 ? 'replies' : 'reply'}
</span>
<div class="comment-replies-container">
${comment.replies.map(reply => {
// Determine the delete button visibility
const deleteButton = reply.user_id == user.id ?
`<div class="comment-delete" data-comment-id="${reply.id}">
<i class="icon f7-icons text-red">trash</i>
</div>` :
'';
return `
<div class="comment" data-comment-id="${reply.id}" data-is-liked="${reply.liked}" data-owner-id="${reply.user_id}"
data-owner-name="${reply.user_login}">
<a href="#" data-url="${reply.user_id == user.id ? '#' : `/profile-view/${reply.user_id}`}" class="${reply.user_id == user.id ? 'view-profile' : ''} comment-profile-img" style="background-image:url('${reply.profile_image || 'assets/img/profile-placeholder.jpg'}');">
</a>
<div class="comment-content-container">
<div class="comment-username">
<a href="#" data-url="${reply.user_id == user.id ? '#' : `/profile-view/${reply.user_id}`}" class="${reply.user_id == user.id ? 'view-profile' : 'a'}">
${reply.user_login}
</a>
<span class="date">${formatPostDate(reply.comment_date)}</span>
</div>
<div class="comment-content">${reply.comment}</div>
<div class="comment-actions">
<div class="comment-like">
<i class="icon f7-icons ${reply.liked ? 'text-red' : ''}">
${reply.liked ? 'heart_fill' : 'heart'}
</i>
<span class="comment-likes-count" data-likes-count="${reply.likes_count}">
${reply.likes_count}
</span>
</div>
<div class="comment-reply">
<i class="icon f7-icons">chat_bubble</i> <span>Reply</span>
</div>
${deleteButton}
</div>
</div>
<div class="clearfix"></div>
</div>`;
}).join('')}
</div>
</div>` : '';
let commenter_link = `/profile-view/${comment.user_id}`;
if (comment.user_id == user.id) {
commenter_link = '/profile/';
}
const deleteButton = comment.user_id == user.id ?
`<div class="comment-delete" data-comment-id="${comment.id}"><i class="icon f7-icons text-red">trash</i></div>` : '';
const commentItem = `
<div class="comment"
data-comment-id="${comment.id}"
data-is-liked="${comment.liked}"
data-owner-id="${comment.user_id}"
data-owner-name="${comment.user_login}">
<a href="#" data-url="${comment.user_id == user.id ? '#' : `/profile-view/${comment.user_id}`}" class="${comment.user_id == user.id ? 'view-profile' : ''} comment-profile-img"
style="background-image:url('${comment.profile_image || 'assets/img/profile-placeholder.jpg'}');">
</a>
<div class="comment-content-container">
<div class="comment-username">
<a href="#" data-url="${comment.user_id == user.id ? '#' : `/profile-view/${comment.user_id}`}" class="${comment.user_id == user.id ? 'view-profile' : 'a'}">
${comment.user_login}
</a>
<span class="date">${formatPostDate(comment.comment_date)}</span>
</div>
<div class="comment-content">${comment.comment}</div>
<div class="comment-actions">
<div class="comment-like">
<i class="icon f7-icons ${comment.liked && 'text-red'}">${comment.liked ? 'heart_fill' : 'heart'}</i>
<span class="comment-likes-count" data-likes-count="${comment.likes_count}">
${comment.likes_count}
</span>
</div>
<div class="comment-reply">
<i class="icon f7-icons">chat_bubble</i> <span>Reply</span>
</div>
${deleteButton}
</div>
${replyItems}
</div>
<div class="clearfix"></div>
</div>
`
commentsContainer.insertAdjacentHTML('beforeend', commentItem)
})
// Add click event listener for liking a comment
const likeButtons = document.querySelectorAll('.comment-like')
likeButtons.forEach(button => {
button.addEventListener('click', (event) => {
const commentId = event.currentTarget.closest('.comment').getAttribute('data-comment-id')
const ownerId = event.currentTarget.closest('.comment').getAttribute('data-owner-id')
toggleCommentLike(commentId, ownerId)
})
})
}
function toggleCommentLike(commentId, ownerId) {
// Find the comment element and its like icon
const commentElement = document.querySelector(`.comment[data-comment-id="${commentId}"]`)
const likeIcon = commentElement.querySelector('.comment-like i')
const isLiked = commentElement.getAttribute('data-is-liked') === 'true'
const likeCountElem = commentElement.querySelector('.comment-likes-count')
let likeCount = parseInt(likeCountElem.getAttribute('data-likes-count'))
// Toggle the like state
if (isLiked) {
likeIcon.classList.remove('text-red')
likeIcon.innerText = 'heart'
likeCount--
commentElement.setAttribute('data-is-liked', 'false')
} else {
likeIcon.classList.add('text-red')
likeIcon.innerText = 'heart_fill'
likeCount++
commentElement.setAttribute('data-is-liked', 'true')
}
// Update like count
likeCountElem.innerText = likeCount
likeCountElem.setAttribute('data-likes-count', likeCount)
maybeLikeComment(commentId, ownerId)
}
$(document).on('click', '.media-post-readmore', function () {
const postDescription = this.previousElementSibling.previousElementSibling; // The short description
const fullDescription = this.previousElementSibling; // The full description
if (fullDescription.classList.contains('hidden')) {
postDescription.classList.add('hidden');
fullDescription.classList.remove('hidden');
this.textContent = '... less';
} else {
postDescription.classList.remove('hidden');
fullDescription.classList.add('hidden');
this.textContent = '... more';
}
});
$(document).on('click', '.media-post-like i', (e) => {
const postId = e.target.getAttribute('data-post-id')
const parent = e.target.closest('.media-post')
const isSingle = parent.classList.contains('single') ? true : false
togglePostLike(postId, isSingle)
})
// set the post id as a data attribute from the edit post popup
$(document).on('click', '.media-post-edit', function () {
const postId = $(this).closest('.media-post').attr('data-post-id')
const isSingleView = $(this).closest('.media-post').hasClass('single')
$('.edit-post-popup').attr('data-post-id', postId)
$('.edit-post-popup').attr('data-is-single', isSingleView)
})
$(document).on('click', '#delete-post', function () {
var view = app.views.current
// set the post id as a data attribute from the edit post popup
const postId = $('.edit-post-popup').attr('data-post-id')
const isSingleView = $('.edit-post-popup').attr('data-is-single')
app.dialog.confirm('Are you sure you want to delete this post?', 'Delete Post', async () => {
app.preloader.show()
const response = await deletePost(postId)
app.preloader.hide()
if (response) {
store.dispatch('getMyPosts', {
page: 1,
clear: true
})
store.dispatch('getMyTags', {
page: 1,
clear: true
})
console.log(isSingleView);
if (isSingleView == 'true') {
// $('.view-profile-link').click()
view.router.back()
// view.router.navigate('/profile/')
}
showToast('Post deleted successfully')
// remove the post from the DOM
$(`.media-post[data-post-id="${postId}"]`).remove()
app.popup.close('.edit-post-popup')
} else {
showToast('Failed to delete post')
app.preloader.hide()
}
})
})
$(document).on('click', '#edit-post', function () {
var view = app.views.current
// set the post id as a data attribute from the edit post popup
const postId = $('.edit-post-popup').attr('data-post-id')
view.router.navigate(`/post-edit/${postId}`, {
force: true
})
app.popup.close('.edit-post-popup')
})
$(document).on('touchstart', '.media-post-content .post-media', detectDoubleTapClosure((e) => {
const parent = e.closest('.media-post')
const postId = parent.getAttribute('data-post-id')
const isLiked = parent.getAttribute('data-is-liked') === 'true'
if (isLiked) {
return
}
togglePostLike(postId)
}), {
passive: false
})
// media-post-video click
$(document).on('click', '.media-post-video', function () {
if (this.paused) {
this.play()
} else {
this.pause()
}
})
// on .popup-open click
$(document).on('click', '.media-post-comment, .media-post-commentcount', async function () {
const postId = this.getAttribute('data-post-id')
if (!postId) {
return
}
document.getElementById('comments-list').innerHTML = '<div class="preloader"></div>'
document.getElementById('comment-form').reset()
// update the post id in the comment form
document.getElementById('comment-form').setAttribute('data-post-id', '')
document.getElementById('comment-form').removeAttribute('data-comment-id')
document.getElementById('comment-form').querySelector('.replying-to').innerHTML = ''
document.getElementById('comment-form').querySelector('.replying-to').classList.add('hidden')
try {
const comments = await fetchComments(postId)
displayComments(comments, postId)
} catch (error) {
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to fetch comments',
}).open()
}
})
$(document).on('click', '.media-post-share', function () {
// set the post id as a data attribute
const postId = $(this).closest('.media-post').attr('data-post-id')
$('.share-popup').attr('data-post-id', postId)
$('#copy-link').attr('data-clipboard-text', `${window.location.origin}/post-view/${postId}`)
})
$(document).on('click', '#share-post-email', function () {
const postId = $(this).closest('.popup').attr('data-post-id')
const postLink = `${window.location.origin}/post-view/${postId}`
// open the email composer
window.open(`mailto:?subject=Check out this post&body=${postLink}`)
})
// data-clipboard-text click
$(document).on('click', '#copy-link', function () {
const copyText = $(this).attr('data-clipboard-text')
navigator.clipboard.writeText(copyText)
app.toast.create({
text: 'Link copied to clipboard',
closeTimeout: 2000
}).open()
})
// on .comment-replies-toggle click
$(document).on('click', '.comment-replies-toggle', function () {
const commentRepliesContainer = this.nextElementSibling
commentRepliesContainer.classList.toggle('show')
const repliesCount = this.getAttribute('data-replies-count')
this.innerText = this.innerText === `Show ${repliesCount} replies` ? `Hide ${repliesCount} replies` : `Show ${repliesCount} replies`
})
// on comment form submit
$('#comment-form').on('submit', async function (e) {
e.preventDefault()
const postId = this.getAttribute('data-post-id')
const commentId = this.getAttribute('data-comment-id')
const comment = this.comment.value
if (!comment) {
// app.dialog.alert('Please enter a comment')
return
}
app.preloader.show()
try {
const response = await addComment(postId, comment, commentId)
app.preloader.hide()
if (response) {
this.reset()
this.removeAttribute('data-comment-id')
this.querySelector('.replying-to').innerHTML = ''
this.querySelector('.replying-to').classList.add('hidden')
const comments = await fetchComments(postId)
displayComments(comments, postId)
} else {
app.notification.create({
text: 'Failed to add comment',
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
}).open()
}
} catch (error) {
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to add comment',
}).open()
app.preloader.hide()
}
})
//.comment-reply click
$(document).on('click', '.comment-reply', function () {
// get the comment id, and comment owner id
const commentId = this.closest('.comment').getAttribute('data-comment-id')
const ownerId = this.closest('.comment').getAttribute('data-owner-id')
const ownerName = this.closest('.comment').getAttribute('data-owner-name')
// add something above the comment form to show the user they are replying to a comment
// add the comment id to the form
document.getElementById('comment-form').setAttribute('data-comment-id', commentId)
document.getElementById('comment-form').comment.focus()
// add the owner name to the form
// <span class="replying-to">Replying to <strong>m88xrk</strong></span>
const replyingTo = document.getElementById('comment-form').querySelector('.replying-to')
replyingTo.innerHTML = `Replying to <strong>${ownerName}</strong>`
replyingTo.classList.remove('hidden')
document.getElementById('comment-form').prepend(replyingTo)
})
$(document).on('click', '.comment-delete', async function () {
app.dialog.confirm('Are you sure you want to delete this comment? This will remove all replies to this comment', 'Delete Comment', async () => {
try {
const commentId = this.getAttribute('data-comment-id')
const response = await deleteComment(commentId)
if (response && response.success) {
// remove the comment from the DOM
$(`.comment[data-comment-id="${commentId}"]`).remove()
showToast('Comment deleted successfully')
}
} catch (error) {
app.dialog.alert('Failed to delete comment')
}
})
})
$(document).on('click', '.comment a', function (e) {
var view = app.views.current
// hide the comments popup
app.popup.close()
// get the href attribute
const href = this.getAttribute('data-url')
if (!href || href === '#') {
return
}
// prevent the default action
e.preventDefault()
view.router.navigate(href, {
force: true
})
})
/js/notifications.js
import {
getSessionUser
} from "./api/auth.js"
import {
approvePostTag,
maybeFollowUser,
removeTagFromPost
} from "./api/profile.js"
import app, { showToast } from "./app.js"
import store from "./store.js"
var $ = Dom7
var notificationsStore = store.getters.getNotifications
var refreshed = false
var userStore = store.getters.user
let notificationInterval = null
$(document).on('page:afterin', '.page[data-name="notifications"]', async function (e) {
const data = notificationsStore.value
if (!data || !data.success) {
return
}
// a list if all unread notification ids
const unreadNotificationIds = [
...data.data.recent,
...data.data.last_week,
...data.data.last_30_days,
].filter((item) => item.is_read === "0").map((item) => item._id);
if (unreadNotificationIds.length > 0) {
store.dispatch('markNotificationsAsRead', unreadNotificationIds)
}
})
userStore.onUpdated((data) => {
if (data && data.id) {
store.dispatch('notificationCount')
store.dispatch('fetchNotifications', {
load_more: false
})
// fetch notifications every 1 min
// create an interval to fetch notifications every 1 min
if (!notificationInterval) {
notificationInterval = setInterval(() => {
refreshed = true
store.dispatch('notificationCount')
store.dispatch('fetchNotifications', {
load_more: false
})
}, 60000)
} else {
clearInterval(notificationInterval)
notificationInterval = setInterval(() => {
refreshed = true
store.dispatch('notificationCount')
store.dispatch('fetchNotifications', {
load_more: false
})
}, 60000)
}
}
if (!data || !data.id) {
clearInterval(notificationInterval)
}
})
notificationsStore.onUpdated(async (data) => {
if (!data || !data.success) {
$('.notification-wrap').html('<p class="text-center">No notifications</p>')
return
}
const notifications = data.data
const recentContainer = document.getElementById('recent');
const thisWeekContainer = document.getElementById('this-week');
const last30DaysContainer = document.getElementById('last-30-days');
if (refreshed) {
recentContainer.innerHTML = '';
thisWeekContainer.innerHTML = '';
last30DaysContainer.innerHTML = '';
refreshed = false
}
var user = await getSessionUser()
document.querySelectorAll('.app-notification-title').forEach(elem => {
if (elem.getAttribute('data-id') === 'last-30') {
if (notifications.last_30_days.length > 0) {
elem.innerHTML = elem.getAttribute('data-title');
} else {
elem.innerHTML = '';
}
return
}
elem.innerHTML = elem.getAttribute('data-title');
})
if (!notifications.recent.length && !notifications.is_paginated) {
recentContainer.innerHTML = '<p class="text-center">No recent notifications</p>';
}
if (!notifications.last_week.length && !notifications.has_more_notifications) {
thisWeekContainer.innerHTML = '<p class="text-center">No notifications from this week</p>';
}
notifications.last_30_days.forEach(notification => {
const notificationItem = createNotificationItem(notification, user);
last30DaysContainer.appendChild(notificationItem);
});
notifications.recent.forEach(notification => {
const notificationItem = createNotificationItem(notification, user);
recentContainer.appendChild(notificationItem);
});
notifications.last_week.forEach(notification => {
const notificationItem = createNotificationItem(notification, user);
thisWeekContainer.appendChild(notificationItem);
});
// add a load more button at the end
if ((notifications.recent.length >= 0 || notifications.last_week.length >= 0) && (notifications.has_more_notifications)) {
$('.load-more-notifications').removeClass('hidden');
} else {
$('.load-more-notifications').addClass('hidden');
}
})
$(document).on('click', '.load-more-notifications', async function (e) {
await store.dispatch('fetchNotifications', {
load_more: true
})
})
function timeAgo(dateString) {
const now = new Date();
const past = new Date(dateString);
const diffInHours = Math.floor((now - past) / (1000 * 60 * 60));
return diffInHours > 24 ? `${Math.floor(diffInHours / 24)}d ago` : `${diffInHours}h ago`;
}
function createNotificationItem(notification, user) {
const isFollow = notification.type === 'follow' ? true : false;
const container = document.createElement(isFollow ? 'div' : 'a');
if (!isFollow) {
container.href = `/post-view/${notification.entity.entity_id}`;
}
let isReadClass = notification.is_read == "0" ? "unread-notif" : "";
container.className = `notification-item ${isReadClass}`;
container.dataset.notificationId = notification._id;
// Profile image and notification content container
const leftContainer = document.createElement('div');
leftContainer.className = 'notification-left';
const imageDiv = document.createElement('a');
imageDiv.className = 'image-square image-rounded';
imageDiv.style.backgroundImage = `url('${notification.entity.initiator_data.profile_image || 'assets/img/profile-placeholder.jpg'}')`;
imageDiv.href = `/profile-view/${notification.entity.user_id}`;
const infoDiv = document.createElement('div');
infoDiv.className = 'notification-info';
let content = '';
// Conditional content rendering based on the type
if (notification.type === 'like') {
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> liked your ${notification.entity.entity_type}
${notification.entity.entity_data.comment ? `<span class="inline font-semibold text-black">: "${notification.entity.entity_data.comment}"</span>` : ''}
<span class=""></span>
</div>
`;
} else if (notification.type === 'comment') {
const eclipseComment = notification.entity.entity_data.comment.length > 50 ? notification.entity.entity_data.comment.substring(0, 50) + '...' : notification.entity.entity_data.comment;
container.href = `/post-view/${notification.entity.entity_data.post_id}?commentId=${notification.entity.entity_id}`;
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> commented on your post:
<span class="font-semibold text-black">"${eclipseComment}"</span>
<span class="${isReadClass}"></span>
</div>
`;
} else if (notification.type === 'follow') {
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> followed you
<span class="${isReadClass}"></span>
</div>
`;
} else if (notification.type === 'mention') {
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> mentioned you in a ${notification.entity.entity_type}
${notification.entity.entity_data.comment ? `<span class="block font-semibold text-black">"${notification.entity.entity_data.comment}"</span>` : ''}
<span class="${isReadClass}"></span>
</div>
`;
} else if (notification.type === 'post') {
// <a href="/profile-garage-vehicle-view/${notification.entity.entity_data?.garage?.id}">
// <strong>${notification.entity.entity_data?.garage?.make || ''} ${notification.entity.entity_data?.garage?.model || ''}</strong>
// </a>
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> has tagged ${notification.entity.entity_type === 'car' ? "your car" : "you"} in a post
<span class="${isReadClass}"></span>
</div>
${(notification.entity.entity_type === 'car' && !notification.entity.entity_data.tag_approved) ? `<div class="notification-text tag-actions">
<div class="btn btn-primary btn-sm approve-tag" data-tag-id="${notification.entity.entity_data.tag_id}">Approve</div>
<div class="btn btn-secondary btn-sm decline-tag" data-tag-id="${notification.entity.entity_data.tag_id}">Decline</div>
</div>` : ''}
`;
} else if (notification.type === 'tag') {
content = `
<div class="notification-text">
<a href="/profile-view/${notification.entity.user_id}"><strong>${notification.entity.initiator_data.display_name}</strong></a> ${notification.entity.entity_type === 'car' ? "tagged your car in a post" : "tagged you in a post"}
<span class="${isReadClass}"></span>
</div>
${(!notification.entity.entity_data.tag_approved && notification.entity.entity_data.tag_id) ? `<div class="notification-text tag-actions">
<div class="btn btn-primary btn-sm approve-tag" data-tag-id="${notification.entity.entity_data.tag_id}">Approve</div>
<div class="btn btn-secondary btn-sm decline-tag" data-tag-id="${notification.entity.entity_data.tag_id}">Decline</div>
</div>` : ''}
`;
container.href = `#`;
}
// Add time ago
const timeSpan = document.createElement('span');
timeSpan.className = 'notification-time';
timeSpan.textContent = timeAgo(notification.date);
infoDiv.innerHTML = `${content} ${timeSpan.outerHTML}`;
// Adding profile image and content to the left container
leftContainer.appendChild(imageDiv);
leftContainer.appendChild(infoDiv);
// Append left container to the main container
container.appendChild(leftContainer);
if (notification.type === 'follow') {
const isFollowing = user.following.includes(notification.entity.user_id);
if (!isFollowing) {
let followBtn = `<div class="btn btn-primary btn-sm toggle-follow" data-is-following="${isFollowing}" data-user-id="${notification.entity.user_id}">
Follow
</div>`;
container.innerHTML += followBtn;
}
} else {
const rightContainer = document.createElement('a');
rightContainer.className = 'notification-left';
let path;
if (notification.entity.entity_type === 'car') {
path = 'post-view';
rightContainer.href = `/${path}/${notification.entity.entity_data.post_id}`;
} else if (notification.entity.entity_type === 'post' || notification.entity.entity_type === 'tag') {
path = 'post-view';
rightContainer.href = `/${path}/${notification.entity.entity_id}`;
} else if (notification.entity.entity_type === 'comment') {
path = 'post-view';
rightContainer.href = `/${path}/${notification.entity.entity_data.post_id}?commentId=${notification.entity.entity_id}`;
} else {
path = 'profile-view';
rightContainer.href = `/${path}/${notification.entity.user_id}`;
}
if (notification.type === 'tag') {
path = 'post-view';
rightContainer.href = `/${path}/${notification.entity.entity_id}`;
}
const imageDiv = document.createElement('div');
imageDiv.className = 'image-square image-rounded';
imageDiv.style.backgroundImage = `url('${notification.entity.entity_data.media}')`;
imageDiv.style.backgroundSize = 'cover';
imageDiv.style.backgroundPosition = 'center';
imageDiv.style.backgroundColor = '#f1f1f1';
rightContainer.appendChild(imageDiv);
container.appendChild(rightContainer)
}
return container;
}
$(document).on('click', '.toggle-follow', async function (e) {
const userId = e.target.dataset.userId;
const isFollowing = e.target.dataset.isFollowing === 'true';
if (!isFollowing) {
// hide the button
e.target.style.display = 'none';
}
// update the button text
e.target.textContent = isFollowing ? 'Follow' : 'Unfollow';
e.target.dataset.isFollowing = !isFollowing;
await maybeFollowUser(userId);
store.dispatch('updateUserDetails')
});
$(document).on('ptr:refresh', '.notification-page.ptr-content', async function (e) {
refreshed = true
try {
await store.dispatch('notificationCount')
await store.dispatch('fetchNotifications', {
load_more: false
})
} catch (error) {
console.log(error);
}
app.ptr.get('.notification-page.ptr-content').done()
})
$(document).on('click', '.approve-tag', async function (e) {
e.preventDefault()
const tagId = e.target.dataset.tagId
app.dialog.confirm('Are you sure you want to approve this tag?', 'Approve Tag', async function () {
try {
app.preloader.show()
const response = await approvePostTag(tagId)
app.preloader.hide()
if (response.success) {
// remove the buttons
e.target.parentElement.innerHTML = ''
showToast('Tag has been approved')
} else {
showToast(response.message || 'Failed to approve tag')
}
} catch (error) {
app.preloader.hide()
showToast('Failed to approve tag')
}
})
})
$(document).on('click', '.decline-tag', async function (e) {
e.preventDefault()
const tagId = e.target.dataset.tagId
app.dialog.confirm('Are you sure you want to decline this tag?', 'Decline Tag', async function () {
try {
app.preloader.show()
const response = await removeTagFromPost(tagId)
app.preloader.hide()
if (response.success) {
// refetch notifications
store.dispatch('fetchNotifications', {
load_more: false
})
showToast('Tag has been declined')
} else {
showToast(response.message || 'Failed to decline tag')
}
} catch (error) {
app.preloader.hide()
showToast('Failed to decline tag')
}
})
})
/js/profile-edit.js
import {
deleteUserAccount,
getSessionUser,
updatePassword,
updateUserDetails,
updateUsername
} from "./api/auth.js"
import {
addUserProfileLinks,
removeProfileLink,
updateCoverImage,
updateProfileImage,
updateSocialLinks
} from "./api/profile.js"
import app, {
showToast
} from "./app.js"
import store from "./store.js"
var $ = Dom7
// --------------- Edit Profile Page ---------------
$(document).on('page:init', '.page[data-name="profile-edit-mydetails"]', async function (e) {
var view = app.views.current
const user = await getSessionUser()
const isEmailVerified = user.email_verified ?? false;
if (!user) {
view.router.navigate('/login')
return
}
// Example of how to fill in the form fields with the provided data
document.querySelector('input[name="email"]').value = user.email || '';
// if email is not verified, show the verify email button
if (isEmailVerified) {
$('#email-verify-span').remove()
}
document.querySelector('input[name="first_name"]').value = user.first_name || '';
document.querySelector('input[name="last_name"]').value = user.last_name || '';
document.querySelector('input[name="tel_no"]').value = user.billing_info?.phone || '';
})
$(document).on('click', '#save-details', async function () {
var view = app.views.current
const user = await getSessionUser()
// Get the values from the input fields
const firstName = $('input[name="first_name"]').val().trim();
const lastName = $('input[name="last_name"]').val().trim();
const email = $('input[name="email"]').val().trim();
const telNo = $('input[name="tel_no"]').val().trim();
// Validate the input fields
if (firstName === '') {
showToast('Please enter your first name', 'Error');
return;
}
if (lastName === '') {
showToast('Please enter your last name', 'Error');
return;
}
if (email === '') {
showToast('Please enter your email', 'Error');
return;
}
// Simple email validation
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
showToast('Please enter a valid email address', 'Error');
return;
}
// // phone validation
// if (telNo === '') {
// showToast('Please enter a valid phone number', 'Error');
// return
// }
// Prepare the data for the API request
const requestData = {
first_name: firstName,
last_name: lastName,
email: email,
phone: telNo,
};
// check if the request data is the same as userdata
let dirtied = false;
for (const key in requestData) {
if (key == 'phone') {
if (requestData[key] !== user.billing_info.phone) {
dirtied = true
break;
}
continue
}
if (requestData[key] !== user[key]) {
dirtied = true
break;
}
}
if (!dirtied) {
return
}
try {
app.preloader.show()
const response = await updateUserDetails(requestData, email !== user.email)
app.preloader.hide()
if (response && response.success) {
// showToast('Details updated successfully', 'Success');
showToast('Details updated successfully')
view.router.navigate('/profile/')
store.dispatch('updateUserDetails')
return;
}
throw new Error(response.message);
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update details',
}).open()
}
});
$(document).on('click', '#update_password', async function () {
var view = app.views.current
// Get the values from the password input fields
const password = $('input[name="password"]').val().trim();
const current_password = $('input[name="current_password"]').val().trim();
const confirmPassword = $('input[name="confirm_password"]').val().trim();
if (current_password === '') {
showToast('Please enter your current password', 'Error');
return
}
if (password.length < 8) {
showToast('Password must be at least 8 characters long.')
return
}
// Check if password contains at least one lowercase letter
if (!/[a-z]/.test(password)) {
showToast('Password must contain at least one lowercase letter.')
return
}
// Check if password contains at least one uppercase letter
if (!/[A-Z]/.test(password)) {
showToast('Password must contain at least one uppercase letter.')
return
}
// Check if password contains at least one number
if (!/\d/.test(password)) {
showToast('Password must contain at least one number.')
return
}
if (confirmPassword === '') {
showToast('Please confirm your password', 'Error');
return;
}
if (password !== confirmPassword) {
showToast('Passwords do not match', 'Error');
return;
}
try {
app.preloader.show()
const response = await updatePassword(password, current_password)
app.preloader.hide()
if (response && response.success) {
showToast('Password updated successfully')
view.router.navigate('/profile/')
return;
}
throw new Error(response.message);
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update password',
}).open()
}
})
// --------------- End Edit Profile Page ---------------
// --------------- Edit Username Page ---------------
$(document).on('page:beforein', '.page[data-name="profile-edit-username"]', async function (e) {
var view = app.views.current
const user = await getSessionUser()
if (!user) {
view.router.navigate('/login')
return
}
// Example of how to fill in the form fields with the provided data
$('.profile-edit-view input[name="username"]').val(user.username || '')
if (user.can_update_username) {
$('#username-editable').remove()
} else {
document.querySelector('#username-editable').innerText = `You can change your username in ${user.next_update_username} days`
}
})
$(document).on('click', '#save-username', async function () {
var view = app.views.current
const user = await getSessionUser()
if (!user.can_update_username) {
return
}
const username = $('.profile-edit-view input[name="username"]').val()
if (username === '') {
showToast('Please enter a username', 'Error')
return
}
// username can only have letters, numbers, and underscores
var usernamePattern = /^[a-zA-Z0-9_]+$/
if (!usernamePattern.test(username)) {
showToast('Username can only contain letters, numbers, and underscores')
return
}
// username must be at least 3 characters long
if (username.length < 3) {
showToast('Username must be at least 3 characters long')
return
}
if (username === user.username) {
return
}
try {
app.preloader.show()
const response = await updateUsername(username)
app.preloader.hide()
if (response && response.success) {
showToast('Username updated successfully', 'Success')
view.router.navigate('/profile/')
store.dispatch('updateUserDetails')
} else {
throw new Error(response.message)
}
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update username',
}).open()
}
})
// --------------- End Edit Username Page ---------------
// --------------- Edit Profile images Page ---------------
$(document).on('page:init', '.page[data-name="profile-edit-images"]', async function (e) {
var view = app.views.current
const user = await getSessionUser()
if (!user) {
view.router.navigate('/login')
return
}
// If a cover photo exists, use it as the background image of the upload label
if (user.cover_image) {
$('input[name="cover_image"]').closest('.custom-file-upload').find('label').css('background-image', `url('${user.cover_image}')`)
$('input[name="cover_image"]').closest('.custom-file-upload').find('label').css('background-size', 'cover')
}
if (user.profile_image) {
$('input[name="profile_image"]').closest('.custom-file-upload').find('label').css('background-image', `url('${user.profile_image}')`)
$('input[name="profile_image"]').closest('.custom-file-upload').find('label').css('background-size', 'contain')
$('input[name="profile_image"]').closest('.custom-file-upload').find('label').css('background-position', 'center')
$('input[name="profile_image"]').closest('.custom-file-upload').find('label').css('background-repeat', 'no-repeat')
}
})
$(document).on('click', '#save-profile-images', async function () {
var view = app.views.current
const cover_image = $('input[name="cover_image"]').prop('files')[0]
const profile_image = $('input[name="profile_image"]').prop('files')[0]
let coverBase64 = null
let profileBase64 = null
if (cover_image) {
// Wrap the FileReader in a Promise to wait for it to complete
coverBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(cover_image)
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error('Failed to read image as base64'))
})
}
if (profile_image) {
// Wrap the FileReader in a Promise to wait for it to complete
profileBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(profile_image)
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error('Failed to read image as base64'))
})
}
if (!coverBase64 && !profileBase64) {
return
}
try {
app.preloader.show()
let promises = []
if (profileBase64) {
promises.push(updateProfileImage(profileBase64))
}
if (coverBase64) {
promises.push(updateCoverImage(coverBase64))
}
const responses = await Promise.all(promises)
app.preloader.hide()
if (responses.every(response => response && response.success)) {
showToast('Images updated successfully', 'Success')
view.router.navigate('/profile/')
store.dispatch('updateUserDetails')
return
}
if (responses.some(response => response && !response.success)) {
throw new Error('Failed to update images')
}
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update images',
}).open()
}
})
$(document).on('change', 'input[name="cover_image"]', function (e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function (event) {
console.log($('.custom-file-upload.cover'));
$('.custom-file-upload.cover')
.find('label')
.css('background-image', `url('${event.target.result}')`)
.css('background-size', 'cover');
};
if (file) {
reader.readAsDataURL(file);
}
});
$(document).on('change', 'input[name="profile_image"]', function (e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function (event) {
console.log($('.custom-file-upload.profile'));
$('.custom-file-upload.profile')
.find('label')
.css('background-image', `url('${event.target.result}')`)
.css('background-size', 'cover');
};
if (file) {
reader.readAsDataURL(file);
}
});
// --------------- End Profile images Page ---------------
// --------------- Edit Socials Page ---------------
$(document).on('page:init', '.page[data-name="profile-edit-socials"]', async function (e) {
var view = app.views.current
const user = await getSessionUser()
if (!user) {
view.router.navigate('/login')
return
}
const externalLinks = user.profile_links || {};
app.popup.create({
el: '.add-link-popup',
swipeToClose: 'to-bottom'
});
// Populate form fields
document.querySelector('input[name="social_instagram"]').value = externalLinks.instagram || '';
document.querySelector('input[name="social_facebook"]').value = externalLinks.facebook || '';
document.querySelector('input[name="social_tiktok"]').value = externalLinks.tiktok || '';
document.querySelector('input[name="social_youtube"]').value = externalLinks.youtube || '';
document.querySelector('input[name="social_mivia"]').value = externalLinks.mivia || '';
document.querySelector('input[name="social_custodian"]').value = externalLinks.custodian || '';
// .social-other-links ul
const externalLinksContainer = $('.social-other-links ul')[0];
externalLinks.external_links?.forEach(linkObj => {
const listItem = document.createElement('li');
listItem.innerHTML = `
<a class="item-link item-content" href="${linkObj.link.url}" data-link-id="${linkObj.id}">
<div class="item-inner">
<div class="item-title">
${linkObj.link.label}
</div>
<div class="item-after delete-external-link"><i class="icon f7-icons">xmark_circle</i></div>
</div>
</a>
`;
externalLinksContainer.appendChild(listItem);
});
})
// Add event listener for the Save button
$(document).on('click', '#add-link-btn', async function () {
const linkTitle = $('input[name="custom_link_title"]').val();
const linkUrl = $('input[name="custom_link_url"]').val();
// Validate the inputs
if (linkTitle === '') {
console.log('Please enter a link title.', $('input[name="custom_link_title"]'));
showToast('Please enter a link title.', 'Error');
return;
}
if (linkUrl === '') {
showToast('Please enter a link URL.', 'Error');
return;
}
// Simple URL validation (basic check)
// const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*\/?$/;
// if (!urlPattern.test(linkUrl)) {
// showToast('Please enter a valid URL.', 'Error');
// return;
// }
const urlPattern = /^(https?:\/\/|www\.)[\da-z\.-]+\.[a-z]{2,6}\/?$/;
if (!urlPattern.test(linkUrl)) {
showToast('Please enter a valid URL.', 'Error');
return;
}
// Mock API request (POST request)
const requestData = {
label: linkTitle,
url: linkUrl
};
app.popup.close()
app.preloader.show()
const response = await addUserProfileLinks({
type: 'external_links',
link: requestData
})
if (response && response.success) {
showToast('Link added successfully', 'Success');
// Add the new link to the list
const listItem = document.createElement('li');
listItem.innerHTML = `
<a class="item-link item-content" href="${linkUrl}" data-link-id="${response.id}">
<div class="item-inner">
<div class="item-title">
${linkTitle}
</div>
<div class="item-after"><i class="icon f7-icons delete-external-link">xmark_circle</i></div>
</div>
</a>
`;
const externalLinksContainer = $('.social-other-links ul')[0];
externalLinksContainer.appendChild(listItem);
store.dispatch('updateUserDetails')
}
app.preloader.hide()
});
// Save social links
$(document).on('click', '#save-profile-socials', async function () {
var view = app.views.current
const user = await getSessionUser()
let instagram = $('input[name="social_instagram"]').val();
let facebook = $('input[name="social_facebook"]').val();
let tiktok = $('input[name="social_tiktok"]').val();
let youtube = $('input[name="social_youtube"]').val();
let mivia = $('input[name="social_mivia"]').val();
let custodian = $('input[name="social_custodian"]').val();
const links = {
instagram,
facebook,
tiktok,
youtube,
mivia,
custodian
};
// Define the allowed username pattern (alphanumeric, _, -, .)
const usernamePattern = /^[a-zA-Z0-9._-]+$/;
// Define a function to strip '@' from the start of a username
function cleanUsername(username) {
return username.startsWith('@') ? username.substring(1) : username;
}
// Clean and validate Instagram username
if (instagram) {
instagram = cleanUsername(instagram);
links.instagram = instagram;
if (!usernamePattern.test(instagram)) {
showToast('Please enter a valid Instagram username (letters, numbers, underscores, periods, hyphens only)', 'Error');
return;
}
}
// Clean and validate TikTok username
if (tiktok) {
tiktok = cleanUsername(tiktok);
links.tiktok = tiktok;
if (!usernamePattern.test(tiktok)) {
showToast('Please enter a valid TikTok username (letters, numbers, underscores, periods, hyphens only)', 'Error');
return;
}
}
// Clean and validate YouTube username
if (youtube) {
youtube = cleanUsername(youtube);
links.youtube = youtube;
if (!usernamePattern.test(youtube)) {
showToast('Please enter a valid YouTube username (letters, numbers, underscores, periods, hyphens only)', 'Error');
return;
}
}
// Clean and validate Mivia username
if (mivia) {
mivia = cleanUsername(mivia);
links.mivia = mivia;
if (!usernamePattern.test(mivia)) {
showToast('Please enter a valid Mivia username (letters, numbers, underscores, periods, hyphens only)', 'Error');
return;
}
}
// Allow URL with "https://" for Custodian and validate it
const urlPattern = /^https:\/\/[\da-z\.-]+\.[a-z]{2,6}\/?$/;
// Allow any URL that starts with "https://"
if (custodian && !custodian.startsWith('https://')) {
showToast('Please enter a valid Custodian URL that starts with https://', 'Error');
return;
}
// Clean and validate Facebook username
if (facebook) {
facebook = cleanUsername(facebook);
links.facebook = facebook;
if (!usernamePattern.test(facebook)) {
showToast('Please enter a valid Facebook username (letters, numbers, underscores, periods, hyphens only)', 'Error');
return;
}
}
// check if the request data is the same as userdata
let dirtied = false;
for (const key in links) {
if (links[key] !== user.profile_links[key]) {
dirtied = true
break;
}
}
if (!dirtied) {
return
}
app.preloader.show()
const response = await updateSocialLinks(links);
app.preloader.hide()
if (response && response.success) {
showToast('Social links updated successfully', 'Success');
view.router.navigate('/profile/')
store.dispatch('updateUserDetails')
} else {
showToast('Failed to update social links', 'Error');
}
})
// Delete link
$(document).on('click', '.delete-external-link', async function (e) {
const linkId = e.target.closest('.item-link').dataset.linkId;
// confirm dialog
app.dialog.confirm('Are you sure you want to delete this link?', 'Delete Link', async function () {
app.preloader.show()
const response = await removeProfileLink(linkId);
app.preloader.hide()
if (response) {
showToast('Link deleted successfully', 'Success')
// remove the link from the list
e.target.closest('.item-link').remove();
store.dispatch('updateUserDetails')
} else {
showToast('Failed to delete link', 'Error')
}
})
})
// --------------- End Edit Socials Page ---------------
$(document).on('input', '#lowercaseInput', function (event) {
this.value = this.value.toLowerCase();
});
// --------------- Delete Profile Page ---------------
// $(document).on('page:init', '.page[data-name="profile-edit-account-settings"]', function (e) {
// });
$(document).on('click', '.account-delete', function (e) {
app.dialog.confirm('Are you sure? It is not possible to restore accounts once deleted.', 'Confirm', function (name) {
app.dialog.passwordConfirm('Please enter your password.', 'Delete Account', async function (password) {
await handleDeleteAccount(password);
});
});
});
async function handleDeleteAccount(password) {
try {
app.preloader.show();
const response = await deleteUserAccount(password);
app.preloader.hide();
if (!response) {
app.dialog.alert('Failed to delete account', 'Error');
return;
}
if (response && response.success == false) {
app.dialog.alert(response.message, 'Error');
return;
}
if (response && response.success) {
app.dialog.alert('Account deleted successfully', 'Success');
setTimeout(() => {
store.dispatch('logout');
}, 1000);
}
} catch (error) {
app.preloader.hide();
app.dialog.alert(error.message || 'Failed to delete account', 'Error');
}
}
// Custom dialog to ask for password
app.dialog.passwordConfirm = function (text, title, callback) {
app.dialog.create({
title: title,
text: text,
content: '<div class="dialog-input-field item-input"><div class="item-input-wrap"><input type="password" name="password" placeholder="Enter your password" class="dialog-input"></div></div>',
buttons: [{
text: 'Cancel',
onClick: function (dialog, e) {
dialog.close();
}
},
{
text: 'Delete',
bold: true,
onClick: function (dialog, e) {
var password = dialog.$el.find('.dialog-input').val(); // Get the password entered
if (!password) {
showToast('Please enter your password', 'Error');
return;
}
callback(password); // Pass the password to the callback
dialog.close();
}
}
]
}).open();
};
/js/profile.js
import store from "./store.js"
import app, {
showToast
} from "./app.js"
import {
addVehicleToGarage,
deleteVehicleFromGarage,
getGargeById,
updateVehicleInGarage
} from "./api/garage.js"
import {
getSessionUser
} from "./api/auth.js"
import {
sendRNMessage
} from "./api/consts.js"
var $ = Dom7
var garageStore = store.getters.myGarage
var myPostsStore = store.getters.myPosts
var myTagsStore = store.getters.myTags
var pathStore = store.getters.getPathData
var userStore = store.getters.user
var refreshed = false;
var isFetchingPosts = false
var totalPostPages = 1
var totalFPostPages = 1
var currentPostPage = 1
var currentFPostPage = 1
// Garage posts
var totalGaragePostPages = 1
var currentGaragePostPage = 1
// Garage tags
var totalGarageTagPages = 1
var currentGarageTagPage = 1
export function displayProfile(user, container = 'profile') {
if (!user) {
console.error('User object not provided');
return;
}
// Select the container element
const containerElem = document.querySelector(`.page[data-name="${container}"]`);
if (!containerElem) {
console.error(`Container element with data-name="${container}" not found.`);
return;
}
// Profile Head
const usernameElem = containerElem.querySelector('.profile-head .profile-username');
const nameElem = containerElem.querySelector('.profile-head .profile-name');
if (usernameElem) usernameElem.textContent = `@${user.username}`;
if (nameElem) nameElem.textContent = `${user.first_name} ${user.last_name}`;
// Profile Image
const profileImageElem = containerElem.querySelector('.profile-head .profile-image');
if (profileImageElem) {
profileImageElem.style.backgroundImage = `url('${user.profile_image || 'assets/img/profile-placeholder.jpg'}')`;
}
// Cover Image
if (user.cover_image) {
const profileBackgroundElem = containerElem.querySelector('.profile-background');
if (profileBackgroundElem) {
profileBackgroundElem.style.backgroundImage = `url('${user.cover_image}')`;
}
}
// Profile Links
const profileLinks = user.profile_links || {};
const setLinkHref = (selector, url) => {
const linkElem = containerElem.querySelector(selector);
if (linkElem) {
linkElem.setAttribute('href', url);
linkElem.onclick = (e) => {
e.preventDefault();
window.open(url, '_blank');
}
// Enable the link
linkElem.style.opacity = 1;
}
};
if (profileLinks.instagram) {
setLinkHref('#instagram', `https://www.instagram.com/${profileLinks.instagram}`);
} else {
// set opacity to 0.5
const instagramElem = containerElem.querySelector('#instagram');
if (instagramElem) {
instagramElem.style.opacity = 0.2;
// disable the link
instagramElem.onclick = (e) => e.preventDefault();
}
}
if (profileLinks.facebook) {
setLinkHref('#facebook', `https://www.facebook.com/${profileLinks.facebook}`);
} else {
// set opacity to 0.5
const facebookElem = containerElem.querySelector('#facebook');
if (facebookElem) {
facebookElem.style.opacity = 0.2;
// disable the link
facebookElem.onclick = (e) => e.preventDefault();
}
}
if (profileLinks.tiktok) {
setLinkHref('#tiktok', `https://www.tiktok.com/@${profileLinks.tiktok}`);
} else {
// set opacity to 0.5
const tiktokElem = containerElem.querySelector('#tiktok');
if (tiktokElem) {
tiktokElem.style.opacity = 0.2;
// disable the link
tiktokElem.onclick = (e) => e.preventDefault();
}
}
if (profileLinks.youtube) {
setLinkHref('#youtube', `https://www.youtube.com/@${profileLinks.youtube}`);
} else {
// set opacity to 0.5
const youtubeElem = containerElem.querySelector('#youtube');
if (youtubeElem) {
youtubeElem.style.opacity = 0.2;
// disable the link
youtubeElem.onclick = (e) => e.preventDefault();
}
}
// Display External Links
const externalLinks = profileLinks.external_links || []; // Assuming this is an array
const linksList = containerElem.querySelector('.profile-external-links ul');
if (linksList) {
linksList.innerHTML = ''; // Clear any existing links
if (externalLinks.length > 0) {
externalLinks.forEach(linkObj => {
const listItem = document.createElement('li');
const link = document.createElement('a');
link.href = linkObj.link.url;
link.target = '_blank';
link.textContent = linkObj.link.label;
listItem.appendChild(link);
linksList.appendChild(listItem);
});
} else {
// Optionally handle the case where there are no external links
const noLinksItem = document.createElement('li');
noLinksItem.textContent = 'No external links available';
linksList.appendChild(noLinksItem);
}
}
}
$(document).on('click', '.profile-external-links ul li a', function (e) {
e.preventDefault()
const url = new URL(e.target.href)
window.open(url, '_blank')
})
export function displayGarage(garage) {
if (!garage) return
const garageContainer = document.getElementById('profile-garage') // Make sure you have a container with this ID
garageContainer.innerHTML = createGarageContent(garage)
}
export function createGarageContent(garages, currentList, pastList) {
// Elements for current and past vehicles
const currentVehiclesList = document.querySelector(currentList)
const pastVehiclesList = document.querySelector(pastList)
if (!currentVehiclesList || !pastVehiclesList) {
console.log('Invalid elements provided for current and past vehicles');
return
}
currentVehiclesList.innerHTML = '' // Clear the list before adding new vehicles
pastVehiclesList.innerHTML = '' // Clear the list before adding new vehicles
if (garages.error) {
currentVehiclesList.innerHTML = '<li>No current vehicles</li>'
pastVehiclesList.innerHTML = '<li>No past vehicles</li>'
return
}
// Function to generate vehicle HTML
function generateVehicleHTML(vehicle) {
return `
<li>
<a href="/profile-garage-vehicle-view/${vehicle.id}" class="item">
<div class="imageWrapper">
<div class="image-square image-rounded"
style="background-image:url('${vehicle.cover_photo || 'assets/img/placeholder1.jpg'}');">
</div>
</div>
<div class="in">
<div>
${vehicle.make} ${vehicle.model}
</div>
</div>
</a>
</li>
`
}
// Sort vehicles into current and past vehicles
garages.forEach(vehicle => {
if (vehicle.owned_until === "" || vehicle.owned_until.toLowerCase() === "present") {
currentVehiclesList.innerHTML += generateVehicleHTML(vehicle)
} else {
pastVehiclesList.innerHTML += generateVehicleHTML(vehicle)
}
})
if (currentVehiclesList.innerHTML === '') {
currentVehiclesList.innerHTML = '<li>No current vehicles</li>'
}
if (pastVehiclesList.innerHTML === '') {
pastVehiclesList.innerHTML = '<li>No past vehicles</li>'
}
}
function generatePostGridItem(post) {
if (!post.media || post.media.length === 0) return ''
const media = post.media[0] // Get the first media item
const isVideo = media.media_type === "video" || media.media_url.includes('.mp4')
if (isVideo) {
return `
<a href="/post-view/${post.id}" class="grid-item" data-src="${media.media_url}/thumbnails/thumbnail.jpg">
<img
src="${media.media_url}/thumbnails/thumbnail.jpg"
loading="lazy"
role="presentation"
sizes="(max-width: 320px) 280px, 320px"
decoding="async"
fetchPriority="high"
style="object-fit: cover; max-width: 130px;"
/>
</a>`
} else {
return `
<a href="/post-view/${post.id}" class="grid-item image-square" data-src="${media.media_url}">
<img
src="${media.media_url}"
loading="lazy"
role="presentation"
sizes="(max-width: 320px) 280px, 320px"
decoding="async"
fetchPriority="high"
style="object-fit: cover; max-width: 130px;"
/>
</a>`
}
}
// Calculate the number of posts and decide if we need to add empty items
export function fillGridWithPosts(posts, profileGridID, reset = false) {
// Select the container where the posts will be displayed
const profileGrid = document.getElementById(profileGridID)
if (reset) {
profileGrid.innerHTML = '' // Clear the grid before adding new posts
}
posts.forEach(post => {
profileGrid.innerHTML += generatePostGridItem(post)
})
}
userStore.onUpdated((user) => {
displayProfile(user)
})
garageStore.onUpdated((garage) => {
// clear path data
store.dispatch('clearPathData')
createGarageContent(garage, '.current-vehicles-list', '.past-vehicles-list')
})
myPostsStore.onUpdated((data) => {
if (data && data.new_data) {
const posts = data.new_data
totalPostPages = data.total_pages
if ((data.page === data.total_pages) || (data.total_pages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.posts-tab').hide()
}
if (data.data.length === 0) {
const profileGrid = document.getElementById('profile-grid-posts')
profileGrid.innerHTML = '<p></p><p>No posts</p>'
return;
}
// Call the function to fill the grid
fillGridWithPosts(posts, 'profile-grid-posts', data.cleared || false)
}
})
myTagsStore.onUpdated((data) => {
if (data && data.new_data) {
const posts = data.new_data
totalFPostPages = data.total_pages
if ((data.page === data.total_pages) || (data.total_pages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.tags-tab').hide()
}
if (data.data.length === 0) {
const profileGrid = document.getElementById('profile-grid-tags')
profileGrid.innerHTML = '<p></p><p>No tagged posts</p>'
return;
}
// Call the function to fill the grid
fillGridWithPosts(posts, 'profile-grid-tags', data.cleared || false)
}
})
$(document).on('page:init', '.page[data-name="profile-garage-vehicle-add"]', function (e) {
app.calendar.create({
inputEl: '#owned-from',
openIn: 'customModal',
header: true,
footer: true,
dateFormat: 'dd/mm/yyyy',
maxDate: new Date()
})
app.calendar.create({
inputEl: '#owned-to',
openIn: 'customModal',
header: true,
footer: true,
dateFormat: 'dd/mm/yyyy',
// minDate: new Date()
})
})
$(document).on('page:init', '.page[data-name="profile-garage-vehicle-edit"]', function (e) {
app.calendar.create({
inputEl: '#owned-from',
openIn: 'customModal',
header: true,
footer: true,
dateFormat: 'dd/mm/yyyy',
maxDate: new Date()
})
app.calendar.create({
inputEl: '#owned-to',
openIn: 'customModal',
header: true,
footer: true,
dateFormat: 'dd/mm/yyyy',
// minDate: new Date()
})
})
$(document).on('infinite', '.profile-landing-page.infinite-scroll-content', async function (e) {
refreshed = false
if (isFetchingPosts) return
const activeTab = document.querySelector('.profile-tabs .tab-link-active')
const activeTabId = activeTab.id
if (!activeTabId || activeTabId === 'my-garage') return
const getterFunc = activeTabId === 'my-posts' ? 'getMyPosts' : 'getMyTags'
isFetchingPosts = true
if (activeTabId === 'my-posts') {
currentPostPage++
if (currentPostPage <= totalPostPages) {
await store.dispatch(getterFunc, {
page: currentPostPage,
clear: false
})
isFetchingPosts = false
}
} else {
currentFPostPage++
if (currentFPostPage <= totalFPostPages) {
await store.dispatch(getterFunc, {
page: currentPostPage,
clear: false
})
isFetchingPosts = false
}
}
})
$(document).on('ptr:refresh', '.profile-landing-page.ptr-content.my-profile', async function (e) {
refreshed = true
try {
store.dispatch('clearPathData')
await store.dispatch('updateUserDetails')
await store.dispatch('getMyGarage')
await store.dispatch('getMyPosts', {
page: 1,
clear: true
})
await store.dispatch('getMyTags', {
page: 1,
clear: true
})
} catch (error) {
console.log(error);
}
app.ptr.get('.profile-landing-page.ptr-content.my-profile').done()
})
$(document).on('page:beforein', '.page[data-name="profile"]', async function (e) {
const user = await getSessionUser()
if (user && user.id) {
const isEmailVerified = user.email_verified ?? false;
if (!isEmailVerified) {
const profileHead = $('.page[data-name="profile"] .profile-head')
if (profileHead.length) {
// Add email verification message before the element
$(`
<div class="email-verification-message">
<p>Your email is not verified. Please verify your email address to access all features.</p>
</div>
`).insertBefore(profileHead);
profileHead.addClass('email-not-verified');
} else {
console.log('Profile head element not found.');
}
}
}
app.popup.create({
el: '.links-popup',
swipeToClose: 'to-bottom'
})
})
// ------- Garage Views -------
$(document).on('page:init', '.page[data-name="profile-garage-vehicle-view"]', async function (e) {
var garageId = e.detail.route.params.id
if (!garageId) {
app.dialog.alert('Garage not found')
app.views.main.router.back()
return
}
if (garageId == -1) {
return;
}
let cachedData = null
try {
if (pathStore && pathStore.value[`/garage/${garageId}`]) {
cachedData = pathStore.value[`/garage/${garageId}`]
}
} catch (error) {
console.error('Error fetching cached data:', error)
}
if (cachedData) {
$('.loading-fullscreen.garage').hide()
store.dispatch('setGarageViewPosts', garageId, 1)
store.dispatch('setGarageViewTags', garageId, 1)
updateProfilePage(cachedData)
return
}
$('.loading-fullscreen.garage').show()
const garage = await getGargeById(garageId)
if (!garage) {
$('.loading-fullscreen').hide()
app.dialog.alert('Garage not found')
app.views.main.router.back()
return
}
$('.loading-fullscreen.garage').hide()
// Assuming `path` is a dynamic path like '/garage/2'
store.dispatch('setPathData', {
path: `/garage/${garageId}`,
data: garage,
})
// Call the function to update the page
updateProfilePage(garage)
store.dispatch('setGarageViewPosts', garageId, 1)
store.dispatch('setGarageViewTags', garageId, 1)
})
store.getters.getGarageViewPosts.onUpdated((data) => {
if (data && data.data) {
const posts = data.data
totalGaragePostPages = data.total_pages
// if there is only one page of posts or no posts
if ((data.page == data.total_pages) || (data.total_pages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.garage-posts-tab').hide()
}
if (data.data.length === 0) {
const profileGrid = document.getElementById('garage-posts-tab')
profileGrid.innerHTML = '<p>No posts yet</p>'
return;
}
// Call the function to fill the grid
fillGridWithPosts(posts, 'garage-posts-tab')
}
})
store.getters.getGarageViewTags.onUpdated((data) => {
if (data && data.data) {
const posts = data.data
totalGarageTagPages = data.total_pages
// if there is only one page of posts or no posts
if ((data.page == data.total_pages) || (data.total_pages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.garage-tags-tab').hide()
}
if (data.data.length === 0) {
const profileGrid = document.getElementById('garage-tags-tab')
profileGrid.innerHTML = '<p>No tagged posts yet</p>'
return;
}
// Call the function to fill the grid
fillGridWithPosts(posts, 'garage-tags-tab')
}
})
// Function to update the HTML with the data
async function updateProfilePage(data) {
const user = await getSessionUser()
// Update the cover photo
const coverPhotoElement = document.querySelector('.vehicle-profile-background')
if (coverPhotoElement) {
coverPhotoElement.style.backgroundImage = `url('${data.cover_photo}')`
}
// Update the profile image
const profileImageElement = document.querySelector('.vehicle-profile-image')
if (profileImageElement) {
profileImageElement.style.backgroundImage = `url('${data.owner.profile_image || 'assets/img/profile-placeholder.jpg'}')`
let profile_link = `/profile-view/${data.owner_id}`
if (user.id == data.owner_id) {
profile_link = '/profile/'
// add class view-profile
profileImageElement.classList.add('view-profile')
}
profileImageElement.setAttribute('href', profile_link)
}
// Update the vehicle make and model
const vehicleTitleElement = document.querySelector('.profile-garage-intro h1')
if (vehicleTitleElement) {
vehicleTitleElement.textContent = `${data.make} ${data.model}`
}
const profileLinks = $('.profile-links-edit.garage')
if (profileLinks) {
const editLink = `<a class="profile-link" href="/profile-garage-vehicle-edit/${data.id}">Edit Vehicle</a>`
const user = await getSessionUser()
if (data.owner_id == user.id) {
profileLinks.prepend(editLink)
}
// $('.garage-add-post').attr('data-garage-id', data.id)
$(document).on('click', '.garage-add-post', async function (e) {
return;
const garageId = $(this).attr('data-garage-id')
if (!garageId) {
app.dialog.alert('Garage not found')
return
}
const user = await getSessionUser()
if (user) {
sendRNMessage({
type: "createPost",
user_id: user.id,
page: 'profile-garage-post',
association_id: garageId,
association_type: 'garage',
})
}
})
}
// Update the ownership information
const ownershipInfoElement = document.querySelector('.garage-owned-information')
if (ownershipInfoElement) {
const ownedUntilText = data.owned_until ? ` - ${data.owned_until}` : ' - Present'
ownershipInfoElement.textContent = `Owned from ${data.owned_since}${ownedUntilText}`
}
// Update the vehicle description
const vehicleDescriptionElement = document.querySelector('.garage-vehicle-description')
if (vehicleDescriptionElement) {
vehicleDescriptionElement.textContent = data.short_description
}
}
$(document).on('page:init', '.page[data-name="profile-garage-edit"]', async function (e) {
const garage = garageStore.value
createGarageContent(garage, '#garage-edit-current-list', '#garage-edit-past-list')
})
$(document).on('page:init', '.page[data-name="profile-garage-vehicle-edit"]', async function (e) {
var garageId = e.detail.route.params.id
var view = app.views.current
if (!garageId) {
app.dialog.alert('Garage not found')
view.router.back(view.history[0], {
force: true
})
return
}
let data = null
try {
if (pathStore && pathStore.value[`/garage/${garageId}`]) {
data = pathStore.value[`/garage/${garageId}`]
}
} catch (error) {
console.error('Error fetching cached data:', error)
}
if (!data) {
$('.loading-fullscreen').show()
const garage = await getGargeById(garageId)
if (!garage) {
app.dialog.alert('Garage not found')
view.router.back(view.history[0], {
force: true
})
return
}
data = garage
// Assuming `path` is a dynamic path like '/garage/2'
store.dispatch('setPathData', {
path: `/garage/${garageId}`,
data: data,
})
}
$('.loading-fullscreen').hide()
// check if user is the owner of the garage
const user = await getSessionUser()
if (data.owner_id != user.id) {
app.dialog.alert('You are not authorized to edit this vehicle')
view.router.back(view.history[0], {
force: true
})
return
}
document.querySelector('input[name="garage_id"]').value = garageId
// Populate form fields with garage data
document.querySelector('select[name="vehicle_make"]').value = data.make
document.querySelector('input[name="vehicle_model"]').value = data.model
document.querySelector('input[name="vehicle_variant"]').value = data.variant
document.querySelector('input[name="vehicle_reg"]').value = data.registration
document.querySelector('input[name="vehicle_colour"]').value = data.colour
document.querySelector('input[name="vehicle_owned_from"]').value = data.owned_since
document.querySelector('input[name="vehicle_owned_to"]').value = data.owned_until || ''
document.querySelector('input[name="vehicle_tagging"]').checked = data.allow_tagging === "1"
document.querySelector('textarea[name="vehicle_description"]').value = data.short_description || ''
// If a cover photo exists, use it as the background image of the upload label
if (data.cover_photo) {
document.querySelector('.custom-file-upload label').style.backgroundImage = `url('${data.cover_photo}')`
document.querySelector('.custom-file-upload label').style.backgroundSize = 'cover'
}
// Set vehicle ownership and toggle the "Owned To" date picker
const ownershipSelect = document.querySelector('select[name="vehicle_ownership"]')
const toggleOwnedToDatePicker = () => {
const ownedToInput = document.querySelector('input[name="vehicle_owned_to"]')
const ownedToBContainer = document.querySelector('#owned-to-block')
if (ownershipSelect.value === "current") { // Current Vehicle
ownedToBContainer.style.display = 'none'
ownedToInput.value = ''
} else {
ownedToBContainer.style.display = 'block'
}
}
// Initially set the visibility based on the garage data
const isPrimary = data.primary_car === "1" ? true : false;
const hasOwndedTo = data.owned_until && data.owned_until.length > 1 ? true : false;
ownershipSelect.value = hasOwndedTo ? "past" : "current"
toggleOwnedToDatePicker()
// Attach event listener to toggle visibility when ownership type changes
ownershipSelect.addEventListener('change', toggleOwnedToDatePicker)
// input vehicle_image
$(document).on('change', 'input#fileuploadInput', function (e) {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = function (e) {
document.querySelector('.custom-file-upload label').style.backgroundImage = `url('${e.target.result}')`
document.querySelector('.custom-file-upload label').style.backgroundSize = 'cover'
}
reader.readAsDataURL(file)
})
// #delete-vehicle on click
$(document).on('click', '#delete-vehicle', async function (e) {
app.dialog.confirm('Are you sure you want to delete this vehicle?', async function () {
try {
app.preloader.show()
const response = await deleteVehicleFromGarage(garageId)
if (!response || !response.success) {
throw new Error('Failed to delete vehicle')
}
app.preloader.hide()
showToast('Vehicle deleted successfully')
await store.dispatch('getMyGarage')
view.router.back('/profile-garage-edit/', {
force: true
})
} catch (error) {
console.log(error);
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to delete vehicle',
}).open()
}
})
})
})
function parseDate(dateString) {
const parts = dateString.split('/');
return new Date(parts[2], parts[1] - 1, parts[0]); // YYYY, MM, DD
}
// submit-vehicle-form
$(document).on('click', '#submit-vehicle-form', async function (e) {
var view = app.views.current
// form data
const form = $('form#vehicleForm')
// values
const garageId = form.find('input[name="garage_id"]').val()
const make = form.find('select[name="vehicle_make"]').val()
const model = form.find('input[name="vehicle_model"]').val()
const variant = form.find('input[name="vehicle_variant"]').val()
const reg = form.find('input[name="vehicle_reg"]').val()
const colour = form.find('input[name="vehicle_colour"]').val()
const description = form.find('textarea[name="vehicle_description"]').val()
const owned_from = form.find('input[name="vehicle_owned_from"]').val()
const owned_to = form.find('input[name="vehicle_owned_to"]').val()
const primary_car = form.find('select[name="vehicle_ownership"]').val()
const allow_tagging = form.find('input[name="vehicle_tagging"]').is(':checked') ? 1 : 0
const cover_image = form.find('input[name="vehicle_image"]').prop('files')[0]
if (!make || make === "0") {
showToast('Please select a vehicle make')
return
}
if (!model) {
showToast('Please enter a vehicle model')
return
}
// if (!owned_from) {
// showToast('Please enter the date you owned the vehicle from')
// return
// }
// // if primary_car is past, owned_to is required
// if (primary_car === "past" && !owned_to) {
// showToast('Please enter the date you owned the vehicle to')
// return
// }
if (owned_to && owned_from) {
const ownedFromDate = parseDate(owned_from.trim());
const ownedToDate = parseDate(owned_to.trim());
if (isNaN(ownedFromDate) || isNaN(ownedToDate)) {
showToast('One or both of the dates are invalid.');
return;
}
if (ownedToDate < ownedFromDate) {
showToast('Owned to date cannot be less than owned from date');
return;
}
}
let base64 = null
if (cover_image) {
// Wrap the FileReader in a Promise to wait for it to complete
base64 = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(cover_image)
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error('Failed to read image as base64'))
})
}
try {
app.preloader.show()
const response = await updateVehicleInGarage({
make,
model,
variant,
registration: reg,
colour,
ownedFrom: owned_from,
ownedTo: owned_to,
primary_car,
allow_tagging,
cover_photo: base64,
vehicle_period: primary_car,
description
},
garageId
)
if (!response || !response.success) {
throw new Error('Failed to update vehicle')
}
app.preloader.hide()
showToast('Vehicle updated successfully')
// refresh garage
await store.dispatch('getMyGarage')
view.router.back(view.history[0], {
force: true
})
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to update vehicle',
}).open()
}
})
$(document).on('page:init', '.page[data-name="profile-garage-vehicle-add"]', async function (e) {
const toggleOwnedToDatePicker = (e) => {
const ownedToInput = document.querySelector('input[name="vehicle_owned_to"]')
const ownedToBContainer = document.querySelector('#owned-to-block')
const value = e.target.value
if (value === "current") { // Current Vehicle
ownedToBContainer.style.display = 'none'
ownedToInput.value = ''
} else {
ownedToBContainer.style.display = 'block'
}
}
$(document).on('change', 'select[name="vehicle_ownership"]', toggleOwnedToDatePicker)
// input vehicle_image
$(document).on('change', 'input[name="vehicle_image"]', function (e) {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = function (e) {
document.querySelector('.custom-file-upload label').style.backgroundImage = `url('${e.target.result}')`
document.querySelector('.custom-file-upload label').style.backgroundSize = 'cover'
}
reader.readAsDataURL(file)
})
})
$(document).on('click', '#submit-add-vehicle-form', async function (e) {
var view = app.views.current
const form = $('form#addVehicleForm')
// values
const make = form.find('select[name="vehicle_make"]').val()
const model = form.find('input[name="vehicle_model"]').val()
const variant = form.find('input[name="vehicle_variant"]').val()
const reg = form.find('input[name="vehicle_reg"]').val()
const colour = form.find('input[name="vehicle_colour"]').val()
const description = form.find('textarea[name="vehicle_description"]').val()
const owned_from = form.find('input[name="vehicle_owned_from"]').val()
const owned_to = form.find('input[name="vehicle_owned_to"]').val()
const primary_car = form.find('select[name="vehicle_ownership"]').val()
const allow_tagging = form.find('input[name="vehicle_tagging"]').is(':checked') ? 1 : 0
const cover_image = form.find('input[name="vehicle_image"]').prop('files')[0]
if (!make || make === "0") {
showToast('Please select a vehicle make')
return
}
if (!model) {
showToast('Please enter a vehicle model')
return
}
// if (!reg) {
// app.dialog.alert('Please enter a vehicle registration number')
// return
// }
// if (!owned_from) {
// app.dialog.alert('Please enter the date you owned the vehicle from')
// return
// }
if (owned_to && owned_from) {
const ownedFromDate = parseDate(owned_from.trim());
const ownedToDate = parseDate(owned_to.trim());
if (isNaN(ownedFromDate) || isNaN(ownedToDate)) {
showToast('One or both of the dates are invalid.');
return;
}
if (ownedToDate < ownedFromDate) {
showToast('Owned to date cannot be less than owned from date');
return;
}
}
// if primary_car is past, owned_to is required
if (primary_car === "past" && !owned_to) {
showToast('Please enter the date you owned the vehicle to');
return
}
if (!cover_image) {
showToast('Please upload a cover image')
return
}
let base64 = null
if (cover_image) {
// Wrap the FileReader in a Promise to wait for it to complete
base64 = await new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(cover_image)
reader.onload = () => resolve(reader.result)
reader.onerror = () => reject(new Error('Failed to read image as base64'))
})
}
try {
app.preloader.show()
const response = await addVehicleToGarage({
make,
model,
variant,
registration: reg,
colour,
ownedFrom: owned_from,
ownedTo: owned_to,
primary_car,
allow_tagging,
cover_photo: base64,
vehicle_period: primary_car,
description
})
if (!response || !response.success) {
throw new Error('Failed to update vehicle')
}
app.preloader.hide()
store.dispatch('getMyGarage')
app.dialog.alert('Vehicle added successfully')
// redirect to garage
view.router.back(`/profile-garage-vehicle-view/${response.id}`, {
force: true
})
} catch (error) {
app.preloader.hide()
app.notification.create({
titleRightText: 'now',
subtitle: 'Oops, something went wrong',
text: error.message || 'Failed to add vehicle',
}).open()
}
})
/js/qr-scanner.js
import {
verifyScan
} from "./api/scanner.js"
import store from "./store.js"
export const onScanSuccess = async (decodedText) => {
// https://mydrivelife.com/qr/clpmhHGEXyUD
// get the qr code and verify it
if (store.getters.isScanningQrCode.value) {
return
}
store.dispatch('setScanningQrCode', true)
try {
const url = new URL(decodedText)
if (url.hostname === 'mydrivelife.com') {
const qrCode = url.pathname.split('/').pop()
if (qrCode) {
const response = await verifyScan(qrCode)
store.dispatch('setScannedData', response)
store.dispatch('setScanningQrCode', false)
}
} else {
store.dispatch('setScannedData', {
status: 'error',
message: 'Oops, looks like you scanned an invalid QR code',
available: false
})
store.dispatch('setScanningQrCode', false)
}
} catch (error) {
console.log('error', error)
}
}
export function onScanFailure(error) {
// handle scan failure, usually better to ignore and keep scanning.
// for example:
// console.warn(`Code scan error = ${error}`)
}
/js/qr.js
import {
onScanFailure,
onScanSuccess
} from './qr-scanner.js'
import {
handleLink,
handleUnlink
} from './api/scanner.js'
import app from "./app.js"
import store from "./store.js"
var $ = Dom7;
var html5QrCode;
let defaultConfig = {
qrbox: {
width: 250,
height: 250
},
fps: 60,
showTorchButtonIfSupported: true,
showZoomSliderIfSupported: true,
// aspectRatio: 1.7777778
}
const renderResult = (result) => {
const user = store.getters.user.value
if (!result || result.status === 'error') {
return `<h2 class="text-center">${result?.message || 'Oops, looks like you scanned an invalid QR code'}</h2>`
}
if (result.available) {
return (
`<h2 class="text-center">Congrats! This QR code is up for grabs</h2>
<button id="link-profile">
Link Profile
</button>`
)
}
if (!result.available) {
var view = app.views.current
if (result.data && result.data.linked_to != user?.id) {
view.router.navigate(`/profile-view/${result.data.linked_to}`)
return;
}
return (
`
<h2 class="text-center">Sorry, this QR code is already linked</h2>
${result.data && result.data.linked_to == user?.id ? (
`<button id="unlink-profile"
onClick={handleUnlink}
>
Unlink Profile
</button>`
) : ' '}
`
)
}
}
// Function to create and open the modal with default content
export function openModal() {
const myModal = app.dialog.create({
title: 'Scan QR Code',
content: `
<div id="custom-modal-content">
<div id="reader" width="600px"></div>
</div>
`,
buttons: [{
text: 'Close',
onClick: function () {
try {
if (html5QrCode) {
html5QrCode.stop()
}
store.dispatch('setScannedData', null)
} catch (error) {
console.error('Error stopping qr code', error)
}
}
}]
})
// Open the modal
myModal.open()
}
// on link profile
$(document).on('click', '#link-profile', async function () {
const result = store.getters.scannedData.value
if (result) {
const response = await handleLink(result)
if (response.status === 'error') {
store.dispatch('setScannedData', {
status: 'error',
message: response.text,
available: false
})
}
app.dialog.close()
app.dialog.alert(response.message)
// reset the scanned data
store.dispatch('setScannedData', null)
}
})
// unlink profile
$(document).on('click', '#unlink-profile', async function () {
const result = store.getters.scannedData.value
// close the modal
app.dialog.close()
if (result) {
const response = await handleUnlink(result)
if (response.type === 'success') {
app.dialog.alert(response.text)
}
// reset the scanned data
store.dispatch('setScannedData', null)
}
})
export function openQRModal() {
openModal()
html5QrCode = new Html5Qrcode("reader")
html5QrCode?.start({
facingMode: "environment"
},
defaultConfig,
onScanSuccess,
onScanFailure
)
}
$(document).on('click', '.open-qr-modal', function () {
openQRModal()
})
store.getters.scannedData.onUpdated((data) => {
if (html5QrCode) {
html5QrCode.stop()
}
if (data) {
const html = renderResult(data);
if (!html) {
// close the modal
app.dialog.close()
return
}
document.getElementById('custom-modal-content').innerHTML = html
}
})
/js/routes.js
// var v = Date.now()
var v = '1.0.1'
var routes = [{
path: '/',
url: './index.html',
// name: 'home',
},
{
path: '/social/',
componentUrl: './pages/home.html?' + v,
keepAlive: true,
},
{
path: '/notifications/',
componentUrl: './pages/notifications.html?' + v,
keepAlive: true,
},
{
path: '/auth/',
url: './pages/auth.html',
// options: {
// animate: false,
// },
},
{
path: '/signin/',
url: './pages/login.html',
},
{
path: '/signup-step1/',
componentUrl: './pages/signup-step1.html?' + v,
},
{
path: '/signup-step2/',
componentUrl: './pages/signup-step2.html?' + v,
},
{
path: '/signup-step3/',
componentUrl: './pages/signup-step3.html?' + v,
},
{
path: '/signup-step4/',
componentUrl: './pages/signup-step4.html?' + v,
},
{
path: '/signup-complete/',
componentUrl: './pages/signup-complete.html?' + v,
},
{
path: '/login/',
componentUrl: './pages/login.html?' + v,
},
{
path: '/forgot-password/',
componentUrl: './pages/forgot-password.html?' + v,
},
{
path: '/discover/',
componentUrl: './pages/discover.html?' + v,
keepAlive: true,
},
{
path: '/discover-view-event/:id',
componentUrl: './pages/discover-view-event.html?' + v,
}, {
path: '/discover-view-venue/:id',
componentUrl: './pages/discover-view-venue.html?' + v,
},
{
path: '/store/',
componentUrl: './pages/store.html?' + v,
},
{
path: '/profile/',
componentUrl: './pages/profile.html?' + v,
keepAlive: true,
},
{
path: '/profile-view/:id',
componentUrl: './pages/profile-view.html?' + v,
},
{
path: '/profile-garage-vehicle-view/:id',
componentUrl: './pages/profile-garage-vehicle-view.html?' + v,
},
{
path: '/post-view/:id',
componentUrl: './pages/post-view.html?' + v,
},
{
path: '/profile-edit/',
componentUrl: './pages/profile-edit.html?' + v,
},
{
path: '/search/',
componentUrl: './pages/search.html?' + v,
},
{
path: '/profile-garage-edit/',
componentUrl: './pages/profile-garage-edit.html?' + v,
},
{
path: '/profile-garage-vehicle-add/',
componentUrl: './pages/profile-garage-vehicle-add.html?' + v,
},
{
path: '/profile-garage-vehicle-edit/:id',
componentUrl: './pages/profile-garage-vehicle-edit.html?' + v,
},
{
path: '/profile-edit-images/',
componentUrl: './pages/profile-edit-images.html?' + v,
},
{
path: '/profile-edit-socials/',
componentUrl: './pages/profile-edit-socials.html?' + v,
},
{
path: '/profile-edit-mydetails/',
componentUrl: './pages/profile-edit-mydetails.html?' + v,
},
{
path: '/profile-edit-username/',
componentUrl: './pages/profile-edit-username.html?' + v,
},
{
path: '/post-edit/:id',
componentUrl: './pages/post-edit.html?' + v,
},
{
path: '/profile-edit-account-settings/',
componentUrl: './pages/profile-edit-account-settings.html?' + v,
},
// Default route (404 page). MUST BE THE LAST
{
path: '(.*)',
componentUrl: './pages/404.html?' + v,
},
]
export default routes
/js/search.js
import {
getDiscoverData
} from "./api/discover.js";
import store from "./store.js";
var $ = Dom7;
let activeTab = 'all';
let lastSearchText = '';
let controller; // To store the current AbortController
var searchResultsStore = store.getters.getSearchResults;
$(document).on('page:afterin', '.page[data-name="search"]', async function (e) {
$('.loading-fullscreen.search-view').hide()
// Delay the focus slightly to ensure it's triggered on mobile
setTimeout(function () {
$('#discover-search').focus();
// Scroll to the input to make sure it's in view (this sometimes triggers the keyboard)
$('#discover-search')[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
// Re-trigger focus after scrolling
$('#discover-search').focus();
}, 300); // Adjust the delay if needed
})
// event listener for tab change
$(document).on('click', '.discovery-wrap .tab-link', async function (e) {
const type = this.getAttribute('data-type')
activeTab = type
})
function debounce(func, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
function addLoaderToTabs() {
const eventsContainer = document.querySelector('#events-results .list ul');
const usersContainer = document.querySelector('#users-results .list ul');
const vehiclesContainer = document.querySelector('#vehicles-results .list ul');
const venuesContainer = document.querySelector('#venues-results .list ul');
const topContainer = document.querySelector('#top-results .list');
const loader = `
<div class="loading-fullscreen search-view">
<div class="preloader preloader-central">
<span class="preloader-inner"><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span><span class="preloader-inner-line"></span><span
class="preloader-inner-line"></span>
</span>
</div>
</div>`;
eventsContainer.innerHTML = loader;
usersContainer.innerHTML = loader;
venuesContainer.innerHTML = loader;
topContainer.innerHTML = loader;
vehiclesContainer.innerHTML = loader;
$('.loading-fullscreen.search-view').show()
}
// Function to save searches to localStorage
function saveSearchToHistory(search) {
const maxHistoryLength = 5; // Limit the number of stored searches
let searchHistory = JSON.parse(localStorage.getItem('searchHistory')) || [];
// Check if search is already in the history
if (!searchHistory.includes(search)) {
// Add the new search to the beginning of the array
searchHistory.unshift(search);
// Keep only the last maxHistoryLength searches
if (searchHistory.length > maxHistoryLength) {
searchHistory = searchHistory.slice(0, maxHistoryLength);
}
// Save the updated history to localStorage
localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
}
}
// Function to display search history
function displaySearchHistory() {
const searchHistory = JSON.parse(localStorage.getItem('searchHistory')) || [];
const historyContainer = $('#search-history');
historyContainer.empty(); // Clear previous history
if (searchHistory.length > 0) {
// Populate the history container with recent searches
searchHistory.forEach(search => {
historyContainer.append(`
<li class="search-history-item" data-index="${searchHistory.indexOf(search)}">
<div>
<i class="icon f7-icons">timer</i>
<span>${search}</span>
</div>
<i class="icon f7-icons delete-history">xmark</i>
</li>
`);
});
// Show the history container
historyContainer.show();
} else {
// Hide the history container if there's no history
historyContainer.hide();
}
}
// Handle clicking on the delete history icon
$(document).on('click', '.delete-history', function (e) {
e.stopPropagation(); // Prevent the search from being triggered
let searchHistory = JSON.parse(localStorage.getItem('searchHistory')) || [];
const index = $(this).closest('li').data('index');
// Remove the search from the history
searchHistory.splice(index, 1);
localStorage.setItem('searchHistory', JSON.stringify(searchHistory));
// Update the search history display after deletion
displaySearchHistory();
});
// Hide search history when clicking outside
$(document).on('click', function (e) {
if (!$(e.target).closest('#discover-search').length && !$(e.target).closest('#search-history').length) {
$('#search-history').hide();
}
});
// Handle clicking on a history item to perform the search
$(document).on('click', '#search-history li', function () {
// Get the search text from the history item span
const search = $(this).find('span').text();
// clear the search input
$('#discover-search').val('');
$('#discover-search').val(search).trigger('input');
$('#search-history').hide(); // Hide the history after selecting
});
// Optionally, display history on input focus
// $(document).on('click', '#discover-search', displaySearchHistory);
const debouncedSearch = debounce(async function () {
const search = $(this).val().trim();
if (search.length < 3 || search === lastSearchText) {
return;
}
lastSearchText = search;
// Abort the previous request if it's still ongoing
if (controller) {
controller.abort();
}
// Create a new AbortController for the current request
controller = new AbortController();
const signal = controller.signal;
addLoaderToTabs();
// Save the valid search to history
saveSearchToHistory(search);
try {
const searchResults = await getDiscoverData(search, 'all', 1, signal);
store.dispatch('setSearchResults', searchResults);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Error fetching search results:', error);
}
}
}, 300); // Adjust the delay (in milliseconds) as needed
$(document).on('change input paste', '#discover-search', debouncedSearch);
// Function to render a list of items in the DOM
function renderList(container, items, renderItem) {
container.innerHTML = '';
const noResultsMessage = `
<li class="item-content w-full">
<strong class="text-center w-full">No results found</strong>
</li>`;
if (!items || items.length === 0) {
container.innerHTML = noResultsMessage;
return;
}
// Render each item
items.forEach(item => {
const li = document.createElement('li');
li.innerHTML = renderItem(item);
container.appendChild(li);
});
}
// Function to render the search results
function renderSearchResults(searchResults) {
const eventsContainer = document.querySelector('#events-results .list ul');
const usersContainer = document.querySelector('#users-results .list ul');
const vehiclesContainer = document.querySelector('#vehicles-results .list ul');
const venuesContainer = document.querySelector('#venues-results .list ul');
const topContainer = document.querySelector('#top-results .list');
// Ensure containers are cleared before rendering
eventsContainer.innerHTML = '';
usersContainer.innerHTML = '';
venuesContainer.innerHTML = '';
topContainer.innerHTML = '';
// Render events
if (searchResults.events) {
renderList(eventsContainer, searchResults.events.data, event => `
<a class="item-link search-result item-content" href="/discover-view-event/${event.id}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${event.thumbnail}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${event.name}</div>
</div>
</a>
`);
}
// Render users
if (searchResults.users) {
renderList(usersContainer, searchResults.users.data, (user) => {
const userLink = user.type == 'user' ? '/profile-view/' + user.id : '/profile-garage-vehicle-view/' + user.id;
let contentText;
if (user.type == 'user') {
contentText = `${user.name} (@${user.username})`;
}
if (user.type == 'vehicle') {
contentText = `${user.name}'s <b>${user.meta.make} ${user.meta.model}</b>`;
}
return `
<a class="item-link search-result item-content" href="${userLink}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${user.thumbnail || 'assets/img/profile-placeholder.jpg'}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${contentText}</div>
</div>
</a>`
});
}
if (searchResults.vehicles) {
renderList(vehiclesContainer, searchResults.vehicles.data, (user) => {
const userLink = user.type == 'post' ? '/post-view/' + user.id : '/profile-garage-vehicle-view/' + user.id;
let contentText;
if (user.type == 'post') {
contentText = `${user.username} tagged ${user.name}`;
}
if (user.type == 'vehicle') {
contentText = `${user.name}'s <b>${user.meta.make} ${user.meta.model}</b>`;
}
return `
<a class="item-link search-result item-content" href="${userLink}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${user.thumbnail || 'assets/img/profile-placeholder.jpg'}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${contentText}</div>
</div>
</a>`
});
}
// Render venues
if (searchResults.venues) {
renderList(venuesContainer, searchResults.venues.data, venue => `
<a class="item-link search-result item-content" href="/discover-view-venue/${venue.id}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${venue.thumbnail}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${venue.name} - ${venue.venue_location}</div>
</div>
</a>
`);
}
// Render top results
let hasTopResults = false;
if (searchResults.top_results) {
if (searchResults.top_results.events && searchResults.top_results.events.length > 0) {
hasTopResults = true;
const eventSubList = document.createElement('ul');
const heading = document.createElement('h2');
heading.innerHTML = 'Trending Events';
heading.classList.add('section-title', 'mt-3', 'mb-2');
topContainer.appendChild(heading);
topContainer.appendChild(eventSubList);
renderList(eventSubList, searchResults.top_results.events, event => `
<a class="item-link search-result item-content" href="/discover-view-event/${event.id}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${event.thumbnail}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${event.name}</div>
</div>
</a>
`);
}
if (searchResults.top_results.users && searchResults.top_results.users.length > 0) {
hasTopResults = true;
const userSubList = document.createElement('ul');
const heading = document.createElement('h2');
heading.innerHTML = 'Trending Users';
heading.classList.add('section-title', 'mt-3', 'mb-2');
topContainer.appendChild(heading);
topContainer.appendChild(userSubList);
renderList(userSubList, searchResults.top_results.users, (user) => {
const userLink = user.type == 'user' ? '/profile-view/' + user.id : '/profile-garage-vehicle-view/' + user.id;
let contentText;
if (user.type == 'user') {
contentText = `${user.name} (@${user.username})`;
}
if (user.type == 'vehicle') {
contentText = `${user.name}'s <b>${user.meta.make} ${user.meta.model}</b>`;
}
return `
<a class="item-link search-result item-content" href="${userLink}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${user.thumbnail || 'assets/img/profile-placeholder.jpg'}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${contentText}</div>
</div>
</a>`
});
}
if (searchResults.top_results.venues && searchResults.top_results.venues.length > 0) {
hasTopResults = true;
const venueSubList = document.createElement('ul');
const heading = document.createElement('h2');
heading.innerHTML = 'Trending Venues';
heading.classList.add('section-title', 'mt-3', 'mb-2');
topContainer.appendChild(heading);
topContainer.appendChild(venueSubList);
renderList(venueSubList, searchResults.top_results.venues, venue => `
<a class="item-link search-result item-content" href="/discover-view-venue/${venue.id}">
<div class="item-media">
<div class="image-square image-rounded" style="background-image:url('${venue.thumbnail}')"></div>
</div>
<div class="item-inner">
<div class="item-title">${venue.name} - ${venue.venue_location}</div>
</div>
</a>
`);
}
}
if (!hasTopResults) {
topContainer.innerHTML = `
<li class="item-content w-full">
<strong class="text-center w-full">No results found</strong>
</li>`;
}
}
searchResultsStore.onUpdated((results) => {
renderSearchResults(results);
});
/js/store.js
import {
getNotificationCount,
getSessionUser,
getUserDetails,
getUserNotifications,
markMultipleNotificationsAsRead
} from './api/auth.js'
import {
sendRNMessage
} from './api/consts.js'
import {
fetchTrendingEvents,
fetchTrendingUsers,
fetchTrendingVenues,
fetchEventCats
} from './api/discover.js'
import {
getPostsForGarage,
getUserGarage
} from './api/garage.js'
import {
fetchPosts,
getPostsForUser
} from './api/posts.js'
var createStore = Framework7.createStore
const DEFAULT_SEARCH_RESULTS = {
events: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
users: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
venues: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
top_results: {
events: [],
users: [],
venues: [],
},
success: false,
}
const DEFAULT_PAGINATED_DATA = {
data: [],
new_data: [],
total_pages: 0,
page: 1,
limit: 10,
}
const store = createStore({
state: {
user: null,
posts: {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
following_posts: {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
registerData: {
user_id: '',
email: '',
password: '',
username: '',
},
myGarage: [],
myPosts: {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
myTags: {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
garageViewPosts: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
garageViewTags: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
scannedData: null,
scanningQrCode: false,
paths: {}, // Object to store unique paths and their data
userPaths: {}, // Object to store unique user paths and their data
userPathsUpdated: false,
notifications: [],
// discover page
trendingEvents: [],
trendingVenues: [],
eventCategories: [],
filteredEvents: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
filteredVenues: {
data: [],
total_pages: 0,
page: 1,
limit: 10,
},
discoverSearchData: DEFAULT_SEARCH_RESULTS,
trendingUsers: DEFAULT_PAGINATED_DATA,
// Search results
searchResults: DEFAULT_SEARCH_RESULTS,
notificationCount: 0,
poorNetworkError: false,
trendingVehicles: DEFAULT_PAGINATED_DATA,
},
getters: {
getTrendingVehicles({
state
}) {
return state.trendingVehicles
},
checkPoorNetworkError({
state
}) {
return state.poorNetworkError
},
getNotifCount({
state
}) {
return state.notificationCount
},
getTrendingUsers({
state
}) {
return state.trendingUsers
},
getSearchResults({
state
}) {
return state.searchResults
},
getFilteredEvents({
state
}) {
return state.filteredEvents
},
getFilteredVenues({
state
}) {
return state.filteredVenues
},
getEventCategories({
state
}) {
return state.eventCategories
},
getTrendingEvents({
state
}) {
return state.trendingEvents
},
getTrendingVenues({
state
}) {
return state.trendingVenues
},
getPathData({
state
}) {
return state.paths
},
getUserPathUpdated({
state
}) {
return state.userPathsUpdated
},
getUserPathData({
state
}) {
return state.userPaths
},
getGarageViewPosts({
state
}) {
return state.garageViewPosts
},
getGarageViewTags({
state
}) {
return state.garageViewTags
},
user({
state
}) {
return state.user
},
getRegisterData({
state
}) {
return state.registerData
},
isAuthenticated({
state
}) {
return !!state.user
},
posts({
state
}) {
return state.posts
},
followingPosts({
state
}) {
return state.following_posts
},
myGarage({
state
}) {
return state.myGarage
},
myPosts({
state
}) {
return state.myPosts
},
myTags({
state
}) {
return state.myTags
},
scannedData({
state
}) {
return state.scannedData
},
isScanningQrCode({
state
}) {
return state.scanningQrCode
},
getNotifications({
state
}) {
return state.notifications
},
},
actions: {
updatePost({
state
}, {
post_id,
caption
}) {
// loop through the posts and find the post with the post_id
const posts = state.posts.data
const post = posts.find(p => p.id == post_id)
const myPosts = state.myPosts.data
const myPost = myPosts.find(p => p.id == post_id)
// update the post with the new data
if (post) {
// update the post with the new data
post.caption = caption
// update the state with the new posts
state.posts = {
...state.posts,
data: posts,
}
}
if (myPost) {
myPost.caption = caption
state.myPosts = {
...state.myPosts,
data: myPosts,
}
}
},
markNotificationsAsRead({
state
}, notification_ids) {
markMultipleNotificationsAsRead(notification_ids)
state.notificationCount = 0
},
async notificationCount({
state
}) {
const response = await getNotificationCount()
state.notificationCount = response.count
},
setSearchResults({
state
}, payload) {
state.searchResults = payload
},
async filterEvents({
state
}, {
filters,
page = 1
}) {
try {
const events = await fetchTrendingEvents(page, true, filters)
const data = {
new_data: events.data,
data: [
...state.filteredEvents.data,
...events.data,
],
total_pages: events.total_pages,
page: page,
limit: events.limit,
}
state.filteredEvents = data
} catch (error) {
console.error('Failed to filter events', error)
state.filteredEvents = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async filterTrendingUsers({
state
}, page = 1) {
try {
const response = await fetchTrendingUsers(page)
const data = {
new_data: response.data,
data: [
...state.trendingUsers.data,
...response.data,
],
total_pages: response.total_pages,
page: page,
limit: response.limit,
}
state.trendingUsers = data
} catch (error) {
console.log('Failed to fetch trending users', error);
state.trendingUsers = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async filterTrendingVehicles({
state
}, page = 1) {
try {
const response = await fetchTrendingUsers(page, true)
const data = {
new_data: response.data,
data: [
...state.trendingVehicles.data,
...response.data,
],
total_pages: response.total_pages,
page: page,
limit: response.limit,
}
state.trendingVehicles = data
} catch (error) {
console.log('Failed to fetch trending vehicles', error);
state.trendingVehicles = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async filterTrendingUsers({
state
}, page = 1) {
try {
const response = await fetchTrendingUsers(page)
const data = {
new_data: response.data,
data: [
...state.trendingUsers.data,
...response.data,
],
total_pages: response.total_pages,
page: page,
limit: response.limit,
}
state.trendingUsers = data
} catch (error) {
console.log('Failed to fetch trending users', error);
state.trendingUsers = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async filterVenues({
state
}, {
filters,
page = 1
}) {
try {
const events = await fetchTrendingVenues(page, true, filters)
let existingVenues = state.filteredVenues.data.length > 0 ? state.filteredVenues.data : state.trendingVenues.data
const data = {
new_data: events.data,
data: [
...existingVenues,
...events.data,
],
total_pages: events.total_pages,
page: page,
limit: events.limit,
}
state.filteredVenues = data
} catch (error) {
console.error('Failed to filter venues', error)
state.filteredVenues = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async fetchEventCategories({
state
}) {
const categories = await fetchEventCats()
state.eventCategories = categories
},
async getTrendingEvents({
state
}) {
const events = await fetchTrendingEvents(1, false)
state.trendingEvents = events
},
async getTrendingVenues({
state
}) {
const venues = await fetchTrendingVenues(1, false)
state.trendingVenues = venues
},
async fetchNotifications({
state
}, {
load_more = false
}) {
const notifications = await getUserNotifications(load_more)
state.notifications = notifications
},
async getUserPosts({
state
}, {
user_id,
page = 1,
clear = false
}) {
const posts = await getPostsForUser(user_id, page)
let prevUserPosts = {
data: []
}
if (!clear) {
if (state.userPaths[`user-${user_id}-posts`]) {
prevUserPosts = state.userPaths[`user-${user_id}-posts`]
}
}
const data = {
new_data: posts.data,
data: [
...prevUserPosts.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
cleared: clear,
}
state.userPaths[`user-${user_id}-posts`] = data
state.userPathsUpdated = true
},
async getUserTags({
state
}, {
user_id,
page = 1,
clear = false
}) {
const posts = await getPostsForUser(user_id, page, true)
let prevUserPosts = {
data: []
}
if (!clear) {
if (state.userPaths[`user-${user_id}-tags`]) {
prevUserPosts = state.userPaths[`user-${user_id}-tags`]
}
}
const data = {
new_data: posts.data,
data: [
...prevUserPosts.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
cleared: clear,
}
state.userPaths[`user-${user_id}-tags`] = data
state.userPathsUpdated = true
},
clearPathData({
state
}) {
state.paths = {}
},
setPathData({
state
}, {
path,
data
}) {
// Ensure the path key exists
if (!state.paths[path]) {
state.paths[path] = {}
}
// Update the data for the given path
state.paths[path] = {
...state.paths[path],
...data,
}
},
removePathData({
state
}, path) {
if (state.paths[path]) {
delete state.paths[path]
}
},
async login({
state
}, {
token
}) {
try {
const userDetails = await getUserDetails(token)
if (!userDetails || !userDetails.success) {
window.localStorage.removeItem('token')
throw new Error('User not found')
}
window.localStorage.setItem('token', token)
state.user = userDetails.user
setTimeout(() => {
sendRNMessage({
type: "authData",
user_id: userDetails.user.id,
page: 'auth',
})
}, 1000)
} catch (error) {
console.error('Login failed', error)
}
},
logout({
state
}) {
state.user = null
window.localStorage.removeItem('token')
window.location.reload()
window.ReactNativeWebView.postMessage(JSON.stringify({
type: "signOut",
user_id: null,
page: 'auth',
}))
},
async updateUserDetails({
state
}, external = false) {
const token = window.localStorage.getItem('token')
if (!token) {
return this.logout()
}
try {
const userDetails = await getUserDetails(token)
if (!userDetails || !userDetails.success) {
window.localStorage.removeItem('token')
throw new Error('User not found')
}
window.localStorage.setItem('token', token)
state.user = {
...userDetails.user,
refreshed: true,
external_refresh: external,
}
} catch (error) {
console.error('Login failed', error)
}
},
async checkAuth(context) {
const token = await getSessionUser()
if (token) {
await context.dispatch('login', {
token: token
})
} else {
window.localStorage.removeItem('token')
}
},
async getPosts({
state
}, {
page = 1,
reset = false
}) {
try {
console.log('Fetching posts', page, reset);
const posts = await fetchPosts(page)
if (reset) {
state.posts = {
new_data: posts.data,
data: posts.data,
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
reset: true,
}
return
}
const data = {
new_data: posts.data,
data: [
...state.posts.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.posts = data
} catch (error) {
console.error('Failed to fetch posts', error)
if (error.name === 'RequestTimeout') {
state.poorNetworkError = true
}
state.posts = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async setGarageViewPosts({
state
}, garage_id, page = 1) {
const posts = await getPostsForGarage(garage_id, page)
let prevData = state.garageViewPosts.data
if (page === 1) {
prevData = []
}
const data = {
data: [
...prevData,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.garageViewPosts = data
},
async setGarageViewTags({
state
}, garage_id, page = 1) {
const posts = await getPostsForGarage(garage_id, page, true)
let prevData = state.garageViewTags.data
if (page === 1) {
prevData = []
}
const data = {
data: [
...prevData,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.garageViewTags = data
},
async getFollowingPosts({
state
}, {
page = 1,
reset = false
}) {
try {
const posts = await fetchPosts(page, true)
if (reset) {
state.following_posts = {
new_data: posts.data,
data: posts.data,
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
reset: true,
}
return
}
const data = {
new_data: posts.data,
data: [
...state.following_posts.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.following_posts = data
} catch (error) {
console.error('Failed to fetch following posts', error)
if (error.name === 'RequestTimeout') {
state.poorNetworkError = true
}
state.following_posts = {
new_data: [],
data: [],
total_pages: 0,
page: 1,
limit: 10,
}
}
},
async setRegisterData({
state
}, {
email,
password,
username,
user_id
}) {
state.registerData = {
email: email,
password: password,
username: username,
user_id: user_id,
}
},
async getMyGarage({
state
}) {
const garage = await getUserGarage(state.user.id)
state.myGarage = garage
},
async getMyPosts({
state
}, {
page = 1,
clear = false
}) {
const posts = await getPostsForUser(state.user.id, page)
if (clear) {
state.myPosts = {
new_data: posts.data,
data: posts.data,
total_pages: posts.total_pages || 0,
page: page,
limit: posts.limit || 10,
cleared: true,
}
return
}
const data = {
new_data: posts.data,
data: [
...state.myPosts.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.myPosts = data
},
async getMyTags({
state
}, {
page = 1,
clear = false
}) {
const posts = await getPostsForUser(state.user.id, page, true)
if (clear) {
state.myTags = {
new_data: posts.data,
data: posts.data,
total_pages: posts.total_pages || 0,
page: page,
limit: posts.limit || 10,
cleared: true,
}
return
}
const data = {
new_data: posts.data,
data: [
...state.myTags.data,
...posts.data,
],
total_pages: posts.total_pages,
page: page,
limit: posts.limit,
}
state.myTags = data
},
setScannedData({
state
}, data) {
state.scannedData = data
},
setScanningQrCode({
state
}, value) {
state.scanningQrCode = value
},
},
})
export default store
/js/utils.js
export function formatPostDate(date) {
const postDate = new Date(date)
// show date as Just now, 1 minute ago, 1 hour ago, 1 day ago, 1 week ago, 1 month ago, 1 year ago
const currentDate = new Date()
const diff = currentDate - postDate
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const weeks = Math.floor(days / 7)
const months = Math.floor(days / 30)
if (months > 0) {
return months === 1 ? '1 month ago' : `${months} months ago`
}
if (weeks > 0) {
return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`
}
if (days > 0) {
return days === 1 ? '1 day ago' : `${days} days ago`
}
if (hours > 0) {
return hours === 1 ? '1 hour ago' : `${hours} hours ago`
}
if (minutes > 0) {
return minutes === 1 ? '1 minute ago' : `${minutes} minutes ago`
}
return 'Just now'
}
export const timedFetch = async (url, fetchObj, error = 'Your connection seems unstable.', timeout = TIMEOUT_MS_LOW) => {
const controller = new AbortController()
const signal = controller.signal
try {
const user = await getSessionUser()
if (!user) return
setTimeout(() => {
controller.abort()
}, TIMEOUT_MS_LOW)
const response = await fetch(url,
fetchObj,
signal
)
const data = await response.json()
return data
} catch (error) {
if (error.name === 'AbortError') {
throw {
message: error,
name: "TimeOutError"
};
} else {
throw error; // Rethrow any other errors
}
}
}
/js/venue-view.js
import {
fetchVenue,
maybeFollowVenue
} from "./api/discover.js";
import app from "./app.js";
var $ = Dom7;
//DISCOVER - VIEW EVENT
$(document).on('page:init', '.page[data-name="discover-view-venue"]', async function (e) {
var venueId = e.detail.route.params.id
if (!venueId || venueId === '-1') {
return;
}
$('.loading-fullscreen').show()
const venueData = await fetchVenue(venueId);
$('.loading-fullscreen').hide()
$('#copy-venue-link').attr('data-venue-id', venueId);
$('#share-email-venue-link').attr('data-venue-id', venueId);
const mainContainer = $('.discover-view-event');
if (!venueData) {
mainContainer.html('<div class="text-align-center">No event found</div>');
return;
}
// Populating the Event Title
mainContainer.find('.event-detail-title').text(venueData.title);
// Populating the Event Location
// create a map link with the address
const mapLink = `https://www.google.com/maps/search/?api=1&query=${venueData.location}`
// create an anchor tag with the map link
const mapLinkTag = `<a href="${mapLink}" target="_blank" class="event-location-map">${venueData.location}</a>`
mainContainer.find('.event-time-address span').html(mapLinkTag);
// Populating the Cover Image
mainContainer.find('.event-detail-img-box .swiper-slide .swiper-image')
.css('background-image', `url('${venueData.cover_photo.url}')`)
.attr('alt', venueData.cover_photo.alt);
// Populating the "About" Tab Content
mainContainer.find('#tab-about .event-des-wrap').html(`<p>${venueData.description}</p>`);
// Populating the "Follow" button state
const followButton = mainContainer.find('.event-list-btn .btn.bg-dark');
if (venueData.is_following) {
followButton.text('Following');
} else {
followButton.text('Follow');
}
// Populating the "Upcoming Events" Tab (if there are any events)
const eventsContainer = mainContainer.find('#tab-events .grid.event-listing');
eventsContainer.empty(); // Clear any placeholder content
if (venueData.events.length > 0) {
venueData.events.forEach(event => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
const startMonth = startDate.toLocaleString('default', {
month: 'short'
});
const startDay = startDate.getDate();
let endDateString = '';
if (startDate.getDate() !== endDate.getDate()) {
endDateString = `
<div class="event-date-item">
<p>${endDate.toLocaleString('default', { month: 'short' })}</p>
<h5>${endDate.getDate()}</h5>
</div>
`
}
const eventItem = `
<a href="/discover-view-event/${event.id}" class="card event-item">
<div class="event-image position-relative">
<div class="image-rectangle" style="background-image: url('${event.thumbnail}');"></div>
<div class="event-dates">
<div class="event-date-item">
<p>${startMonth}</p>
<h5>${startDay}</h5>
</div>
${endDateString}
</div>
</div>
<div class="card-content">
<h3 class="event-title">${event.title}</h3>
<p class="event-info">Starts ${event.start_date}</p>
<div class="event-info">${event.location}</div>
</div>
</a>
`;
eventsContainer.append(eventItem);
});
} else {
eventsContainer.html('<div class="text-align-center">No upcoming events</div>');
}
// follow button
const is_following = venueData.is_following
if (is_following) {
mainContainer.find('.venue-follow-btn').text('Following')
}
mainContainer.find('.venue-follow-btn').on('click', async function () {
const followButton = $(this);
const isFollowing = followButton.text() === 'Following';
// change the button text
followButton.text(isFollowing ? 'Follow' : 'Following');
const response = await maybeFollowVenue(venueId)
});
})
// event-location-map
$(document).on('click', '.event-time-address span a', function (e) {
e.preventDefault();
const mapLink = $(this).attr('href');
window.open(mapLink, '_blank');
});
$(document).on('click', '#copy-venue-link', function () {
const venueId = $(this).attr('data-venue-id');
const eventLink = `${window.location.origin}/discover-view-venue/${venueId}`;
navigator.clipboard.writeText(eventLink);
app.toast.create({
text: 'Link copied to clipboard',
closeTimeout: 2000
}).open()
});
// #share-email-venue-link click event
$(document).on('click', '#share-email-venue-link', function () {
const venueId = $(this).attr('data-venue-id');
const eventLink = `${window.location.origin}/discover-view-venue/${venueId}`;
window.open(`mailto:?subject=Check out this venue&body=${eventLink}`);
});
/js/view-post.js
import app from "./app.js"
import {
getPostById
} from "./api/posts.js"
import {
formatPostDate
} from "./utils.js"
import store from "./store.js"
import { detectDoubleTapClosure, loadVideos, togglePostLike } from "./homepage.js"
var $ = Dom7
var containerWidth = window.innerWidth
export function displayPost(post) {
const postsContainer = document.getElementById('post-view-container')
postsContainer.innerHTML = '' // Clear any existing posts
const user = store.getters.user.value
let post_actions = `
<div class="media-post-actions">
<div class="media-post-like" data-post-id="${post.id}">
<i class="icon f7-icons ${post.is_liked ? 'text-red' : ''}" data-post-id="${post.id}">${post.is_liked ? 'heart_fill' : 'heart'}</i>
</div>
<div class="media-post-comment popup-open" data-popup=".comments-popup" data-post-id="${post.id}">
<i class="icon f7-icons">chat_bubble</i>
</div>
<div class="media-post-share popup-open" data-popup=".share-popup">
<i class="icon f7-icons">paperplane</i>
</div>
`;
if (post.user_id == user.id) {
post_actions += `
<div class="media-post-edit popup-open" data-popup=".edit-post-popup" data-post-id="${post.id}">
<i class="icon f7-icons">gear_alt</i>
</div>
`;
}
post_actions += `</div>`;
const date = formatPostDate(post.post_date);
const maxDescriptionLength = 200; // Set your character limit here
const isLongDescription = post.caption.length > maxDescriptionLength;
const shortDescription = isLongDescription ? post.caption.slice(0, maxDescriptionLength) : post.caption;
let imageHeight = 400;
if (post.media.length > 0) {
const intrinsicWidth = post.media[0].media_width;
const intrinsicHeight = post.media[0].media_height;
const media_type = post.media[0].media_type;
// Calculate intrinsic aspect ratio
const intrinsicRatio = intrinsicWidth / intrinsicHeight;
// Calculate the rendered height based on the container width
const renderedHeight = containerWidth / intrinsicRatio;
// Use either the rendered height or the fallback height
if (renderedHeight > 0) {
if (renderedHeight > 500) {
imageHeight = 500
} else {
imageHeight = renderedHeight
}
if (media_type === 'video') {
imageHeight = renderedHeight
}
}
}
let profile_link;
if (post.user_id == user.id) {
profile_link = `
<a href="#" class="view-profile media-post-header">
<div class="media-post-avatar" style="background-image: url('${post.user_profile_image || 'assets/img/profile-placeholder.jpg'}');"></div>
<div class="media-post-user">${post.username}</div>
<div class="media-post-date">${date}</div>
</a>`
} else {
profile_link = `
<a href="/profile-view/${post.user_id}" class="media-post-header">
<div class="media-post-avatar" style="background-image: url('${post.user_profile_image || 'assets/img/profile-placeholder.jpg'}');"></div>
<div class="media-post-user">${post.username}</div>
<div class="media-post-date">${date}</div>
</a>`
}
const postItem = `
<div class="media-post single" data-post-id="${post.id}" data-is-liked="${post.is_liked}">
<div class="media-single-post-content">
${profile_link}
<div class="media-single-post-content">
<swiper-container pagination class="demo-swiper-multiple" space-between="50">
${post.media.map((mediaItem, index) => {
return `<swiper-slide class="swiper-slide post-media ${mediaItem.media_type === 'video' ? 'video' : ''}" style="height: ${imageHeight}px; ">
${mediaItem.media_type === 'video' ?
`<video
style="height: ${imageHeight}px;"
class="video-js"
data-src="${mediaItem.media_url}/manifest/video.m3u8"
preload="auto"
playsinline
loop
controls
autoplay
poster="${mediaItem.media_url}/thumbnails/thumbnail.jpg" <!-- Add the thumbnail as the poster image -->
></video>`
: `
<img
src="${mediaItem.media_url}"
alt="${mediaItem.caption || post.username + 's post'}"
style="text-align: center;"
onerror = "this.style.display='none';"
/>`}
</swiper-slide>
`}).join('')}
</swiper-container>
</div>
${post_actions}
<div class="media-post-likecount" data-like-count="${post.likes_count}">${post.likes_count} likes</div>
<div class="media-post-description">
<strong>${post.username}</strong> <br/> <span class="post-caption">${shortDescription}</span>
<span class="full-description hidden">${post.caption}</span>
${isLongDescription ? `<span class="media-post-readmore">... more</span>` : ''}
</div>
${post.comments_count > 0 ? `<div class="media-post-commentcount popup-open" data-popup=".comments-popup" data-post-id="${post.id}">View ${post.comments_count} comments</div>` : ''}
</div>
</div>
`;
postsContainer.insertAdjacentHTML('beforeend', postItem)
loadVideos()
}
$(document).on('touchstart', '.media-single-post-content .post-media', detectDoubleTapClosure((e) => {
const parent = e.closest('.media-post')
const postId = parent.getAttribute('data-post-id')
const isLiked = parent.getAttribute('data-is-liked') === 'true'
if (isLiked) {
return
}
togglePostLike(postId, true)
var pathStore = store.getters.getPathData
if (pathStore && pathStore.value[`/post/${postId}`]) {
var post = pathStore.value[`/post/${postId}`]
post.is_liked = true
post.likes_count += 1
store.dispatch('setPathData', {
path: `/post/${postId}`,
data: post,
})
}
}), {
passive: false
})
$(document).on('page:beforein', '.page[data-name="post-view"]', async function (e) {
var pathStore = store.getters.getPathData
var postId = e.detail.route.params.id
var query = e.detail.route.query
let commentId;
if (query && query.commentId) {
commentId = query.commentId
}
if (!postId || postId === '-1') {
return
}
let cachedData = null
try {
if (pathStore && pathStore.value[`/post/${postId}`]) {
cachedData = pathStore.value[`/post/${postId}`]
}
} catch (error) {
console.error('Error fetching cached data:', error)
}
if (!cachedData) {
$('.loading-fullscreen.post-view').show()
const post = await getPostById(postId)
if (!post) {
app.dialog.alert('Post not found', 'Error')
return
}
store.dispatch('setPathData', {
path: `/post/${postId}`,
data: post,
})
cachedData = post
} else {
$('.loading-fullscreen.post-view').hide()
}
displayPost(cachedData)
if (commentId) {
$('.media-post-comment').click()
}
setTimeout(() => {
// find .comment data-comment-id="${comment.id}" and animate it to glow#
if (commentId) {
const comment = $(`.comment[data-comment-id="${commentId}"]`)
console.log('Comment:', comment);
if (comment.length > 0) {
comment.addClass('target-highlight')
// Scroll to the comment
document.querySelector(`.comment[data-comment-id="${commentId}"]`).scrollIntoView({
behavior: 'smooth', // Optional, adds smooth scrolling
block: 'start', // Aligns the element to the top of the view
inline: 'nearest' // Aligns the element horizontally in the viewport
});
}
setTimeout(() => {
comment.removeClass('target-highlight')
}, 3000)
}
}, 2000)
})
/js/view-user-profile.js
import {
getSessionUser,
getUserById
} from "./api/auth.js"
import {
getUserGarage
} from "./api/garage.js"
import {
maybeFollowUser
} from "./api/profile.js"
import app from "./app.js"
import {
createGarageContent,
displayProfile,
fillGridWithPosts
} from "./profile.js"
import store from "./store.js"
var $ = Dom7
var totalPostPages = 1
var totalFPostPages = 1
var currentPostPage = 1
var currentFPostPage = 1
var isFetchingPosts = false
var refreshed = false;
var userId = null
$(document).on('page:init', '.page[data-name="profile-view"]', async function (e) {
userId = e.detail.route.params.id
currentPostPage = 1
currentFPostPage = 1
isFetchingPosts = false
refreshed = false
store.dispatch('getUserPosts', {
user_id: userId,
clear: true
})
store.dispatch('getUserTags', {
user_id: userId,
clear: true
})
})
$(document).on('page:beforein', '.page[data-name="profile-view"]', async function (e) {
$('.loading-fullscreen').show()
var pathStore = store.getters.getPathData
userId = e.detail.route.params.id
const sessionUser = await getSessionUser()
if (!sessionUser || !sessionUser.id) {
return;
}
// Follow button
const followButton = $('.user-follow-btn')
const sessionFollowings = sessionUser.following;
if (sessionFollowings.includes(`${userId}`)) {
followButton.text('Following')
} else {
followButton.text('Follow')
}
followButton.attr('data-user-id', userId)
let cachedData = null
try {
if (pathStore && pathStore.value[`/user/${userId}`]) {
cachedData = pathStore.value[`/user/${userId}`]
}
} catch (error) {
console.error('Error fetching cached data:', error)
}
await renderProfileData(cachedData, userId)
})
$(document).on('click', '.user-follow-btn', async function () {
const followButton = $(this)
const isFollowing = followButton.text() === 'Following'
// change the button text
followButton.text(isFollowing ? 'Follow' : 'Following')
const response = await maybeFollowUser(followButton.attr('data-user-id'))
if (response && response.success) {
store.dispatch('updateUserDetails', {
external: true
})
}
})
async function renderProfileData(cachedData, userId) {
// if (!refreshed && !cachedData) {
// }
refreshed = false
if (!cachedData) {
const data = await getUserById(userId)
console.log('User data:', data);
if (!data || data.error) {
$('.loading-fullscreen').hide()
app.dialog.alert('User not found', 'Error')
view.router.back(view.history[0], {
force: true
})
return
}
const garage = await getUserGarage(userId)
if (garage) {
createGarageContent(garage, '.pview-current-vehicles-list', '.pview-past-vehicles-list')
}
// Assuming `path` is a dynamic path like '/garage/2'
store.dispatch('setPathData', {
path: `/user/${userId}`,
data: {
user: data.user,
garage: garage,
},
})
displayProfile(data.user, 'profile-view')
} else {
displayProfile(cachedData.user, 'profile-view')
if (cachedData.garage) {
createGarageContent(cachedData.garage, '.pview-current-vehicles-list', '.pview-past-vehicles-list')
}
}
$('.loading-fullscreen').hide()
}
function populateUsersPosts(data) {
if (data) {
const postsKey = `user-${userId}-posts`
const tagsKey = `user-${userId}-tags`
// Handle posts
if (data[postsKey]) {
totalPostPages = data[postsKey].total_pages || 0
let reset = data[postsKey].cleared || false
// Only update the DOM if there are new posts
if (data[postsKey].new_data && data[postsKey].new_data.length > 0) {
fillGridWithPosts(data[postsKey].new_data, 'profile-view-grid-posts', reset)
// Clear new_data after processing to avoid re-rendering
data[postsKey].new_data = []
}
if ((data[postsKey].page === totalPostPages) || (totalPostPages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.posts-tab.view-profile').hide()
}
if (data[postsKey].data.length === 0) {
const profileGrid = document.getElementById('profile-view-grid-posts')
profileGrid.innerHTML = '<p></p><p>No posts</p>'
return;
}
}
// Handle tags
if (data[tagsKey]) {
totalFPostPages = data[tagsKey].total_pages || 0
let reset = data[tagsKey].cleared || false
// Only update the DOM if there are new tags
if (data[tagsKey].new_data && data[tagsKey].new_data.length > 0) {
fillGridWithPosts(data[tagsKey].new_data, 'profile-view-grid-tags', reset)
// Clear new_data after processing to avoid re-rendering
data[tagsKey].new_data = []
}
if ((data[tagsKey].page === totalFPostPages) || (totalFPostPages == 0)) {
// hide preloader
$('.infinite-scroll-preloader.tags-tab.view-profile').hide()
}
if (data[tagsKey].data.length === 0) {
const profileGrid = document.getElementById('profile-view-grid-tags')
profileGrid.innerHTML = '<p></p><p>No tagged posts</p>'
return;
}
}
}
}
store.getters.getUserPathUpdated.onUpdated(() => {
const data = store.getters.getUserPathData.value
populateUsersPosts(data)
})
$(document).on('infinite', '.profile-landing-page.infinite-scroll-content.view-page', async function (e) {
if (isFetchingPosts) return
const activeTab = document.querySelector('.profile-tabs .tab-link-active')
const activeTabId = activeTab.id
if (!activeTabId || activeTabId === 'my-garage') return
const getterFunc = activeTabId === 'my-posts' ? 'getUserPosts' : 'getUserTags'
isFetchingPosts = true
if (activeTabId === 'my-posts') {
currentPostPage++
if (currentPostPage <= totalPostPages) {
await store.dispatch(getterFunc, {
user_id: userId,
page: currentPostPage
})
isFetchingPosts = false
}
} else {
currentFPostPage++
if (currentFPostPage <= totalFPostPages) {
await store.dispatch(getterFunc, {
user_id: userId,
page: currentFPostPage
})
isFetchingPosts = false
}
}
})
$(document).on('ptr:refresh', '.profile-landing-page.view-page.ptr-content', async function (e) {
try {
currentPostPage = 1
currentFPostPage = 1
store.dispatch('removePathData', `/user/${userId}`)
await renderProfileData(null, userId)
store.dispatch('getUserPosts', {
user_id: userId,
clear: true
})
store.dispatch('getUserTags', {
user_id: userId,
clear: true
})
refreshed = true
} catch (error) {
console.log('Error refreshing profile page:', error);
}
app.ptr.get('.profile-landing-page.view-page.ptr-content').done()
})