diff --git a/package-lock.json b/package-lock.json index d05149a..65e624e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "contentful": "^10.6.16", "dotenv-cli": "^7.3.0", "framer-motion": "^10.16.4", + "ioredis": "^5.3.2", "lucia": "^2.7.4", "mongodb": "^6.2.0", "mongoose": "^8.0.4", @@ -2176,6 +2177,11 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4679,6 +4685,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/code-block-writer": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", @@ -5371,6 +5385,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", @@ -7666,6 +7688,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -8930,11 +8975,21 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10754,6 +10809,25 @@ "esprima": "~4.0.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -11272,6 +11346,11 @@ "node": "*" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", diff --git a/package.json b/package.json index 3ff1c01..1da42c8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "contentful": "^10.6.16", "dotenv-cli": "^7.3.0", "framer-motion": "^10.16.4", + "ioredis": "^5.3.2", "lucia": "^2.7.4", "mongodb": "^6.2.0", "mongoose": "^8.0.4", diff --git a/public/static/images/stock/our-mission.png b/public/static/images/stock/our-mission.png index 72b3cd4..b44dc57 100644 Binary files a/public/static/images/stock/our-mission.png and b/public/static/images/stock/our-mission.png differ diff --git a/public/static/images/stock/who-we-are.png b/public/static/images/stock/who-we-are.png index 2dacf81..39e4002 100644 Binary files a/public/static/images/stock/who-we-are.png and b/public/static/images/stock/who-we-are.png differ diff --git a/src/app/(pages)/about/page.tsx b/src/app/(pages)/about/page.tsx index ef0ffd0..4582398 100644 --- a/src/app/(pages)/about/page.tsx +++ b/src/app/(pages)/about/page.tsx @@ -1,7 +1,19 @@ 'use client'; -import {Container} from '@chakra-ui/react'; +import { + Skeleton, + Container, + Flex, + Heading, + Text, + Box, + Image, + SimpleGrid, + Icon, +} from '@chakra-ui/react'; import axios from 'axios'; +import {FiAlertCircle} from 'react-icons/fi'; import {useQuery} from '@tanstack/react-query'; +import {TeamMember} from '@types'; // get all team members const getFeatured = async () => { @@ -12,12 +24,164 @@ const getFeatured = async () => { }; const About = () => { - const {isPending, error, data} = useQuery({ + const {isPending, error, data} = useQuery>({ queryKey: ['team'], queryFn: getFeatured, }); - return ; + return ( + + + + + Welcome to the Circle + + + Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint + cillum sint consectetur cupidatat. + + + + + team photo + + + Find events and connect with other tennis players! + + + Lorem ipsum dolor sit amet, officia excepteur ex fugiat + reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit + ex esse exercitation amet. Nisi anim cupidatat excepteur officia. + Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet + voluptate voluptate dolor minim nulla est proident. + + + team photo + + + + + + Find events and connect with other tennis players! + + + Lorem ipsum dolor sit amet, officia excepteur ex fugiat + reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit + ex esse exercitation amet. Nisi anim cupidatat excepteur officia. + Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet + voluptate voluptate dolor minim nulla est proident. + + + our mission + + + + + Meet the Team + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a leo + tempus, euismod purus vitae, blandit lectus. + + + + tennis racket + + {error && ( + + + An unexpected error has occurred + + )} + + {isPending && ( + + {Array(4) + .fill(0, 0, 4) + .map(() => ( + + ))} + + )} + {data && ( + + {data.map(i => ( + + {i.name} + + + {i.name} + + + {i.role} + + + + ))} + + )} + + + + ); }; export default About; diff --git a/src/app/(pages)/gallery/page.tsx b/src/app/(pages)/gallery/page.tsx index 2fcc8dc..cfd3154 100644 --- a/src/app/(pages)/gallery/page.tsx +++ b/src/app/(pages)/gallery/page.tsx @@ -10,6 +10,9 @@ import { VStack, SimpleGrid, Image, + Flex, + Spinner, + Heading, } from '@chakra-ui/react'; import axios from 'axios'; @@ -22,8 +25,9 @@ const Gallery = () => { caption: string; }> >([]); - const [after, setAfter] = useState(null); + const [after, setAfter] = useState('unset'); const [loading, setLoading] = useState(false); + const [allLoaded, setAllLoaded] = useState(false); const loader = useRef(null); const skeletons = new Array(9) @@ -32,8 +36,9 @@ const Gallery = () => { )); - const fetchPosts = async (afterParam = null) => { + const fetchPosts = async (afterParam: string) => { if (loading) return; + if (after === undefined) return; setLoading(true); try { const res = await axios.post( @@ -45,9 +50,11 @@ const Gallery = () => { }, } ); - console.log(res); - setPosts(prevPosts => [...prevPosts, ...res.data.data]); - setAfter(res.data.paging?.cursors?.after); + + if (!allLoaded) { + setPosts(prevPosts => [...prevPosts, ...res.data.data]); + setAfter(res.data.paging?.cursors?.after); + } } catch (error) { console.error('Error fetching Instagram posts:', error); } finally { @@ -56,10 +63,14 @@ const Gallery = () => { }; useEffect(() => { - fetchPosts(); + fetchPosts(after); }, []); useEffect(() => { + if (after === undefined) { + setAllLoaded(true); + } + const options = { root: null, rootMargin: '0px', @@ -68,7 +79,7 @@ const Gallery = () => { const observer = new IntersectionObserver(entries => { const [entry] = entries; - if (entry.isIntersecting && after) { + if (entry.isIntersecting && !allLoaded && !loading) { fetchPosts(after); } }, options); @@ -82,30 +93,32 @@ const Gallery = () => { observer.unobserve(loader.current); } }; - }, [after, loading]); + }, [after, allLoaded, loading]); return (
- View our recent posts - Come join us for a fun round of tennis! + View our recent posts + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi a + leo tempus, euismod purus vitae, blandit lectus. +
- - + + {posts.length === 0 ? skeletons : posts.map((post, index) => ( { ))} - + {loading && (
- Loading... +
)}
diff --git a/src/app/(pages)/page.tsx b/src/app/(pages)/page.tsx index 4699594..0350641 100644 --- a/src/app/(pages)/page.tsx +++ b/src/app/(pages)/page.tsx @@ -113,7 +113,10 @@ const Home = () => { - + Find events and connect with other tennis players! @@ -131,11 +134,15 @@ const Home = () => { - find events & connect + + find events & connect + { const {after} = await request.json(); - try { + const fetchPosts = async () => { const url = 'https://graph.instagram.com/v18.0/17841417789493733/media'; const params = { fields: 'id,caption,media_type,media_url,permalink', access_token: process.env.NEXT_INSTAGRAM_TOKEN, limit: 9, - after: after, + after: after === 'unset' ? null : after, }; const response = await axios.get(url, {params}); - if (response.data.paging?.next) { - delete response.data.paging.next; + return response.data; + }; + + try { + if (!after) { + return ServerResponse.success({}); } - return ServerResponse.success(response.data); + const cachedPosts = await Cache.fetch( + `instagram-posts;after=${after}`, + fetchPosts, + 60 * 60 + ); + + return ServerResponse.success(cachedPosts); } catch (error) { logger.error('Error fetching data from Instagram:', error); return ServerResponse.serverError('Failed to fetch data from Instagram'); diff --git a/src/lib/index.ts b/src/lib/index.ts index b72f7ff..4688716 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,3 +3,4 @@ export * from './lucia'; export * from './mongoose'; export * from './resend'; export * from './contentful'; +export * from './redis'; diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..05739ea --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,32 @@ +import Redis from 'ioredis'; + +export const redis = new Redis(`${process.env.REDIS_URL}`); + +export class Cache { + static async fetch(key: string, fetcher: () => T, expires: number) { + const existing = await this.get(key); + + if (existing !== null) return existing; + + return this.set(key, fetcher, expires); + } + + static async set(key: string, fetcher: () => T, expires: number) { + const value = await fetcher(); + await redis.set(key, JSON.stringify(value), 'EX', expires); + + return value; + } + + static async get(key: string): Promise { + const value = await redis.get(key); + + if (value === null) return null; + + return JSON.parse(value); + } + + static async del(key: string) { + await redis.del(key); + } +}