Building a Live Concert Enhancement Platform for TikTok with Next.js
Introduction
I recently participated in the TikTok TechJam hackathon and developed "TikTok Concerts," a platform enhancing live concert experiences on TikTok Live. The project features event scheduling, a global events map, and user interfaces for artists and fans, built using Next.js, TypeScript, React, and Tailwind CSS. If you find the project interesting, please explore the platform and consider voting for it in the hackathon!
I just submitted my project for the TikTok TechJam hackathon!
For those who don't know, TikTok just held its first hackathon. TechJam is a hackathon open to college students, with multiple tracks to choose from. I chose the music discovery track. The goal was to create a project that enhances artist exposure on TikTok, promotes diverse content discovery, boosts the viral potential of music on TikTok, and increases engagement and interaction between artists and fans.
TikTok is a social media platform known for its short videos, and it has become a major player in the music industry. Many artists have gained popularity through viral TikTok videos, making it an important platform for music discovery and promotion.
Overview of TikTok Concerts
My project, titled TikTok Concerts, is designed to enhance the experience of watching live concerts on TikTok Live. It offers several features to help artists and users connect more easily.
The inspiration for this project came from an event called TikTok In The Mix. This event was the first global live concert and set the record for the biggest live event ever on TikTok. Since it was so popular, I thought it would be a good idea to create features that enhance the live concert experience for both artists and fans. These features will encourage artists to share their performances on TikTok Live and invite fans to watch through the TikTok app. It will also help smaller artists reach a wider audience.
What it Does
My project supports the following main features:
Event Scheduling: Artists can schedule their live performances and make them publicly available.
Global Events Map: A dynamic map showing all scheduled events worldwide.
Fans can go to /users
to see upcoming events on a dynamic map and view details like the date and description of each event. Artists can go to /artists
to see events they have scheduled, delete events, update events, and add new events. The website is also designed to be responsive on smaller screens.
For example, an artist can schedule a live performance, and fans can easily find and join the event through the global events map. This makes it easier for artists to promote their events and for fans to discover new music.
How I Built it
This project is built using the following technologies:
Framework: Next.js
UI Library: React
Database: MongoDB
API: Integration with Google Maps API
Deployment: Vercel
I used Next.js with React to create the pages, app layout, client components, and server components. I used TypeScript for type safety and to create interfaces. I used MongoDB to design the database event schema and model.
Next.js was chosen for its powerful features like server-side rendering and static site generation, which help improve performance and SEO. TypeScript ensures type safety and reduces runtime errors, while Tailwind CSS provides a utility-first approach to design, ensuring responsive and consistent styling.
Background and Technology Overview
Introduction to Next.js
Next.js is a powerful, open-source JavaScript framework built on top of React, created by Vercel (formerly known as ZEIT). It offers a robust set of features for building modern web applications, including server-side rendering (SSR), static site generation (SSG), and automatic code splitting. These capabilities make Next.js an excellent choice for developers looking to create high-performance, SEO-friendly web applications.
Key Features of Next.js Relevant to the Project
The "TikTok Concerts" project uses several important features of Next.js to build a dynamic, responsive, and high-performance web application. Here’s a detailed look at how these features are used:
App directory structure and nested routing
- The
app
directory provides a more intuitive and nested routing system. For example, routes for artists and users are organized within subdirectories, making the code easier to maintain.
- The
Server actions
Data Fetching: Instead of traditional API routes, server actions fetch data directly within components, reducing the need for client-side fetching and improving performance.
Data Mutations: Actions like creating, updating, or deleting events are handled server-side within the component's scope. This streamlines the logic and enhances security.
Server side rendering
In Next.js you can create server components to render UI on the server
Server components can improve performance by reducing the amount of client-side rendering
Server-side rendering helps with search engine optimization (SEO) because search engines can crawl and index content more effectively when it's rendered on the server.
Next.js router cache
Next.js Router Cache improves page navigation by storing the results of data fetching for each page.
Faster Navigation: Pages you have visited before load almost instantly when you revisit them, improving the user experience.
Efficient Data Management: Reduces unnecessary data fetching, improving overall application performance.
TypeScript Integration
TypeScript enhances code quality with type safety and clear data structures
Type Safety: Reduces runtime errors and improves development efficiency.
Interfaces and Types: Ensures consistency across the codebase.
Tailwind Integration
Tailwind CSS provides a utility-first approach to design, ensuring responsive and consistent styling across components
Responsive Design: The design adapts to different devices, ensuring a consistent and user-friendly experience.
Consistent Styling: Simplifies the styling process using reusable utility classes.
User Experience Design
UI/UX Principles for Engagement
Responsive Design: Ensures the website is accessible and user-friendly on different devices, enhancing the overall user experience.
User and Artist Pages: Separate pages for users and artists to manage and view events, offering a customized experience for each type of user.
Interactive Features: Includes elements like event details and descriptions to engage users and provide them with relevant information.
Development Process
Feel free to follow along with my code by visiting the GitHub repo.
Setting Up the Development Environment with Next.js
I started by setting up a new Next.js project with TypeScript support:
npx create-next-app@latest
From there, I answered the prompts in the terminal as follows:
What is your project named? tiktok-concerts
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use
src/
directory? YesWould you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/*)? No
After answering these prompts, your project will be configured as specified, and all the dependencies will be automatically installed.
Building the page routes
Within my src
directory, I created a new folder called app
to store all the page routes. The app
folder contains the following items:
- A CSS file that imports Tailwind, where you can also define custom CSS rules.
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
A home page
"use client"; import React from 'react'; /** * Home page component * * @returns The home page component. */ export default function Home() { return ( <main className="flex flex-col items-center gap-10 justify-between p-4 sm:p-8 md:p-16 lg:p-24"> <h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold my-4">Welcome to TikTok Concerts!</h1> <p className="text-sm sm:text-base md:text-lg lg:text-xl">Discover live streamed concerts and performances near you.</p> </main> ); }
An app layout that imports the CSS file, defines metadata for the homepage, and contains the content inherited throughout the rest of the app.
import type { Metadata } from "next";
import "./globals.css";
import React from "react";
import ToggleMenu from "@/components/ToggleMenu";
// Metadata for the layout.
export const metadata: Metadata = {
title: "TikTok Concerts",
description: "Watch your favorite artists perform live on TikTok."
};
/**
* Root layout component.
*
* @param children - The content to be rendered within the layout.
*
* @returns The layout component.
*/
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<div className="flex flex-col h-screen justify-between">
<ToggleMenu />
<main className="mb-auto p-4 h-fit">
{children}
</main>
<footer className="h-10 text-center">
<p>
Disclaimer: This project, TikTok Concerts, is not officially affiliated with, endorsed by, or sponsored by TikTok. All trademarks and copyrights are the property of their respective owners.
</p>
<p>© 2024 Sirus Salari. All rights reserved</p>
</footer>
</div>
</body>
</html>
);
}
A separate folder for each page, including
/about
,/artists
,/update
, and/users
Within each folder, there is a
page.tsx
file where you write out the HTML for the page, and import componentsAdditionally, within each folder, there is a
layout.tsx
file where you define metadata for that page and inherit the root app layout.
Creating the components
Within the src
directory, there's a folder called components
. Inside the components directory, I created the following components:
EventCard - A server component that effectively fetches, displays, and provides options to delete or update events in a user-friendly manner
import React from 'react'; import { deleteEvent, getEvents } from "@/lib/action"; /** * Get all events. * * @returns All events. */ export default async function GetEvents() { try { const events = await getEvents(); if (events.length === 0) { return <h1>No Events</h1>; } else { return ( <div className='grid grid-rows-1 md:grid-rows-2 lg:grid-rows-3 gap-5'> {events.map((event: any) => ( <div key={event._id} className='p-5 border rounded shadow-md'> <h3 className='text-center text-lg md:text-xl lg:text-2xl font-bold'>{event.title as string}</h3> <p className='text-sm md:text-base lg:text-lg'><b>Description: </b>{event.description as string}</p> <p className='text-sm md:text-base lg:text-lg'><b>Date: </b>{event.date as string}</p> <p className='text-sm md:text-base lg:text-lg'><b>Address: </b>{event.address as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.city as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.state as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.zip as string}</p> <form action={deleteEvent} className='m-4'> <input hidden type="text" name="id" defaultValue={event._id.toString()}/> <button type="submit" className='hover:bg-gradient-to-r from-splash to-razzmatazz hover:text-black inline-flex justify-center py-2 px-4 sm:px-6 md:px-8 lg:px-10 border border-transparent shadow-sm text-sm sm:text-base md:text-lg lg:text-xl font-medium rounded-md text-white'>Delete</button> </form> <form action="/update" className='m-4'> <input hidden type="text" name="id" defaultValue={event._id.toString()}/> <button className='hover:bg-gradient-to-r from-splash to-razzmatazz hover:text-black inline-flex justify-center py-2 px-4 sm:px-6 md:px-8 lg:px-10 border border-transparent shadow-sm text-sm sm:text-base md:text-lg lg:text-xl font-medium rounded-md text-white'>Update</button> </form> </div> ))} </div> ); } } catch (error) { console.log(error); } };
This component is used in the
/artists
page, and imports the server actions for deleting and fetching eventsFirst, a call is made to the getEvents server action. If no data is returned, the component will display HTML stating that there are no events.
If data is returned, each event in the array is mapped to a
div
with the key attribute set to the MongoDB ObjectID value of the event.Within the
div
, there are child elements to display the event's details, such as the title, description, date, address, city, state, and zip code.Additionally, each
div
contains two forms: a delete form that submits to the deleteEvent server action with the event's ObjectID as a hidden input, and an update form that redirects to /update with the event's ObjectID as a hidden input.Events are displayed in a responsive grid layout that adjusts the number of rows based on the screen size.
EventForm - A client component that provides a user-friendly interface for creating and submitting new events, with responsive design and consistent styling
"use client"; import { createEvent } from "@/lib/action"; import React, { useRef } from "react"; /** * Event form component. * * @returns The event form component. */ export default function EventForm() { // Create a reference to the form element const ref = useRef<HTMLFormElement>(null); const inputStyle = { color: 'black', backgroundColor: 'white', border: '1px solid black', padding: '8px', display: 'block', width: '100%', borderRadius: '5px', }; return ( <div className='container mx-auto px-4 sm:px-8 md:px-16 lg:px-24'> <h2 className='text-center m-4 text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold'>Add Event</h2> <form className='space-y-4' autoComplete="off" ref={ref} action={async (FormData) => { ref.current?.reset(); await createEvent(FormData); }}> <div> <label htmlFor="title" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Title</label> <input type="text" name="title" id='title' style={inputStyle} required /> </div> <div> <label htmlFor="description" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Description</label> <textarea id='description' name="description" style={inputStyle} required /> </div> <div> <label htmlFor="date" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Date</label> <input type="date" name="date" id='date' style={inputStyle} required /> </div> <div> <label htmlFor="address" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Address</label> <input type="text" name="address" id='address' style={inputStyle} required /> </div> <div> <label htmlFor="city" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>City</label> <input type="text" name="city" id='city' style={inputStyle} required /> </div> <div> <label htmlFor="state" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>State</label> <input type="text" name="state" id='state' style={inputStyle} required /> </div> <div> <label htmlFor="zip" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Zip</label> <input type="text" name="zip" id='zip' style={inputStyle} required /> </div> <button type='submit' className='hover:bg-gradient-to-r from-splash to-razzmatazz hover:text-black inline-flex justify-center py-2 px-4 sm:px-6 md:px-8 lg:px-10 border border-transparent shadow-sm text-sm sm:text-base md:text-lg lg:text-xl font-medium rounded-md text-white'>Submit</button> </form> </div> ); };
This component is used in the
/artists
page, and imports the server action for creating events, as well as the useRef React HookFirst, I created a reference to the form element with useRef, and this reference will be used later on to reset the form fields after submission
The component returns a form where the action attribute is set to an async function that first resets the form, then calls createEvent with the form data
The form includes several input fields for title, description, address, city, state, and zip.
EventList - A server component that effectively fetches and displays events in a user-friendly manner, ensuring a responsive and organized presentation of event details
import React from 'react'; import { getEvents } from "@/lib/action"; /** * Get all events. * * @returns All events. */ export default async function GetEvents() { try { // Get all events const events = await getEvents(); if (events.length === 0) { return <h1>No Events</h1>; } else { return ( <div className='grid grid-rows-1 md:grid-rows-2 lg:grid-rows-3 gap-5'> {events.map((event: any) => ( <div key={event._id} className='p-5 border rounded shadow-md'> <h3 className='text-lg text-center md:text-xl lg:text-2xl font-bold'>{event.title as string}</h3> <p className='text-sm md:text-base lg:text-lg'><b>Description:</b> {event.description as string}</p> <p className='text-sm md:text-base lg:text-lg'><b>Date:</b> {event.date as string}</p> <p className='text-sm md:text-base lg:text-lg'><b>Address:</b> {event.address as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.city as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.state as string}</p> <p className='text-sm md:text-base lg:text-lg'>{event.zip as string}</p> </div> ))} </div> ); } } catch (error) { console.log(error); } };
This component is used in the
/users
page, and imports the server action for fetching eventsFirst, a call is made to getEvents to fetch the list of events. If the array is empty, it returns HTML stating that there are no events
If there are events in the array, each event is mapped to a
div
with the key attribute set to the event's ObjectID, with child elements to display the title, description, date, address, city, state, and zip.The events are displayed in a responsive grid layout that adjusts the number of rows based on the screen size.
Map - A client component that provides a dynamic and interactive map that displays event locations with markers, enhancing the user experience with geocoding and responsive design
'use client' import React, { useCallback, useState } from 'react'; import { GoogleMap, Marker, useJsApiLoader} from '@react-google-maps/api'; import { Event } from '@/types/event'; const mapContainerStyle = { width: '100%', height: '100%' }; // Map component props interface MapProps { events: Event[]; } const nightModeStyles = [ { elementType: "geometry", stylers: [{ color: "#242f3e" }] }, { elementType: "labels.text.stroke", stylers: [{ color: "#242f3e" }] }, { elementType: "labels.text.fill", stylers: [{ color: "#746855" }] }, { featureType: "administrative.locality", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }], }, { featureType: "poi", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }], }, { featureType: "poi.park", elementType: "geometry", stylers: [{ color: "#263c3f" }], }, { featureType: "poi.park", elementType: "labels.text.fill", stylers: [{ color: "#6b9a76" }], }, { featureType: "road", elementType: "geometry", stylers: [{ color: "#38414e" }], }, { featureType: "road", elementType: "geometry.stroke", stylers: [{ color: "#212a37" }], }, { featureType: "road", elementType: "labels.text.fill", stylers: [{ color: "#9ca5b3" }], }, { featureType: "road.highway", elementType: "geometry", stylers: [{ color: "#746855" }], }, { featureType: "road.highway", elementType: "geometry.stroke", stylers: [{ color: "#1f2835" }], }, { featureType: "road.highway", elementType: "labels.text.fill", stylers: [{ color: "#f3d19c" }], }, { featureType: "transit", elementType: "geometry", stylers: [{ color: "#2f3948" }], }, { featureType: "transit.station", elementType: "labels.text.fill", stylers: [{ color: "#d59563" }], }, { featureType: "water", elementType: "geometry", stylers: [{ color: "#17263c" }], }, { featureType: "water", elementType: "labels.text.fill", stylers: [{ color: "#515c6d" }], }, { featureType: "water", elementType: "labels.text.stroke", stylers: [{ color: "#17263c" }], }, ]; /** * Map component. * * @param {MapProps} props Map component props. * @returns {JSX.Element} Map component. */ export default function Map({ events }: MapProps) { let geocoder: google.maps.Geocoder; const [geocodedEvents, setGeocodedEvents] = useState(events); /** * Geocode the events. * * @returns {void} * @throws {Error} Geocoding failed for address. * @returns {Promise<Event[]>} The geocoded events. */ const geocode = useCallback(() => { geocoder = new google.maps.Geocoder(); // Geocode the events const geocodePromises: Promise<Event>[] = events.map(event => new Promise((resolve, reject) => { const address = `${event.address}, ${event.city}, ${event.state} ${event.zip}`; geocoder.geocode({ address: address }, (results, status) => { if (status === 'OK' && results !== null) { event.latitude = results[0].geometry.location.lat().toString(); event.longitude = results[0].geometry.location.lng().toString(); resolve(event); } else { reject(new Error(`Geocoding failed for address: ${event.address}`)); } }); })); // Set the geocoded events Promise.all(geocodePromises) .then(geocodedEvents => { setGeocodedEvents(geocodedEvents); }) .catch(error => console.error(error)); }, [events]); // Load the Google Maps API const { isLoaded } = useJsApiLoader({ id: 'google-map-script', googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, }) return ( <div className='w-full h-64 sm:h-96 md:h-128 lg:h-256'> {isLoaded && <GoogleMap options={{ styles: nightModeStyles }} mapContainerStyle={mapContainerStyle} center={ { lat: parseFloat(geocodedEvents[0].latitude) || 0, lng: parseFloat(geocodedEvents[0].longitude) || 0 } } zoom={4} onLoad={geocode}> {geocodedEvents.map(event => ( <Marker key={event._id} position={{ lat: parseFloat(event.latitude), lng: parseFloat(event.longitude) }} /> ))} </GoogleMap>} </div> ); }
This component is used in the
/users
page, and imports the event interface, the useCallback and useState React Hooks, as well as the GoogleMap, Marker, and useJsApiLoader components from the react-google-maps/api libraryFirst, I defined the map component properties which is an array of events where each event is of type Event (as defined and imported by the event interface)
Next, I set up a function to geocode the addresses (the Google Maps API requires longitude and latitude data to place markers on a mp)
The geocode function uses the useCallback hook, and maps over the events array and creates a promise for each event to geocode its address
If geocoding is successful, it updates the event's latitude and longitude
If geocoding fails, it rejects the promise with an error
Promise.all
is used to wait for all geocoding promises to resolve, and the geocodedEvents state is updated with the resultsThe component then returns a
div
with responsive height classesIf the Google Maps API is loaded, it renders a GoogleMap component with the following properties
options: sets the map styles to nightModeStyles
mapContainerStyle: sets the container style to mapContainerStyle
center: sets the initial center of the map to the latitude and longitude of the first geocoded event
zoom: sets the initial zoom level to 4
onLoad: calls the geocode function when the map loads
Inside the GoogleMap component, it maps over the geocodedEvents array and renders a marker for each event at its geocoded position
ToggleMenu - A client component that provides a responsive and interactive header, enhancing the user experience with a mobile-friendly navigation menu
'use client'; import { useState, useEffect } from 'react'; import { usePathname } from 'next/navigation'; import { FaBars, FaTimes } from 'react-icons/fa'; import Link from 'next/link'; /** * Header component. * * @returns {JSX.Element} Header component. */ export default function Header () { const [isOpen, setIsOpen] = useState(false); const pathname = usePathname(); const toggleMenu = () => { setIsOpen(!isOpen); }; // Close the menu when the path changes useEffect(() => { setIsOpen(false); }, [pathname]); return ( <header className="bg-gradient-to-r from-splash to-razzmatazz text-black p-4"> <div className="container mx-auto flex justify-between items-center"> <h1 className="text-2xl font-bold">TikTok Concerts</h1> <div className="text-2xl lg:hidden" onClick={toggleMenu}> {isOpen ? <FaTimes /> : <FaBars />} </div> <nav className={`hidden lg:flex space-x-4`}> <Link href="/" className="hover:underline font-bold p-2">Home</Link> <Link href="/about" className="hover:underline font-bold p-2">About</Link> <Link href="/users" className="hover:underline font-bold p-2">Users</Link> <Link href="/artists" className="hover:underline font-bold p-2">Artists</Link> <Link target="_blank" href="https://github.com/sirus-the-beaver/tiktok-concerts" className="hover:underline font-bold p-2">GitHub Repo</Link> </nav> </div> {isOpen && ( <nav className="bg-gray-800 lg:hidden"> <ul className="flex flex-col items-center space-y-2 p-4"> <li><Link href="/" className="hover:underline font-bold block p-2">Home</Link></li> <li><Link href="/about" className="hover:underline font-bold block p-2">About</Link></li> <li><Link href="/users" className="hover:underline font-bold block p-2">Users</Link></li> <li><Link href="/artists" className="hover:underline font-bold block p-2">Artists</Link></li> <li><Link target="_blank" href="https://github.com/sirus-the-beaver/tiktok-concerts" className="hover:underline font-bold block p-2">GitHub Repo</Link></li> </ul> </nav> )} </header> ); };
This component is used in the root app layout, and imports the useState and useEffect React Hooks, the usePathname Next.js component, the FaBars and FaTimes React icons, and the Link React component
First, I set a state variable, isOpen, to false to manage the menu's open/close state
Next, I define the toggleMenu function which toggles the isOpen state between true and false
The useEffect hook is used to close the menu whenever the path changes (a user clicks a link to go to another page)
The component returns a header element with a
div
for the menu icon that toggles the menu on click, visible only on smaller screens, as well as anav
that is visible only on larger screensIf isOpen is true, a secondary
nav
is rendered for smaller screensThe header is responsive, with different navigation layouts for larger and smaller screens
UpdateEventForm - A client component that provides a user-friendly interface for updating an existing event, with responsive design and consistent styling
'use client'; import { updateEvent } from '@/lib/action'; import React, { useRef } from 'react'; import { redirect } from 'next/navigation'; /** * Update event form component. * * @returns The update event form component. */ export default function UpdateEventForm({eventId}: {eventId: string}) { const ref = useRef<HTMLFormElement>(null); const inputStyle = { color: 'black', backgroundColor: 'white', border: '1px solid black', padding: '8px', display: 'block', width: '100%', borderRadius: '5px', }; /** * Handle the form submission. * * @param FormData - The form data. * * @returns The updated event. */ const actionHandler = async (FormData: any) => { ref.current?.reset(); await updateEvent(FormData); redirect('/artists'); } return ( <div className='container mx-auto px-4 sm:px-8 md:px-16 lg:px-24'> <h2 className='text-center m-4 text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold'>Update Event</h2> <form autoComplete="off" className='space-y-4' ref={ref} action={actionHandler}> <input hidden type="text" name="id" id='id' defaultValue={eventId}/> <div> <label htmlFor="title" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Title</label> <input type="text" name="title" id='title' style={inputStyle}/> </div> <div> <label htmlFor="description" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Description</label> <textarea id='description' name="description" style={inputStyle} /> </div> <div> <label htmlFor="date" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Date</label> <input type="date" name="date" id='date' style={inputStyle} /> </div> <div> <label htmlFor="address" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Address</label> <input type="text" name="address" id='address' style={inputStyle} /> </div> <div> <label htmlFor="city" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>City</label> <input type="text" name="city" id='city' style={inputStyle} /> </div> <div> <label htmlFor="state" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>State</label> <input type="text" name="state" id='state' style={inputStyle} /> </div> <div> <label htmlFor="zip" className='block text-sm sm:text-base md:text-lg lg:text-xl font-medium text-gray-700'>Zip</label> <input type="text" name="zip" id='zip' style={inputStyle} /> </div> <button type='submit' className='hover:bg-gradient-to-r from-splash to-razzmatazz hover:text-black inline-flex justify-center py-2 px-4 sm:px-6 md:px-8 lg:px-10 border border-transparent shadow-sm text-sm sm:text-base md:text-lg lg:text-xl font-medium rounded-md text-white'>Update</button> </form> </div> ) }
This component is used in the
/update
page, and imports the server action for updating events, as well as the useRef React Hook, and the redirect function from Next.jsFirst, I defined a reference to the form with useRef, which will be used later on to reset the form after submission
Next, I define the actionHandler function which is an async function that handles the form submission. It resets the form fields, calls updateEvent with the form data, and then redirects the user to the
/artists
page after the event is updatedThe component returns a form with the action attribute set to the actionHandler function
The form includes several input fields including, ID (a hidden input field for the event ID, pre-filled with the eventId prop), title, description, date, address, city, state, and zip
The form fields are not marked as required, allowing for partial updates
Defining the server actions
Within my src
directory, I created another directory called lib
, where I added a file named action.ts
that contains the server actions.
'use server';
import Event from "@/models/Event";
import { revalidatePath } from "next/cache";
import { connectToDatabase } from "./db";
/**
* Get all events.
*
* @returns The events.
*/
export async function getEvents() {
await connectToDatabase();
try {
const data = await Event.find();
return JSON.parse(JSON.stringify(data));
} catch (error) {
console.log(error);
return [];
}
}
/**
* Create a new event.
*
* @param formData - The form data.
*
* @returns The new event.
*/
export async function createEvent(formData: FormData) {
// Connect to the database
await connectToDatabase();
// Get the form data
const title = formData.get('title') as string;
const description = formData.get('description') as string;
const date = formData.get('date') as string;
const address = formData.get('address') as string;
const city = formData.get('city') as string;
const state = formData.get('state') as string;
const zip = formData.get('zip') as string;
try {
// Create a new event
const newEvent = await Event.create({
title,
description,
date,
address,
city,
state,
zip
});
newEvent.save();
// Update the events page with the new event
revalidatePath('/artists');
revalidatePath('/users');
return newEvent.toString();
} catch (error) {
console.log(error);
return{message: 'Failed to create a new event'};
}
};
/**
* Delete an event.
*
* @param id - The event id.
*
* @returns The deleted event.
*/
export async function deleteEvent(id: FormData) {
await connectToDatabase();
const eventId = id.get('id') as string;
try {
await Event.deleteOne({ _id: eventId });
revalidatePath('/artists');
revalidatePath('/users');
return (`Successfully deleted event with id ${eventId}`);
} catch (error) {
return {message: 'Failed to delete event'};
}
};
/**
* Update an event.
*
* @param formData - The form data.
*
* @returns The updated event.
*/
export async function updateEvent(formData: FormData) {
await connectToDatabase();
const eventId = formData.get('id');
const event = await Event.findOne({ _id: eventId });
const formerTitle = event?.title;
const formerDescription = event?.description;
const formerDate = event?.date;
const formerAddress = event?.address;
const formerCity = event?.city;
const formerState = event?.state;
const formerZip = event?.zip;
try {
await Event.updateOne({ _id: eventId },
{
title: formData.get('title') === '' ? formerTitle : formData.get('title'),
description: formData.get('description') === '' ? formerDescription : formData.get('description'),
date: formData.get('date') === '' ? formerDate : formData.get('date'),
address: formData.get('address') === '' ? formerAddress : formData.get('address'),
city: formData.get('city') === '' ? formerCity : formData.get('city'),
state: formData.get('state') === '' ? formerState : formData.get('state'),
zip: formData.get('zip') === '' ? formerZip : formData.get('zip')
}
);
revalidatePath('/artists');
revalidatePath('/users');
return (`Successfully updated event with id ${eventId}`);
} catch (error) {
return {message: 'Failed to update event'};
}
};
I started by importing the database event model, the revalidatePath function from Next.js, and a function that I created to connect to the MongoDB database.
The file contains the following server actions:
getEvents
Fetches all events from the database
It first connects to the database, then tries to fetch all events
The fetched data is returned after converting it to JSON format
If an error occurs, it logs the error and returns an empty array
createEvent
Creates a new event in the database
It first connects to the database, then extracts form data from the formData object
Next, it tries to create a new event and saves it
After creating the event, it revalidates the paths
/artists
and/users
to purge cached data and update the events page (this step is necessary due to Next.js's client side router cache)
deleteEvent
Deletes an event from the database
It first connects to the database, then extracts the event's ObjectID from the id form data
It then tries to delete the event, and afterwards, it revalidates the paths
/artists
and/users
updateEvent
Updates an existing event in the database
It first connects to the database, then extracts the event's ObjectID from the formData object
Next, it fetches the current event details so that any empty fields remain the same
It then tries to update the event, only changing fields that are not empty in the form data
After updating the event, it revalidates the paths
/artists
and/users
Connecting to the database and designing the event schema and model
Within the lib
directory, I also created a file called db.ts
where I define my function to connect to the database.
import mongoose, { Connection } from 'mongoose';
// Cache the connection to avoid connecting to the database multiple times
let cachedConnection: Connection | null = null;
/**
* Connect to the database.
*
* @returns The connection to the database.
*/
export async function connectToDatabase() {
if (cachedConnection) {
return cachedConnection;
}
try {
const cnx = await mongoose.connect(process.env.MONGODB_URI!);
cachedConnection = cnx.connection;
return cachedConnection;
} catch (error) {
console.log(error);
throw error;
};
};
First, I imported the mongoose library. Then, I created a variable named cachedConnection
to store the database connection, preventing multiple reconnections.
The connectToDatabase function is an async function that first checks if cachedConnection is not null. If a cached connection exists, it returns the cached connection. Otherwise, it tries to connect to the MongoDB database and then caches the connection.
Within the src
directory, I created another directory called models
and added a file named Event.ts
. This is where I defined the event schema and model.
import mongoose, { Document, Model } from 'mongoose';
export interface IEvent {
title: string;
description: string;
date: string;
address: string;
city: string;
state: string;
zip: string;
}
export interface IEventDocument extends IEvent, Document {
_id: mongoose.Schema.Types.ObjectId;
createdAt: Date;
updatedAt: Date;
};
// Create a new schema for events
const eventSchema = new mongoose.Schema<IEventDocument>(
{
_id: { type: mongoose.Schema.Types.ObjectId, auto: true},
title: { type: String, required: true },
description: { type: String, required: true },
date: { type: String, required: true },
address: { type: String, required: true },
city: { type: String, required: true },
state: { type: String, required: true },
zip: { type: String, required: true }
},
{ timestamps: true }
);
// Create a new model for events
const Event: Model<IEventDocument> =
mongoose.models?.Event || mongoose.model('Event', eventSchema);
export default Event;
First, I imported the mongoose library. Then, I created a TypeScript interface that defines the structure of an event object.
Next, I created an interface that extends both IEvent and Document from mongoose which includes all properties from IEvent and adds _id (the ObjectID), createdAt, and updatedAt.
I then defined the eventSchema which is a mongoose schema for the IEventDocument interface. The schema defines the structure of the event documents in the database.
Finally, I created the Event model with the eventSchema, and check if a model named Event already exists to avoid redefining the model.
Challenges and Solutions
Handling authentication
Authentication was supposed to be handled with the TikTok Login Kit using NextAuth.js, but I faced configuration issues that caused errors related to missing tokens and issuer definitions. After troubleshooting and checking GitHub, I decided to skip authentication for the initial release.
Connection to the database
During development, I didn't have any issues connecting to the database. However, during deployment, I faced problems with server actions and database requests. The server wasn't fetching data correctly from the database, leading to session timeouts.
The issues were resolved by:
Ensuring the database connection was correctly established in each server action.
Revalidating pages after server actions to clear cached data.
Future Enhancements
Potential Features to Add
Looking ahead, I plan to add several features to the project:
TikTok Authentication: Reattempting integration with the TikTok Login Kit.
Enhanced Location Input: Allowing users to input just the name of a location rather than the full address.
Notification System: Implementing a system to notify fans of upcoming concerts.
Conclusion
Building the TikTok Concerts platform using Next.js has been an exciting and rewarding experience.
By leveraging the powerful features of Next.js, TypeScript, React, and Tailwind CSS, I was able to create a dynamic and responsive web application that aims to enhance the live concert experience on TikTok Live. Despite facing challenges with authentication, I successfully implemented key features such as event scheduling, a global events map, and user-friendly interfaces for both artists and fans.
Looking ahead, I plan to add more features like TikTok authentication, enhanced location input, and a notification system to further improve the platform.
This project not only showcases the potential of modern web technologies but also aims to foster greater engagement and interaction between artists and their fans on TikTok.
Call to action
I hope you enjoyed learning about my project, TikTok Concerts. If you find it interesting and believe it can make a difference in enhancing live concert experiences on TikTok, I would greatly appreciate your support.
Please visit my website to explore the platform and see it in action: TikTok Concerts.
Additionally, if you like my project, please consider voting for my submission in the TikTok TechJam hackathon. Your vote can help bring more visibility to the project and support its development. You can vote for my submission here: Vote for TikTok Concerts.
Thank you for your support!
Subscribe to my newsletter
Read articles from Sirus Salari directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Sirus Salari
Sirus Salari
Hello! My name is Sirus, I am currently enrolled at Oregon State University, and pursuing my B.S. in computer science. Before I even decided to pursue my B.S. in computer science, I had had some solid understanding in programming fundamentals from some courses I took during my first degree program, and also through self-teaching web development through Coursera, Udemy, and the Odin Project. If you're still reading, you're probably wondering what my first degree is in, and why I chose to pursue a second degree. I graduated from the University of California, Irvine in 2021 with a B.A. in psychology. When I started college, I had no idea what I wanted to do as a career, but I just knew that I was passionate about psychology. During my third year at UCI, I took a cognitive robotics course as part of my required classes for my major, and this was the first time I was ever exposed to coding, and my first time programming. I quickly fell in love with programming, and I decided to take an intro to programming course using Python during my fourth year at UCI. I continued to love and enjoy programming and learning about all the logic involved. Initially, my plan was to combine my interests for psychology and computer science into one by applying to PhD programs in an extremely niche field called "computational neuroscience". Essentially, this field focuses on mathematical/theoretical models of consciousness, the engineering of brain-computer-interfaces, and artificial intelligence/machine learning. I was denied from all 8 of the PhD programs I applied to. Instead of being defeated, I decided to continue pursuing my passion for programming. I started with just toying around with web development using HTML and CSS, then learned JavaScript, started making project websites, learned a lot from a Coursera course and Udemy course on web development, and learned a lot from following the Odin Project curriculum. Even though my passion and interest in programming never died, I found it hard to find the motivation to continue self-teaching. Ultimately, I still wanted to find a career that would involve programming. As a result, I started researching different options for people who wish to receive a second degree in computer science. I found Oregon State University as the perfect match for my needs, as it's entirely virtual while still having the same content as in-person classes, it's affordable compared to other options, and it allowed me to transfer all of my general education requirements from UCI, saving me tons of money and time on starting all over with another degree.