Skip to content

Instantly share code, notes, and snippets.

@piboistudios
Last active December 15, 2024 15:19
Show Gist options
  • Save piboistudios/72734605d736222bdead88884b4bb10c to your computer and use it in GitHub Desktop.
Save piboistudios/72734605d736222bdead88884b4bb10c to your computer and use it in GitHub Desktop.
Just a Chill Reactive FlatPickr Service Scheduler w/ AlpineJS (Demo: https://lou.network/alpinejs-flatpickr-scheduler.html ; 12-16 is the only day with multiple prebooked appointments)
<html>
<head>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<link rel="stylesheet" type="text/css" href="https://npmcdn.com/flatpickr/dist/themes/dark.css">
</head>
<body>
<details>
<summary>
What do?
</summary>
<p>Basically, you select a date, select services, and select a timeslot.</p>
<p>The dates/timslots will update reactively based on the selected services to only show dates with available
timeslots that can accomodate the duration of the booked services.</p>
</details>
<details>
<summary>
Why do?
</summary>
<p>Red's Burritos won't pay for themselves</p>
</details>
<form x-data="foo" @submit.prevent>
<input type="date" x-init="doflatpickr" />
<div> Selected:<br />
<template x-if="duration && selected_date?.length">
<div>Select a Timeslot: <template x-for="slot in selected_date">
<button x-text="slot.start + ' - ' + slot.end"
@click="console.log('selected something', (selected = slot))">
</button>
</template>
</div>
</template>
<!-- Day: <span x-text="selected_date"></span> -->
Timeslot: <span x-text="selected?.dates?.join?.(' - ')"></span>
<br />
Appointment Duration: <span x-text="duration + ' minutes'"></span>
<template x-for="service in selected_services">
<div>
<dl>
<dt x-text="service.title"></dt>
<dd><button @click="service.$selected = !service.$selected">Remove</button></dd>
</dl>
</div>
</template>
</div>
<template x-for="service in available_services">
<div>
<dl>
<template x-for="(value, key) in service">
<div>
<strong>
<dt x-text="key"></dt>
</strong>
<dd x-text="service[key]"></dd>
</div>
</template>
</dl>
<button x-data @click="service.$selected = !service.$selected">
Select
</button>
</div>
</template>
</form>
</body>
<script>
let now;
let today;
let range;
const free = () => findNonOverlappingTimes(busy(), range);
const busy = () => JSON.parse(window.busy.textContent);
const freetimes = () => timeslots(free(), { begin: '04:00', end: '23:59' })
const services = () => JSON.parse(window.services.textContent);
window.addEventListener('alpine:init', () => {
Alpine.data('foo', function () {
const self = this;
return {
services: services(),
get selected_services() {
return self.$data['services'].filter(s => s.$selected);
},
get available_services() {
return self.$data['services'].filter(s => !s.$selected);
},
freebusy: freetimes(),
selected: null,
selected_date: [],
get duration() {
const duration = this.selected_services.reduce((total, service) => {
return total + service.duration_minutes;
}, 0);
return duration;
},
opts: {
// done
get services() {
return self.$data['selected_services'];
},
// api route
get freebusy() {
return self.$data['freebusy']
},
// json []
get selected_date() {
return self.$data['selected_date'];
},
set selected_date(v) {
return self.$data['selected_date'] = v;
},
// json null
get selected() {
return self.$data["selected"]
},
set selected(v) {
return self.$data["selected"] = v;
},
// sum aggregate filter
get duration() {
return self.$data["duration"];
},
},
slots: {},
doflatpickr() {
const el = this.$el;
const opts = this.opts;
let slots;
let cdateStr;
const pickr = flatpickr(el, {
onChange(selectedDates, dateStr, pickr) {
cdateStr = dateStr;
update();
}
});
Alpine.effect(update)
function update() {
opts.services.length;
const duration = opts.duration;
slots = /* opts.freebusy;// */filterByDuration(opts.freebusy, duration);
// console.log({ slots, duration });
// console.log(slots)
const days = Object.entries(slots).filter(([s, e]) => e.length).map(s => s[0]);
pickr.set('enable', days);
opts.selected_date = slots[cdateStr] && slots[cdateStr]
.flatMap(e => {
function date(i) {
return cdateStr + 'T' + i
}
const range = [date(e.start), date(e.end)].map(v => new Date(v));
let begin = range[0], end;
const slots = [];
if (duration > 0)
while (true) {
end = new Date(begin.getTime() + (1000 * 60 * duration));
if (end > range[1]) {
break;
}
slots.push({
start: time(begin),
end: time(end),
dates: [begin, end]
});
begin = end;
}
else slots.push({
start: time(begin),
end: time(range[1]),
dates: [begin, range[1]]
});
return slots;
// const range = (e.dates[1].getTime() - e.dates[0].getTime()) / (60 * 1000);
// return range > opts.duration;
});
// .filter(s => s.length);
// .
setTimeout(checkConstraints, 1);
}
function checkConstraints() {
const duration = opts.duration;
if (!opts.selected) return;
if (!opts.selected_date) {
opts.selected = undefined;
return;
}
const start = mins(opts.selected.start);
const end = mins(opts.selected.end);
opts.selected = opts.selected_date.find(slot => {
const slotStart = mins(slot.start);
const slotEnd = mins(slot.end);
return slotStart < start && slotStart < end &&
slotEnd > end && slotEnd > start
});
}
function filterByDuration(obj, durationMinutes) {
const filteredObj = {};
for (const date in obj) {
const filteredEvents = obj[date].filter(event => {
const [startHour, startMinute] = event.start.split(':').map(Number);
const [endHour, endMinute] = event.end.split(':').map(Number);
const startTimeInMinutes = startHour * 60 + startMinute;
const endTimeInMinutes = endHour * 60 + endMinute;
const eventDuration = endTimeInMinutes - startTimeInMinutes;
return eventDuration > durationMinutes;
});
if (filteredEvents.length > 0) {
filteredObj[date] = filteredEvents;
}
}
return filteredObj;
}
}
}
})
});
now = new Date();
today = new Date([now.getFullYear(), now.getMonth() + 1, now.getDate()])
range = [new Date(today.getTime() - 1000 * 60 * 60 * 12), new Date(today.getTime() + 1000 * 60 * 60 * 24 * 30)];
function timeslots(freebusytimes, hours) {
hours ??= {
begin: '09:00',
end: '17:00'
}
const ret = freebusytimes.reduce((result, times) => {
const start = new Date(times.start);
const end = new Date(times.end);
for (const d of between(start, end)) {
const _day = day(d)
result[_day] ??= [];
let overlaps;
for (let i = 0; i < result[_day].length; i++) {
let range = result[_day][i];
const rstart = new Date(_day + "T" + range.start + "Z");
const rend = new Date(_day + "T" + range.end + "Z");
if (rend > start && rend < end && rstart < start) {
overlaps ??= range.start = time(start);
}
if (rstart < end && rstart > start && rend > end) {
overlaps ??= range.end = time(end)
}
check(range);
}
if (!overlaps) {
const startday = day(start);
const startDay = new Date(startday);
const endday = day(end);
const endDay = new Date(endday);
let timestart, timeend;
if (_day === startday) {
timestart = time(start);
} else {
timestart = hours.begin;
}
if (_day === endday) {
timeend = time(end);
} else {
timeend = hours.end
}
const range = {
start: timestart,
end: timeend
};
result[_day].push(range)
check(range);
}
function check(range) {
const startmins = mins(range.start);
const endmins = mins(range.end);
if (mins(hours.begin) > startmins) {
range.start = hours.begin;
}
if (mins(hours.end) < endmins) {
range.end = hours.end;
}
if (range.start > hours.end && range.start < hours.begin) range.start = hours.begin;
if (range.end > hours.end && range.end < hours.begin) range.end = hours.end;
if (mins(range.end) < mins(range.start)) {
result[_day].splice(result[_day].indexOf(range), 1);
}
}
}
return result;
}, {});
for (const k in ret) {
if (!ret[k].length) delete ret[k];
}
return ret;
}
function mins(time) {
const [hour, min] = time.split(':').map(Number);
return (hour * 60) + min;
}
function findNonOverlappingTimes(inputTimes, range) {
// Sort the input times by start time
inputTimes.sort((a, b) => new Date(a.start) - new Date(b.start));
// Find the earliest start time and latest end time
const earliestStart = range[0];
const latestEnd = range[1];
// Initialize a list to store non-overlapping time slots
const nonOverlappingTimes = [];
// Iterate through the sorted input times
let currentEndTime = earliestStart;
for (const timeSlot of inputTimes) {
const startTime = new Date(timeSlot.start);
const endTime = new Date(timeSlot.end);
// If the current time slot starts after the previous end time,
// there's a non-overlapping time slot between them
if (startTime > currentEndTime) {
nonOverlappingTimes.push({
start: currentEndTime.toISOString(),
end: startTime.toISOString()
});
}
currentEndTime = endTime;
}
// Check for a non-overlapping time slot after the latest end time
if (latestEnd < new Date()) {
nonOverlappingTimes.push({
start: latestEnd.toISOString(),
end: new Date().toISOString()
});
}
return nonOverlappingTimes;
}
/**
*
* @param {Date} date
* @returns
*/
function day(date) {
return [date.getFullYear().toString().padStart(4, '0'), ...[date.getMonth() + 1, date.getDate() + 1].map(p => p.toString().padStart(2, '0'))].join('-')
}
/**
*
* @param {Date} date
*/
function time(date) {
return [date.getHours(), date.getMinutes()].map(p => p.toString().padStart(2, '0')).join(':');
}
function between(startDate, endDate) {
const dates = [];
const currentDate = new Date(startDate);
while (currentDate <= endDate) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
return dates;
}
</script>
<script type="application/json" id="busy">
[
{
"start": "2024-12-16T00:15:00Z",
"end": "2024-12-16T01:00:00Z"
},
{
"start": "2024-12-16T01:15:00Z",
"end": "2024-12-16T01:45:00Z"
},
{
"start": "2024-12-16T03:15:00Z",
"end": "2024-12-16T03:30:00Z"
},
{
"start": "2024-12-16T03:55:00Z",
"end": "2024-12-16T09:45:00Z"
},
{
"start": "2024-12-18T00:15:00Z",
"end": "2024-12-18T02:15:00Z"
},
{
"start": "2024-12-19T00:30:00Z",
"end": "2024-12-19T02:30:00Z"
},
{
"start": "2024-12-23T00:15:00Z",
"end": "2024-12-23T01:00:00Z"
},
{
"start": "2024-12-25T00:15:00Z",
"end": "2024-12-25T02:15:00Z"
},
{
"start": "2024-12-26T00:30:00Z",
"end": "2024-12-26T02:30:00Z"
},
{
"start": "2024-12-30T00:15:00Z",
"end": "2024-12-30T01:00:00Z"
},
{
"start": "2025-01-01T00:15:00Z",
"end": "2025-01-01T02:15:00Z"
},
{
"start": "2025-01-02T00:30:00Z",
"end": "2025-01-02T02:30:00Z"
},
{
"start": "2025-01-06T00:15:00Z",
"end": "2025-01-06T01:00:00Z"
},
{
"start": "2025-01-08T00:15:00Z",
"end": "2025-01-08T02:15:00Z"
},
{
"start": "2025-01-09T00:30:00Z",
"end": "2025-01-09T02:30:00Z"
},
{
"start": "2025-01-13T00:15:00Z",
"end": "2025-01-13T01:00:00Z"
}
]
</script>
<script type="application/json" id="services">
[
{
"id": 28,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.485Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Med Senegalese Twist",
"price_low": 120,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 1,
"price_range": "$120",
"duration": "3 hr 45 min",
"duration_minutes": 225,
"thumbnails": [],
"images": []
},
{
"id": 29,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.487Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Small Knotless Box Braids",
"price_low": 200,
"price_high": null,
"short_desc": "Protective and trendy, this braid style parts sections of your hair in knotless boxes for a tiled pattern part and even braids.",
"long_desc": null,
"overall_rating": 5,
"ratings": 1,
"price_range": "$200",
"duration": "6 hr 0 min",
"duration_minutes": 360,
"thumbnails": [],
"images": []
},
{
"id": 30,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.490Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Small Box Braids",
"price_low": 170,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 3,
"price_range": "$170",
"duration": "5 hr 0 min",
"duration_minutes": 300,
"thumbnails": [],
"images": []
},
{
"id": 31,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.492Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Sew In",
"price_low": 120,
"price_high": null,
"short_desc": "With a sew in, hair is braided down, wefts are sewn on from ear to ear.",
"long_desc": null,
"overall_rating": 5,
"ratings": 2,
"price_range": "$120",
"duration": "2 hr 30 min",
"duration_minutes": 150,
"thumbnails": [],
"images": []
},
{
"id": 32,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.495Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Loc Maintenance",
"price_low": 45,
"price_high": null,
"short_desc": "Incorporate new growth, and retwist any stray locs.",
"long_desc": null,
"overall_rating": 5,
"ratings": 9,
"price_range": "$45",
"duration": "1 hr 15 min",
"duration_minutes": 75,
"thumbnails": [],
"images": []
},
{
"id": 33,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.498Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Loc Style",
"price_low": 15,
"price_high": null,
"short_desc": "Short, long, dread, wool — we'll style your locs to your liking.",
"long_desc": null,
"overall_rating": 5,
"ratings": 4,
"price_range": "$15",
"duration": "15 min",
"duration_minutes": 15,
"thumbnails": [],
"images": []
},
{
"id": 34,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.500Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Spring Twist",
"price_low": 140,
"price_high": null,
"short_desc": "Get 4 boxes of spring twist hair of your color choice for individual spongy natural looking spring twist.",
"long_desc": null,
"overall_rating": 5,
"ratings": 1,
"price_range": "$140",
"duration": "4 hr 0 min",
"duration_minutes": 240,
"thumbnails": [],
"images": []
},
{
"id": 35,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.502Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Women's Trim",
"price_low": 15,
"price_high": null,
"short_desc": "Maintain strong and healthy hair with a trim.",
"long_desc": null,
"overall_rating": 5,
"ratings": 7,
"price_range": "$15",
"duration": "30 min",
"duration_minutes": 30,
"thumbnails": [],
"images": []
},
{
"id": 36,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.503Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Cornrows",
"price_low": 45,
"price_high": null,
"short_desc": "Cornrows braid the hair close to the scalp for chic raised braids.",
"long_desc": null,
"overall_rating": 5,
"ratings": 6,
"price_range": "$45",
"duration": "1 hr 45 min",
"duration_minutes": 105,
"thumbnails": [],
"images": []
},
{
"id": 37,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.505Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Insta Loc Extensions",
"price_low": 400,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 0,
"ratings": 0,
"price_range": "$400",
"duration": "5 hr 0 min",
"duration_minutes": 300,
"thumbnails": [],
"images": []
},
{
"id": 38,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.507Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T06:08:28.935Z",
"title": "Butterfly Locs",
"price_low": 130,
"price_high": null,
"short_desc": "Protective and trendy, this braid style parts sections of your hair in boxes for a tiled pattern parts.",
"long_desc": null,
"overall_rating": 5,
"ratings": 2,
"price_range": "$130",
"duration": "3 hr 0 min",
"duration_minutes": 180,
"thumbnails": [
1
],
"images": [
1
]
},
{
"id": 39,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.508Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:21:04.459Z",
"title": "Large Box Braids",
"price_low": 95,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 1,
"price_range": "$95",
"duration": "2 hr 45 min",
"duration_minutes": 165,
"thumbnails": [
3,
4
],
"images": [
3,
4
]
},
{
"id": 40,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.510Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:21:22.577Z",
"title": "Med Knotless Box Braids",
"price_low": 150,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 2,
"price_range": "$150",
"duration": "4 hr 0 min",
"duration_minutes": 240,
"thumbnails": [
5
],
"images": [
5
]
},
{
"id": 41,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.512Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:20:34.315Z",
"title": "Crochet Braids",
"price_low": 85,
"price_high": null,
"short_desc": "Stylish and protective. Attaching extensions through your natural hair for a full-bodied look.",
"long_desc": null,
"overall_rating": 5,
"ratings": 39,
"price_range": "$85",
"duration": "2 hr 30 min",
"duration_minutes": 150,
"thumbnails": [
2
],
"images": [
2
]
},
{
"id": 42,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.513Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Havana Twist",
"price_low": 95,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 1,
"price_range": "$95",
"duration": "2 hr 45 min",
"duration_minutes": 165,
"thumbnails": [],
"images": []
},
{
"id": 43,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.515Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:21:46.544Z",
"title": "Medium Box Braids",
"price_low": 120,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 9,
"price_range": "$120",
"duration": "3 hr 30 min",
"duration_minutes": 210,
"thumbnails": [
6
],
"images": [
6
]
},
{
"id": 44,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.517Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:22:06.458Z",
"title": "Faux Locs",
"price_low": 150,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 3,
"price_range": "$150",
"duration": "5 hr 0 min",
"duration_minutes": 300,
"thumbnails": [
7
],
"images": [
7
]
},
{
"id": 45,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.519Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Passion Twist",
"price_low": 140,
"price_high": null,
"short_desc": "9 packs of expressions twisted up passion water wave hair. Can also be found on Amazon",
"long_desc": null,
"overall_rating": 5,
"ratings": 3,
"price_range": "$140",
"duration": "4 hr 0 min",
"duration_minutes": 240,
"thumbnails": [],
"images": []
},
{
"id": 46,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.521Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T07:22:35.574Z",
"title": "Soft Locs",
"price_low": 140,
"price_high": null,
"short_desc": "",
"long_desc": null,
"overall_rating": 5,
"ratings": 3,
"price_range": "$140",
"duration": "4 hr 0 min",
"duration_minutes": 240,
"thumbnails": [
8,
9
],
"images": [
8,
9
]
},
{
"id": 47,
"status": "draft",
"user_created": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_created": "2024-12-13T03:16:27.522Z",
"user_updated": "763841c4-bffb-4ca0-acad-ddd5eadbb07b",
"date_updated": "2024-12-13T05:59:27.460Z",
"title": "Small To Med Feedin Braids",
"price_low": 150,
"price_high": null,
"short_desc": "Feed in Cornrows braid the hair close to the scalp for chic raised braids.",
"long_desc": null,
"overall_rating": 0,
"ratings": 0,
"price_range": "$150",
"duration": "5 hr 0 min",
"duration_minutes": 300,
"thumbnails": [],
"images": []
}
]
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment