Introducing: Swipe Cards Collection (AI Component + JSON)

A new way to show data :new_button:

Hello Fellow Gliders :playground_slide:

I’m excited to share a versatile Swipe Cards Collection I created using an AI Component and JSON. Though it might remind you of Tinder, its applications are extensive. This post will show you what it does and guide you through the implementation.

First, here is the baby

1000014763

Fine-tuning

You can change anything you want in it by asking the AI to make so changes. Remember that the AI in the Glide AI Component doesn’t remember previous message. It only know the current message context and the code already in place. So make sure to do good prompts.

JSON Configuration

Create the needed columns in your desired table. By using my default AI Component, here is the columns configuration and the JSON Object.

Then, you need to create a joined list and a template to make a JSON Array of all the rows. You can use query column before joined list if you need to filter the data.


Likes Counter

You can now create, if needed, the likes counter.
Create a likes-dislikes table.


Then, create the relation.

Context

Add a user-specific text column that will store current Card JSON object in it to be able to execute action in the right row context from the AI Component.


AI Component

You first need to create the variables and actions:



  • For the user id, you can use row id of the users table. For the example, I forgot to create the row id, so I used email instead.

Then paste:

Do exactly this : <html class="color-scheme-light"><head>
<style>:root {--color-accent: 215deg 99% 28%;

      }</style>
    <link rel="preload" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap" as="style">
    
    <meta name="color-scheme" content="light dark">
    <style>
      html, body {
        font-family: Inter,-apple-system,BlinkMacSystemFont,Roboto,sans-serif;
        background-color: transparent;
      }
    </style>
    <script type="module">
      import { init } from "https://go.glideapps.com/shell/v1.js";
      init({"fieldsDataObject":{"currentCard":"{\"Name\":\"Item Title\",\"Description\":\"A short description of the item\",\"Category\":\"Category 1\",\"Image\":\"https://images.pexels.com/photos/158827/field-corn-air-frisch-158827.jpeg\",\"ID\":\"a-k3jPSWlSAKokMunvj5TDg\",\"Likes\":18}"},"schema":[{"id":"jsonArray","type":"string","name":"jsonArray"},{"id":"currentCard","type":"string","name":"currentCard"}],"actions":[{"title":"swipedLeft","id":"swipedLeft"},{"title":"swipedRight","id":"swipedRight"}],"config":{"colorScheme":"light","cssVariables":{"--color-accent":"215deg 99% 28%"},"alpineConfig":{"mode":"standalone","hasBodyAction":false}},"debug":false})
    </script>
    <script src="//cdn.jsdelivr.net/npm/@iframe-resizer/child@5.2.1"></script>
    <style>/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{border-color:hsl(var(--color-gray-200)/1);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:hsl(var(--color-gray-400)/1);opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.headline-lg,.headline-md,.headline-sm,.headline-xl,.headline-xs{font-weight:600}.headline-sm{font-size:20px;letter-spacing:-.025em;line-height:1.375}@media (min-width:768px){.headline-sm{font-size:1.25rem;line-height:1.75rem}}.body-base{font-size:1rem;line-height:1.5rem;line-height:1.5}@media (min-width:768px){.body-base{font-size:.875rem;line-height:1.25rem}}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-x-0{left:0;right:0}.bottom-0{bottom:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2}.flex{display:flex}.h-14{height:3.5rem}.h-\[600px\]{height:600px}.h-full{height:100%}.w-14{width:3.5rem}.w-full{width:100%}.max-w-sm{max-width:24rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.select-none{user-select:none}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.overflow-hidden{overflow:hidden}.rounded-full{border-radius:9999px}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-gray-200{--tw-border-opacity:1;border-color:hsl(var(--color-gray-200)/var(--tw-border-opacity))}.bg-accent{--tw-bg-opacity:1;background-color:hsl(var(--color-accent)/var(--tw-bg-opacity))}.bg-background{--tw-bg-opacity:1;background-color:hsl(var(--color-background-DEFAULT)/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-10{--tw-bg-opacity:0.1}.bg-opacity-20{--tw-bg-opacity:0.2}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-black{--tw-gradient-from:#000 var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-cover{object-fit:cover}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.text-accent{--tw-text-opacity:1;color:hsl(var(--color-accent)/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-opacity-80{--tw-text-opacity:0.8}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\@container{container-type:inline-size}@layer colors{:root.color-scheme-auto{color-scheme:light;--color-gray-0:0deg 0% 100%;--color-gray-50:0deg 0% 98%;--color-gray-100:0deg 0% 96%;--color-gray-200:0deg 0% 94%;--color-gray-300:0deg 0% 88%;--color-gray-400:0deg 0% 68%;--color-gray-500:0deg 0% 56%;--color-gray-600:0deg 0% 44%;--color-gray-700:0deg 0% 31%;--color-gray-800:0deg 0% 20%;--color-gray-900:0deg 0% 5%;--color-gray-950:0deg 0% 0%;--color-background-DEFAULT:0deg 0% 100%}@media (prefers-color-scheme:dark){:root.color-scheme-auto{color-scheme:dark;--color-gray-0:0deg 0% 4%;--color-gray-50:0deg 0% 9%;--color-gray-100:0deg 0% 12%;--color-gray-200:0deg 0% 15%;--color-gray-300:0deg 0% 20%;--color-gray-400:0deg 0% 31%;--color-gray-500:0deg 0% 44%;--color-gray-600:0deg 0% 57%;--color-gray-700:0deg 0% 72%;--color-gray-800:0deg 0% 90%;--color-gray-900:0deg 0% 100%;--color-gray-950:0deg 0% 100%;--color-background-DEFAULT:0deg 0% 9%}}:root.color-scheme-light{color-scheme:light;--color-gray-0:0deg 0% 100%;--color-gray-50:0deg 0% 98%;--color-gray-100:0deg 0% 96%;--color-gray-200:0deg 0% 94%;--color-gray-300:0deg 0% 88%;--color-gray-400:0deg 0% 68%;--color-gray-500:0deg 0% 56%;--color-gray-600:0deg 0% 44%;--color-gray-700:0deg 0% 31%;--color-gray-800:0deg 0% 20%;--color-gray-900:0deg 0% 5%;--color-gray-950:0deg 0% 0%;--color-background-DEFAULT:0deg 0% 100%}:root.color-scheme-dark{color-scheme:dark;--color-gray-0:0deg 0% 4%;--color-gray-50:0deg 0% 9%;--color-gray-100:0deg 0% 12%;--color-gray-200:0deg 0% 15%;--color-gray-300:0deg 0% 20%;--color-gray-400:0deg 0% 31%;--color-gray-500:0deg 0% 44%;--color-gray-600:0deg 0% 57%;--color-gray-700:0deg 0% 72%;--color-gray-800:0deg 0% 90%;--color-gray-900:0deg 0% 100%;--color-gray-950:0deg 0% 100%;--color-background-DEFAULT:0deg 0% 9%}}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}@media (min-width:640px){.sm\:px-5{padding-left:1.25rem;padding-right:1.25rem}}@media (min-width:768px){.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:px-7{padding-left:1.75rem;padding-right:1.75rem}}@media (min-width:1280px){.xl\:px-8{padding-left:2rem;padding-right:2rem}}</style>
  </head>
  <body x-data="state" class="" :class="{ 'cursor-pointer': hasBodyAction }">
    <div class="@container w-full body-base antialiased">
      <div :class="{ 'px-4 py-1 sm:px-5 md:px-6 lg:px-7 xl:px-8': mode === 'standalone' }">
        <div x-data="swipeCards" class="relative w-full max-w-sm mx-auto h-[600px] overflow-hidden select-none">
          <template x-for="(item, index) in JSON.parse(jsonArray)" :key="index">
              <div class="absolute inset-0 bg-background rounded-xl border border-gray-200 transition-all duration-300 ease-out overflow-hidden" :style="{ 
                      opacity: index === currentIndex ? 1 : 0, 
                      zIndex: JSON.parse(jsonArray).length - Math.abs(index - currentIndex),
                      transform: getCardTransform(index),
                      transition: 'transform 0.3s ease-out, opacity 0.3s ease-out'
                   }" @mousedown="startDrag($event, index)" @touchstart="startDrag($event, index)" @mousemove="drag($event, index)" @touchmove="drag($event, index)" @mouseup="endDrag($event, index)" @touchend="endDrag($event, index)" @mouseleave="endDrag($event, index)">
                  <img :src="item.Image" :alt="item.Name" class="absolute inset-0 w-full h-full object-cover pointer-events-none">
                  <div class="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black to-transparent p-4">
                      <div class="flex items-center justify-between mb-2">
                          <h3 class="headline-sm text-white" x-text="item.Name"></h3>
                          <span class="px-2 py-1 text-xs font-medium text-accent bg-accent bg-opacity-10 rounded-full" x-text="item.Category"></span>
                      </div>
                      <p class="text-white text-opacity-80 line-clamp-2 mb-8" x-text="item.Description"></p>
                      <div class="flex justify-between items-center">
                          <button @click="swipe('left', index)" class="w-14 h-14 flex items-center justify-center bg-white bg-opacity-20 backdrop-blur-sm rounded-full text-red-500 text-2xl transition-colors hover:bg-red-50">
                              ✕
                          </button>
                          <button @click="swipe('right', index)" class="w-14 h-14 flex items-center justify-center bg-accent bg-opacity-20 backdrop-blur-sm rounded-full text-white transition-colors hover:bg-accent-dark">
                              <span class="text-lg">❤</span>
                              <span class="text-sm ml-1" x-text="item.Likes"></span>
                          </button>
                      </div>
                  </div>
              </div>
          </template>
      </div>
      
      <script>
          document.addEventListener('alpine:init', () => {
              Alpine.data('swipeCards', () => ({
                  startX: 0,
                  currentX: 0,
                  isDragging: false,
                  threshold: 100,
                  currentIndex: 0,
                  cardOffsets: {},
      
                  init() {
                      this.$watch('jsonArray', () => this.updateCurrentCard());
                      this.$watch('currentIndex', () => this.updateCurrentCard());
                      this.$nextTick(() => setTimeout(() => this.updateCurrentCard(), 250));
                  },
      
                  updateCurrentCard() {
                      const cards = JSON.parse(this.jsonArray || '[]');
                      if (cards.length > 0 && this.currentIndex < cards.length) {
                          this.currentCard = JSON.stringify(cards[this.currentIndex]);
                      }
                  },
      
                  getCardTransform(index) {
                      const offset = this.cardOffsets[index] || 0;
                      const scale = index === this.currentIndex ? 1 : 0.8;
                      return `translateX(${offset}px) rotate(${offset * 0.1}deg) scale(${scale})`;
                  },
      
                  startDrag(e, index) {
                      if (e.target.closest('button')) return;
                      this.isDragging = true;
                      this.startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
                      this.currentX = this.startX;
                      this.draggedIndex = index;
                  },
      
                  drag(e, index) {
                      if (!this.isDragging || this.draggedIndex !== index) return;
                      this.currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
                      const deltaX = this.currentX - this.startX;
                      this.cardOffsets[index] = deltaX;
                  },
      
                  endDrag(e, index) {
                      if (!this.isDragging || this.draggedIndex !== index) return;
                      this.isDragging = false;
                      const deltaX = this.currentX - this.startX;
                      const direction = deltaX > 0 ? 'right' : 'left';
                      if (Math.abs(deltaX) > this.threshold) {
                          this.swipe(direction, index);
                      } else {
                          this.cardOffsets[index] = 0;
                      }
                      this.draggedIndex = null;
                  },
      
                  swipe(direction, index) {
                      const swipeAnimation = direction === 'left' ? -150 : 150;
                      this.cardOffsets[index] = swipeAnimation;
                      
                      setTimeout(() => {
                          this.currentIndex = (this.currentIndex + 1) % JSON.parse(this.jsonArray).length;
                          this.updateCurrentCard();
                          this.cardOffsets[index] = 0;
                          if (direction === 'left') {
                              this.swipedLeft();
                          } else {
                              this.swipedRight();
                          }
                      }, 500);
                  },
      
                  swipedLeft() {
                      // Call the provided action
                      if (typeof swipedLeft === 'function') {
                          swipedLeft();
                      }
                  },
      
                  swipedRight() {
                      // Call the provided action
                      if (typeof swipedRight === 'function') {
                          swipedRight();
                      }
                  }
              }));
          });
      </script>
      </div>
    </div>
  
</body></html>

Happy Gliding! :high_voltage::fire:

Hope you like it! Feel free to comment or ask any questions. :slight_smile:

4 Likes

hehe, you’re still doing this :slight_smile:
Have you actually tried using a Lookup like I suggested last time?
Save yourself a column, and the bonus is that the Lookup is “JSON aware”, where a Joined list is not.

3 Likes

Gold :star_struck:. Thank you for sharing

1 Like

Thanks for sharing @MaximeBaker

1 Like

It’s a nice collection style. We could try to think of good use cases for it in a business setting.

I love it how Glide removed the custom code component and this is the answer :laughing:

2 Likes

I’ll try this!

I usually
1/ ask to add the data fields to the DOM (header)
2/ create component with this exact same code
{{write the code in the body minus the first 2 div}}

I always find this process to avoid waisting time if you remove step 2/ the data fields are kept safe

1 Like

Good hint!