├── .gitignore ├── README.md ├── favicon.svg ├── functions └── calFetch.mjs ├── index.html ├── main.js ├── netlify.toml ├── package-lock.json ├── package.json ├── postcss.config.js ├── style.css └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | # Local Netlify folder 7 | .netlify 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Google Calendar Event Page 2 | 3 | Google Calendar is the default system for a reason: it’s stable, ubiquitous, and easy-to-use. Displaying calendar data, however can be a bit clunky, especially if you want your event page to reflect your site’s branding. In this series, I’ll show you how to: 4 | - Create a custom calendar event page with HTML, Tailwind CSS, and ViteJS 5 | - How to publish your site to Netlify 6 | - How to get a Google API key and store it safely as a Netlify environmental variable 7 | - How to write your own custom serverless function, allowing for dynamic query string parameters 8 | - How to fetch dynamic calendar data from your Google Calendar endpoint and display it on your site. 9 | 10 | 🔗 Key Links 🔗 11 | - Live code: https://codinginpublic.dev/projects/google-calendar-event-page/ 12 | - GitHub: https://github.com/coding-in-public/google-calendar-event-page 13 | Help me make it better! Contribute to the community-improvements branch on GitHub. 14 | 15 | 📹 Other Videos in this Series 📹 16 | - Full Playlist: https://www.youtube.com/watch?v=SOsGToYI0MQ&list=PLoqZcxvpWzzeuzWsIpH-N1b3vnRVbrdm3 17 | - Video 1: https://youtu.be/SOsGToYI0MQ 18 | - Video 2: https://youtu.be/CObyHiI7TEQ 19 | - Video 3: https://youtu.be/wf5gVe8hWnk 20 | - Video 4: https://youtu.be/_ec2ps7w8s4 21 | 22 | --------------------------------------- 23 | 24 | 🔗 Additional Links 🔗 25 | - NodeJS: https://nodejs.org/en/ 26 | - Vite JS: https://vitejs.dev/ 27 | - Tailwind: https://tailwindcss.com/docs/installation/using-postcss 28 | 29 | --------------------------------------- 30 | 31 | 🌐 Connect With Me 🌐 32 | - Website: https://www.codinginpublic.dev 33 | - Blog: https://www.chrispennington.blog 34 | - Twitter: https://twitter.com/cpenned 35 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /functions/calFetch.mjs: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const { CAL_API, CAL_ID } = process.env; 4 | const BASEPARAMS = `orderBy=startTime&singleEvents=true&timeMin=${new Date().toISOString()}` 5 | const BASEURL = `https://www.googleapis.com/calendar/v3/calendars/${CAL_ID}/events?${BASEPARAMS}` 6 | 7 | const HEADERS = { 8 | 'Content-Type': 'application/json', 9 | 'Access-Control-Allow-Methods': 'GET', 10 | } 11 | 12 | exports.handler = async function (event, context) { 13 | const finalURL = `${BASEURL}${event.queryStringParameters.maxResults ? `&maxResults=${event.queryStringParameters.maxResults}` : ''}&key=${CAL_API}` 14 | try { 15 | if (event.httpMethod === 'GET') { 16 | return fetch(finalURL) 17 | .then((response) => response.json()) 18 | .then((data) => ({ 19 | statusCode: 200, 20 | body: JSON.stringify(data.items, null, 2), 21 | HEADERS 22 | })) 23 | } 24 | return { 25 | statusCode: 401 26 | } 27 | } catch (e) { 28 | console.error(e) 29 | return { 30 | statusCode: 500, 31 | body: e.toString() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Google Calender Event Page 8 | 9 | 10 |
11 |

My Calender Events

12 | 13 | 14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |

Loading Events

