@articles-media/articles-dev-box 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ import { useState, useEffect, memo } from 'react';
2
+
3
+ // import { useSelector, useDispatch } from 'react-redux'
4
+
5
+ // import Link from 'next/link'
6
+ // import dynamic from 'next/dynamic'
7
+
8
+ import axios from 'axios'
9
+
10
+ // import Popover from 'react-bootstrap/Popover';
11
+ // import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
12
+
13
+ import { useInView } from 'react-intersection-observer';
14
+
15
+ // import ROUTES from 'components/constants/routes';
16
+ import useAd from '../hooks/Ads/useAd';
17
+ import useAds from '../hooks/Ads/useAds';
18
+
19
+ // import SavePromoModal from 'components/Ads/SavePromoModal';
20
+ // const SavePromoModal = dynamic(
21
+ // () => import('@/components/Ads/SavePromoModal'),
22
+ // { ssr: false }
23
+ // )
24
+
25
+ // const AdDetailsModal = dynamic(
26
+ // () => import('@/components/Ads/AdDetailsModal'),
27
+ // { ssr: false }
28
+ // )
29
+
30
+ // import generateRandomInteger from 'util/generateRandomInteger'
31
+ // import { setViewedAds } from '@/redux/actions/adsActions';
32
+ import { differenceInMinutes, parse, parseISO } from 'date-fns';
33
+ import ArticlesButton from '../Button';
34
+ // import useAds from 'hooks/Ads/useAds';
35
+
36
+ // import "../../styles/components/Ads/Ad.scss";
37
+ import "../../styles/components/Ad.scss";
38
+
39
+ function generateRandomInteger(min, max) {
40
+ return Math.floor(Math.random() * (max - min + 1)) + min;
41
+ }
42
+
43
+ function Ad(props) {
44
+
45
+ // const dispatch = useDispatch()
46
+
47
+ const userReduxState = false
48
+ const viewedAds = []
49
+
50
+ // const userReduxState = useSelector((state) => state.auth.user_details)
51
+ // const viewedAds = useSelector((state) => state.ads.viewed_ads)
52
+
53
+ // const reduxAds = useSelector((state) => state.ads.ads)
54
+ // const dev_force_ad = useSelector((state) => state.site.dev_force_ad)
55
+
56
+ // const ads = []
57
+
58
+ const {
59
+ data: ads,
60
+ isLoading: adsIsLoading,
61
+ mutate: adsMutate
62
+ } = useAds()
63
+
64
+ // props.setLocation(props.tabLocation);
65
+
66
+ let { previewMode } = props;
67
+ let previewData = props.previewData || {}
68
+
69
+ const [adId, setAdId] = useState(null)
70
+
71
+ const [randomAdId, setRandomAdId] = useState(null);
72
+ const [promoId, setPromoId] = useState(null);
73
+
74
+ // const [ad, setAd] = useState({});
75
+
76
+ const [promo, setPromo] = useState(null);
77
+ const [promoIndex, setPromoIndex] = useState(0);
78
+
79
+ const [modalShow, setModalShow] = useState(false);
80
+ // const [modalLoading, setModalLoading] = useState(false);
81
+
82
+ const [adDetailsExpanded, setAdDetailsExpanded] = useState(false);
83
+
84
+ const [selectedDate, handleDateChange] = useState(new Date());
85
+
86
+ const [loggedEvents, setLoggedEvents] = useState([]);
87
+
88
+ const { data: ad, isLoading: adIsLoading } = useAd(adId)
89
+
90
+ useEffect(() => {
91
+
92
+ if (!ads) return
93
+
94
+ if (ads?.length > 0 && !adId) {
95
+ console.log("Ad Mounted or reduxAds changed")
96
+ setAdId(props.ad_id || ads[generateRandomInteger(0, ads?.length - 1)]?._id)
97
+ }
98
+
99
+ return
100
+
101
+ let randomId = props.ad_id || reduxAds[generateRandomInteger(0, reduxAds.length - 1)]?._id
102
+
103
+ setRandomAdId(randomId)
104
+
105
+ getAdData()
106
+
107
+ }, [ads]);
108
+
109
+ // useEffect(() => {
110
+
111
+ // if (randomAdId) {
112
+
113
+
114
+
115
+ // }
116
+
117
+ // }, [randomAdId]);
118
+
119
+ useEffect(() => {
120
+
121
+ // if (randomAdId) {
122
+
123
+ // axios.get(`/api/ads/${randomAdId}`, {
124
+ // params: {
125
+ // ad_id: props.ad_id,
126
+ // ...(dev_force_ad && { force_ad: dev_force_ad })
127
+ // }
128
+ // })
129
+ // .then(function (response) {
130
+ // setAd(response.data.result)
131
+ // })
132
+ // .catch(function (error) {
133
+ // console.log(error);
134
+ // });
135
+
136
+ // }
137
+
138
+ }, [ad]);
139
+
140
+ useEffect(() => {
141
+
142
+ if (ad?.populated_promos && promoIndex >= 0) {
143
+ setPromo(ad?.populated_promos[promoIndex])
144
+ }
145
+
146
+ }, [promoIndex, ad]);
147
+
148
+ // const popover = (
149
+ // <Popover className="hours-popover " id="popover-basic">
150
+ // <Popover.Header as="h3">Store Hours</Popover.Header>
151
+ // <Popover.Body>
152
+ // <div className="day-wrap">
153
+ // <div className="day">Sunday:</div>
154
+ // <div className="hours">6:30AM–8PM</div>
155
+ // </div>
156
+ // <div className="day-wrap active">
157
+ // <div className="day"><b>Monday:</b></div>
158
+ // <div className="hours">6:30AM–8PM</div>
159
+ // </div>
160
+ // <div className="day-wrap">
161
+ // <div className="day">Tuesday:</div>
162
+ // <div className="hours">6:30AM–8PM</div>
163
+ // </div>
164
+ // <div className="day-wrap">
165
+ // <div className="day">Wednesday:</div>
166
+ // <div className="hours">6:30AM–8PM</div>
167
+ // </div>
168
+ // <div className="day-wrap">
169
+ // <div className="day">Thursday:</div>
170
+ // <div className="hours">6:30AM–8PM</div>
171
+ // </div>
172
+ // <div className="day-wrap">
173
+ // <div className="day">Friday:</div>
174
+ // <div className="hours">6:30AM–8PM</div>ad?.
175
+ // </div>
176
+ // <div className="day-wrap">
177
+ // <div className="day">Saturday:</div>
178
+ // <div className="hours">6:30AM–8PM</div>
179
+ // </div>
180
+ // </Popover.Body>
181
+ // </Popover>
182
+ // );
183
+
184
+ function adDetailsExpandedToggle() {
185
+ setAdDetailsExpanded(!adDetailsExpanded);
186
+ }
187
+
188
+ const { ref, inView, entry } = useInView({
189
+ /* Optional options */
190
+ threshold: 0,
191
+ triggerOnce: true
192
+ });
193
+
194
+ function logEvent(event) {
195
+
196
+ if (previewMode) {
197
+ console.log("Preventing this event from being logged as this ad is being shown in preview mode.")
198
+ }
199
+
200
+ if (loggedEvents.find(obj => obj == event)) {
201
+ console.log("Already logged this event");
202
+ return
203
+ }
204
+
205
+ axios.get(`/api/ads/event`, {
206
+ params: {
207
+ ad_id: ad?._id,
208
+ event: event
209
+ }
210
+ })
211
+ .then(function (response) {
212
+ setLoggedEvents([...loggedEvents, event])
213
+ console.log(response.data);
214
+ // setAd(response.data.result)
215
+ })
216
+ .catch(function (error) {
217
+ console.log(error);
218
+ });
219
+
220
+ }
221
+
222
+ useEffect(() => {
223
+
224
+ if (!previewMode) {
225
+
226
+ console.log("inView", inView)
227
+
228
+ if (inView && adId) {
229
+
230
+ // Records previously viewed ads from the last 5 minutes
231
+
232
+ // Attempts to prevent duplicate viewed ads for more then 5 minutes
233
+
234
+ let unexpiredRecentViews = [
235
+
236
+ {
237
+ ad_id: adId,
238
+ date: new Date().toString()
239
+ },
240
+
241
+ ...viewedAds.filter(obj => {
242
+
243
+ console.log(
244
+ differenceInMinutes(new Date(), new Date(obj.date))
245
+ )
246
+
247
+ if (
248
+ differenceInMinutes(new Date(), new Date(obj.date)) > 5
249
+ ) {
250
+ console.log("adsViewed - Remove Old Ad View Object")
251
+ return
252
+ } else {
253
+ console.log("adsViewed - Keep Ad View Object")
254
+ return (obj)
255
+ }
256
+
257
+ })
258
+
259
+ ]
260
+
261
+ console.log("unexpiredRecentViews", unexpiredRecentViews)
262
+
263
+ // TODO - Record viewed ad locally to prevent repeat
264
+ // dispatch(
265
+ // setViewedAds(
266
+ // unexpiredRecentViews
267
+ // // []
268
+ // // [
269
+ // // {
270
+ // // ad_id: adId,
271
+ // // date: new Date().toString()
272
+ // // },
273
+ // // unexpiredRecentViews,
274
+ // // ]
275
+ // )
276
+ // )
277
+
278
+ // axios.post(`/api/ads/viewed`, {
279
+ // ad_id: adId,
280
+ // section: props.section,
281
+ // section_id: props.section_id
282
+ // })
283
+ // .then(function (response) {
284
+ // // console.log(response);
285
+ // // setAd(response.data.result)
286
+ // })
287
+ // .catch(function (error) {
288
+ // console.log(error);
289
+ // });
290
+
291
+ }
292
+
293
+ }
294
+
295
+ }, [inView, adId]);
296
+
297
+ // TODO - Log when a ad free member would have viewed an ad to later show them how many ads they avoided
298
+ // TODO - Make component to show to user in membership settings page how many ads they avoided
299
+
300
+ return (
301
+ <div
302
+ ref={ref}
303
+ className="ad-wrap"
304
+ style={
305
+ {
306
+ "--articles-ad-background-color": previewData.background_color || ad?.background_color,
307
+ "--articles-ad-font-color": previewData.font_color || ad?.font_color,
308
+ "--articles-ad-border-color": previewData.border_color || ad?.border_color,
309
+ }
310
+ }
311
+ >
312
+
313
+ {/* TODO */}
314
+ {/* {modalShow &&
315
+ <SavePromoModal
316
+ setModalShow={setModalShow}
317
+ promo={promo}
318
+ ad={ad}
319
+ />
320
+ }
321
+
322
+ {adDetailsExpanded &&
323
+ <AdDetailsModal
324
+ setModalShow={setAdDetailsExpanded}
325
+ ad={ad}
326
+ previewData={previewData}
327
+ />
328
+ } */}
329
+
330
+ <div
331
+ className='ad'
332
+ >
333
+
334
+ <div
335
+ className="main-panel"
336
+ >
337
+
338
+ <div className="ad-warning flex-header">
339
+
340
+ <div className=''>{ad?.city && 'Local'} Advertisement</div>
341
+
342
+ {/* TODO - Quick link to manage for devs */}
343
+ {/* {userReduxState?.roles?.isDev &&
344
+ <div className=''>
345
+ <Link
346
+ href={`${ROUTES.ADVERTISING}/${ad?._id}`}
347
+ >
348
+ <i
349
+ className="fad fa-code action me-0"
350
+ style={{
351
+ fontSize: '0.7rem',
352
+ width: '20px',
353
+ height: '20px'
354
+ }}
355
+ >
356
+
357
+ </i>
358
+ </Link>
359
+ </div>
360
+ } */}
361
+
362
+ </div>
363
+
364
+ <div className="content-wrap">
365
+ <div className="photo-banner">
366
+
367
+ <div className="logo">
368
+ {(previewData.logo?.location || ad?.logo?.location) &&
369
+ <img
370
+ src={previewData?.logo?.key ?
371
+ `${process.env.NEXT_PUBLIC_CDN}${previewData?.logo?.key}`
372
+ :
373
+ `${process.env.NEXT_PUBLIC_CDN}${ad?.logo?.key}`
374
+ }
375
+ alt=""
376
+ />
377
+ }
378
+ </div>
379
+
380
+ <div className="icon d-none">
381
+ <i className="fas fa-mug-hot"></i>
382
+ </div>
383
+
384
+ <img
385
+ className="photo" src={
386
+ previewData?.background?.key ?
387
+ `${process.env.NEXT_PUBLIC_CDN}${previewData.background?.key}`
388
+ :
389
+ `${process.env.NEXT_PUBLIC_CDN}${ad?.background?.key}`
390
+ }
391
+ alt=""
392
+ />
393
+
394
+ </div>
395
+
396
+ <div className="details-wrap">
397
+
398
+ <div className="detail-title">
399
+
400
+ <div className="detail">
401
+ {/* <span className="icon"><i className="fas fa-store-alt"></i></span> */}
402
+ <span className='h4'>{previewData?.business || ad?.business}</span>
403
+ </div>
404
+
405
+ <div className='flex flex-column d-none'>
406
+ <div className="detail">
407
+ <span className="icon"><i className="fas fa-search-location"></i></span>
408
+ <span>{ad?.city}, {ad?.state}</span>
409
+ </div>
410
+
411
+ <div className="detail">
412
+
413
+ <span className="icon"><i className="fas fa-clock me-2"></i></span>
414
+
415
+ <span>
416
+ 6:30AM–8PM
417
+ {/* <i className="fas fa-caret-square-down me-0 ms-1"></i> */}
418
+ {/* <OverlayTrigger rootClose trigger="click" placement="bottom" overlay={popover}>
419
+ <i style={{ cursor: 'pointer' }} className="fas fa-caret-square-down me-0 ms-1"></i>
420
+ </OverlayTrigger> */}
421
+ </span>
422
+
423
+ </div>
424
+ </div>
425
+
426
+ </div>
427
+
428
+ {ad?.city && <div className="details mb-3 d-none">
429
+ {/*
430
+ <div className="detail">
431
+ <span className="icon"><i className="fas fa-search-location"></i></span>
432
+ <span>{ad?.city}, {ad?.state}</span>
433
+ </div> */}
434
+
435
+ {/* <div className="detail">
436
+ <span className="icon"><i className="fas fa-user-friends"></i></span>
437
+ <span>5-10 Employees</span>
438
+ </div> */}
439
+
440
+ {/* <div className="detail">
441
+ <span className="icon"><i className="fas fa-user-friends"></i></span>
442
+ <span>Outdoor Seating</span>
443
+ </div> */}
444
+
445
+ </div>}
446
+
447
+ <div className="short-description">{previewData?.description || ad?.description}</div>
448
+
449
+ {/* <p>{JSON.stringify(previewData)}</p> */}
450
+
451
+ </div>
452
+ </div>
453
+
454
+ {/* Make dynamic */}
455
+ {(userReduxState?.roles?.isDev && ad?.populated_promos?.length > 0) &&
456
+ <div>
457
+
458
+ {/* {ad?.populated_promos && <pre>
459
+ {
460
+ JSON.stringify(ad?.populated_promos[Math.floor(Math.random() * ad?.populated_promos?.length)])
461
+ }
462
+ </pre>} */}
463
+
464
+ {/* Active Promo */}
465
+ {promo && <div className="promos-wrap">
466
+ {
467
+ promo &&
468
+ <div
469
+ key={promo._id}
470
+ className="promo-wrap d-flex justify-content-between align-items-center mx-2 p-1 px-2 border border-2 border-light mb-0"
471
+ >
472
+
473
+ <div className=''>
474
+ <div>
475
+ {promo.title}
476
+ </div>
477
+ <div className="small">
478
+ <div className="small">
479
+ {promo.details}
480
+ </div>
481
+ </div>
482
+ </div>
483
+
484
+ <ArticlesButton
485
+ className="px-3"
486
+ small
487
+ onClick={() => {
488
+ console.log("Load Save Modal")
489
+ setModalShow(true)
490
+ }}
491
+ >
492
+ Save
493
+ </ArticlesButton>
494
+
495
+ </div>
496
+
497
+ }
498
+ </div>}
499
+
500
+ {/* Controls */}
501
+ <div className='d-flex justify-content-between'>
502
+ <div className='px-2'>{ad?.populated_promos?.length} Promos Active</div>
503
+ <div className='controls'>
504
+ <i
505
+ className="fad fa-arrow-circle-left"
506
+ type="button"
507
+ onClick={() => {
508
+
509
+ if (promoIndex == 0) {
510
+ setPromoIndex(ad?.populated_promos?.length - 1)
511
+ } else {
512
+ setPromoIndex(prev => prev - 1)
513
+ }
514
+
515
+ }}
516
+ ></i>
517
+ {ad?.populated_promos?.map((obj, obj_i) =>
518
+ <i
519
+ key={obj._id}
520
+ className={`fa-square ${obj_i == promoIndex ? 'fad' : 'fas'}`}
521
+ >
522
+
523
+ </i>
524
+ )}
525
+ <i
526
+ className="fad fa-arrow-circle-right"
527
+ type="button"
528
+ onClick={() => {
529
+
530
+ if (promoIndex == ad?.populated_promos?.length - 1) {
531
+ setPromoIndex(0)
532
+ } else {
533
+ setPromoIndex(prev => prev + 1)
534
+ }
535
+
536
+ }}
537
+ ></i>
538
+ </div>
539
+ </div>
540
+
541
+ </div>
542
+ }
543
+
544
+ <hr style={{ borderColor: 'white' }} className="mt-auto mb-0" />
545
+
546
+ <div className="action-wrap d-flex justify-content-lg-between px-3 py-2">
547
+
548
+ <div
549
+ onClick={() => {
550
+ adDetailsExpandedToggle()
551
+ logEvent('Details')
552
+ }}
553
+ className="action flex-grow-1 flex-shrink-0"
554
+ >
555
+ Details
556
+ </div>
557
+
558
+ {/* <a style={
559
+ {
560
+ color: ad?.font_color,
561
+ borderColor: `${ad?.border_color}!important`
562
+ }
563
+ } className="action flex-grow-1 flex-shrink-0" href={ad?.website} target="_blank" rel="noreferrer">
564
+ <div>
565
+ Hours
566
+ </div>
567
+ </a> */}
568
+
569
+ <span className='px-4'></span>
570
+
571
+ <a
572
+ className="action flex-grow-1 flex-shrink-0"
573
+ href={ad?.website}
574
+ target="_blank"
575
+ rel="noreferrer"
576
+ onClick={() => logEvent('Website')}
577
+ >
578
+ <div>
579
+ Website
580
+ </div>
581
+ </a>
582
+
583
+ </div>
584
+ </div>
585
+
586
+ </div>
587
+
588
+ {/* {previewMode &&
589
+ <div className='small'>
590
+ <pre>
591
+ {JSON.stringify(previewData, null, 2)}
592
+ </pre>
593
+ </div>
594
+ } */}
595
+
596
+ {!previewMode &&
597
+ <div
598
+ className='advertise-with-us p-1'
599
+ style={
600
+ {
601
+ // ...(props.previewData ? props.previewData.background_color : ad?.background_color),
602
+ backgroundColor: previewData.background_color || ad?.background_color,
603
+ color: previewData.font_color || ad?.font_color,
604
+ borderColor: previewData.border_color || ad?.border_color
605
+ }
606
+ }
607
+ >
608
+ {/* <Link
609
+ className='small d-block w-100 text-center'
610
+ href={ROUTES.ADVERTISING}
611
+ >
612
+ <i className="fas fa-share me-1"></i>
613
+ Advertise with Articles Media!
614
+ </Link> */}
615
+ <div
616
+ className='small d-block w-100 text-center'
617
+ // href={ROUTES.ADVERTISING}
618
+ >
619
+ <i className="fas fa-share me-1"></i>
620
+ Advertise with Articles Media!
621
+ </div>
622
+ </div>
623
+ }
624
+
625
+ </div>
626
+ );
627
+ }
628
+
629
+ export default memo(Ad)
@@ -0,0 +1,55 @@
1
+ import useSWR from "swr";
2
+
3
+ import axios from "axios";
4
+
5
+ const fetcher = async (data) => {
6
+ if (process.env.NODE_ENV === 'development') {
7
+ try {
8
+ const res = await axios.get(`http://localhost:3001/api/ads/${data.ad_id}`, {
9
+ params: {
10
+ ad_id: data.ad_id
11
+ }
12
+ });
13
+ return res.data.result;
14
+ } catch (err) {
15
+ // Failed to fetch from localhost, fallback to default URL
16
+ }
17
+ }
18
+
19
+ return axios.get(data.url, {
20
+ params: {
21
+ ad_id: data.ad_id
22
+ }
23
+ }).then((res) => res.data.result);
24
+ };
25
+
26
+ const minutes = 60;
27
+ const options = {
28
+ dedupingInterval: ((1000 * 60) * minutes),
29
+ // keepPreviousData: true,
30
+ // fallbackData: []
31
+ }
32
+
33
+ const useAd = (ad_id) => {
34
+
35
+ const { data, error, isLoading, mutate } = useSWR(
36
+ ad_id ?
37
+ {
38
+ url: `https://articles.media/api/ads/${ad_id}`,
39
+ ad_id
40
+ }
41
+ :
42
+ null,
43
+ fetcher,
44
+ options
45
+ );
46
+
47
+ return {
48
+ data,
49
+ error,
50
+ isLoading,
51
+ mutate,
52
+ };
53
+ };
54
+
55
+ export default useAd;