23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | const eventContainer = document.querySelector('#events-container'); 4 | const eventAmtToFetch = document.querySelector('#eventAmt'); 5 | 6 | const getRandomNumBetween = (min, max) => Math.floor(Math.random() * (max-min +1)) + min; 7 | const getMonth = (month) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month]; 8 | const getDayOfWeek = (weekday) => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][weekday] 9 | const isAM = (hour) => hour < 12; 10 | const getHour = (hour) => (hour <= 12 ? hour : hour - 12); 11 | const getMinute = (minute) => (minute === 0 ? '00' : minute); 12 | 13 | function processDate(date){ 14 | const hour = getHour(date.getHours()) === 0 15 | ? false 16 | : getHour(date.getHours()); 17 | const minute = getMinute(date.getMinutes()); 18 | const timeSuffix = `${isAM(date.getHours()) 19 | ? `AM` 20 | : `PM` 21 | }` 22 | const time = hour && `${hour}:${minute}${timeSuffix}`; 23 | 24 | return { 25 | month: getMonth(date.getMonth()), 26 | weekday: getDayOfWeek(date.getDay()), 27 | time, 28 | date: date.getDate(), 29 | } 30 | } 31 | 32 | function mapEventObject(event){ 33 | const startDate = event.start.dateTime 34 | ? processDate(new Date(event.start.dateTime)) 35 | : processDate(new Date(`${event.start.date}T00:00:00`)) 36 | const endDate = event.end.dateTime 37 | ? processDate(new Date(event.end.dateTime)) 38 | : processDate(new Date(`${event.end.date}T00:00:00`)) 39 | let dateRange; 40 | if (startDate.date !== endDate.date){ 41 | dateRange = `${startDate.month} ${startDate.date}–${endDate.month} ${endDate.date}` 42 | } else if (!startDate.time) { 43 | dateRange = `${startDate.month} ${startDate.date}`; 44 | } else { 45 | dateRange = `${startDate.weekday}, ${startDate.time}–${endDate.time}`; 46 | } 47 | 48 | return { 49 | name: event.summary, 50 | description: event.description, 51 | location: event.location, 52 | start: startDate, 53 | end: endDate, 54 | dateRange, 55 | link: event.htmlLink, 56 | } 57 | } 58 | 59 | function createEvent(e, i){ 60 | const colors = ['blue', 'amber', 'rose', 'indigo', 'pink']; 61 | const colorScheme = colors[getRandomNumBetween(0, colors.length - 1)] 62 | return `
63 |
64 |
${e.start.month}
65 |
${e.start.date}
66 |
67 |
68 |
69 |

${e.dateRange}

70 |

71 | ${e.name} 72 | ${e.location 73 | ? `

${e.location}

` 74 | : ''} 75 |

76 | ${ 77 | e.description 78 | ? `
79 | 85 | 88 |
89 |
` 90 | : '
' 91 | } 92 | View Event 93 |
94 |
` 95 | } 96 | 97 | async function loadEvents(max=8){ 98 | try { 99 | const endpoint = await fetch(`./.netlify/functions/calFetch?maxResults=${max}`); 100 | const data = await endpoint.json(); 101 | const processedEvents = data.map(e => mapEventObject(e)); 102 | eventContainer.innerHTML = processedEvents.map((event, i) => createEvent(event, i)).join(''); 103 | } catch (e) { 104 | eventContainer.innerHTML = `

🙀 Something went wrong!

` 105 | console.log(e); 106 | } 107 | } 108 | loadEvents(); 109 | 110 | eventContainer.addEventListener('click', (e) =>{ 111 | if(e.target.hasAttribute('aria-expanded')){ 112 | e.target.setAttribute('aria-expanded', e.target.getAttribute('aria-expanded') === 'false' ? 'true' : 'false'); 113 | e.target.querySelector('svg').classList.toggle('rotate-180'); 114 | e.target.nextElementSibling.classList.toggle('hidden'); 115 | } 116 | }) 117 | eventAmtToFetch.addEventListener('change', (e) => loadEvents(eventAmtToFetch.value)) -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = 'functions' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-cal-api", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "devDependencies": { 10 | "autoprefixer": "^10.4.2", 11 | "netlify-cli": "^8.18.1", 12 | "postcss": "^8.4.5", 13 | "tailwindcss": "^3.0.18", 14 | "vite": "^2.7.2" 15 | }, 16 | "dependencies": { 17 | "node-fetch": "^3.2.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["*.{html,js}"], 3 | safelist: [ 4 | 'bg-blue-500', 5 | 'bg-amber-500', 6 | 'bg-rose-500', 7 | 'bg-indigo-500', 8 | 'bg-pink-500', 9 | 'text-blue-50', 10 | 'text-amber-50', 11 | 'text-rose-50', 12 | 'text-indigo-50', 13 | 'text-pink-50', 14 | 'ring-blue-500', 15 | 'ring-amber-500', 16 | 'ring-rose-500', 17 | 'ring-indigo-500', 18 | 'ring-pink-500', 19 | 'shadow-blue-200', 20 | 'shadow-amber-200', 21 | 'shadow-rose-200', 22 | 'shadow-indigo-200', 23 | 'shadow-pink-200', 24 | ], 25 | theme: { 26 | extend: { 27 | gridTemplateRows: { 28 | 'auto1': 'auto 1fr', 29 | }, 30 | gridTemplateColumns: { 31 | 'cards': 'repeat(auto-fit, minmax(250px, 1fr))', 32 | } 33 | }, 34 | }, 35 | plugins: [], 36 | } --------------------------------------------------------------------------------