{"id":20349232,"url":"https://github.com/strapi/strapi-conf-next-template","last_synced_at":"2025-04-12T01:23:22.725Z","repository":{"id":65980251,"uuid":"358194312","full_name":"strapi/strapi-conf-next-template","owner":"strapi","description":null,"archived":false,"fork":false,"pushed_at":"2021-04-15T10:33:02.000Z","size":2743,"stargazers_count":23,"open_issues_count":0,"forks_count":9,"subscribers_count":12,"default_branch":"main","last_synced_at":"2025-03-20T17:04:07.368Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/strapi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null}},"created_at":"2021-04-15T09:02:36.000Z","updated_at":"2024-07-17T21:11:20.000Z","dependencies_parsed_at":"2023-02-19T19:15:56.231Z","dependency_job_id":null,"html_url":"https://github.com/strapi/strapi-conf-next-template","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strapi%2Fstrapi-conf-next-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strapi%2Fstrapi-conf-next-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strapi%2Fstrapi-conf-next-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/strapi%2Fstrapi-conf-next-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/strapi","download_url":"https://codeload.github.com/strapi/strapi-conf-next-template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248502252,"owners_count":21114773,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T22:24:46.340Z","updated_at":"2025-04-12T01:23:22.704Z","avatar_url":"https://github.com/strapi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Strapi Conf Next Template - based on the awesome [Virtual Event Starter Kit](https://vercel.com/virtual-event-starter-kit) by [Vercel](https://vercel.com/)!\n\n# Designed to work with [Strapi Conf Admin Template](https://github.com/strapi/strapi-conf-admin-template)\n\n### Demo: https://conf.strapi.io\n\nThis virtual event starter kit was used to run [StrapiConf](https://conf.strapi.io). It includes the following features:\n\n- Multiple stages with an embedded YouTube stream\n- Sponsor expo, including individual virtual booths and customizable download cards\n- Career Fair, allowing attendees to network and find job opportunties\n- Ticket registration and generation\n- Speaker pages and bios, and customizable download cards\n- Schedule\n\nThis platform is built upon three principles:\n\n- **Delegation:** Running a conference is difficult – you have to **delegate** tasks to third-parties to ensure success. Certain elements of an online conference experience are tough to get right, and we'd rather lean on established, industry leading solutions.\n- **Flexibility:** While delegating certain elements of the conference experience is helpful, it's also important to own the platform. That's why this template provides a **flexible** open-source codebase that can be modified for your event.\n- **Reducing Risk:** It's inevitable something will go wrong during your event. This platform **reduces risk** by leaning on a dynamic site that outputs as static files using [Incremental Static Generation](https://nextjs.org/docs/basic-features/data-fetching). These static files are cached, ensuring your site is never down. Then, it uses [API Routes](https://nextjs.org/docs/api-routes/introduction) to sprinkle dynamic content on top, which are hosted by a provider with 99.99% uptime.\n\n### Built With\n\n- Framework: [Next.js](https://nextjs.org)\n  - [CSS Modules](https://nextjs.org/docs/basic-features/built-in-css-support)\n  - [TypeScript](https://nextjs.org/docs/basic-features/typescript)\n- CMS: [Strapi](https://github.com/strapi/strapi-conf-admin-template)\n- Videos: [YouTube](https://www.youtube.com)\n- Deployment: [Vercel](https://vercel.com)\n- Authentication: [GitHub OAuth](https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps)\n- Database: [Redis](https://redis.io)\n\n## Running Locally\n\nFirst, set local environment variables.\n\n```\ncp .env.local.example .env.local\n```\n\nThen install packages and run the development server:\n\n```bash\nyarn install\nyarn dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\n## Clone and Deploy\n\nClick the button below to clone and deploy this template on [Vercel](https://vercel.com/). For this to work, you will need your API to be deployed to add the appropriate env variable in your Vercel config!\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fstrapi%2Fstrapi-conf-next-template)\n\n## Customize\n\nJust update your data and styling to get it to fit your needs!\n\n### Constants\n\n`lib/constants.ts` contains a list of variables you should customize.\n\n## Authentication and Database\n\nSome features won’t work until you set up authentication and database. The demo ([demo.vercel.events](https://demo.vercel.events)) uses [GitHub OAuth](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app) for authentication and [Redis](https://redis.io/) for database. You can use different providers as you see fit.\n\n### Authentication\n\nYou need to have GitHub OAuth set up to be able to customize the ticket after signing up on the registration form.\n\nFirst, create a [GitHub OAuth application](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-an-oauth-app) to use for authentication.\n\n- Set **Authorization Callback URL** as `\u003cyour domain\u003e/api/github-oauth`\n- After creating the OAuth app, create a **client secret**.\n\n#### Running Locally:\n\n- Set the Authorization Callback URL as `http://localhost:3000/api/github-oauth` on GitHub.\n- On `.env.local`, set `NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID` as the **Client ID** of the OAuth app.\n- Set `GITHUB_OAUTH_CLIENT_SECRET` as the **Client secret** of the OAuth app.\n- Finally, make sure the `NEXT_PUBLIC_SITE_ORIGIN` environment variable is set as `http://localhost:3000`. This is required to get the OAuth popup to work locally.\n- Restart the app (`yarn dev`) after editing `.env.local`.\n\nOnce it’s set up, sign up using the registration form on the home page (not on a stage page) and then click \"Generate with GitHub\".\n\n#### On Vercel:\n\n- Set the Authorization Callback URL as `\u003cyour deployment’s URL\u003e/api/github-oauth` on GitHub.\n- Set `NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET` on [Vercel Project Environment Variables Settings](https://vercel.com/docs/environment-variables) for the production environment.\n- Edit `SITE_URL` in `lib/constants.ts` to match your deployment’s URL (no trailing slash).\n- Push the code to redeploy the Project on Vercel.\n\n### Database\n\nYou need a database to save user data and enable the following features:\n\n- Generating a unique ticket number for each email when signing up on the registration form. If DB is not set up, it’ll always be `1234`.\n- Generating a unique ticket image or ticket URL after signing in with GitHub. If DB is not set up, each ticket image or URL will show generic data.\n\nThe demo ([demo.vercel.events](https://demo.vercel.events)) uses [Redis](https://redis.io/), but you can customize it to use any database you like.\n\n#### Running Redis Locally\n\n1. Install Redis locally and run it.\n2. Specify the following in `.env.local`:\n\n```\nREDIS_PORT=6379 # Default Redis port number\nREDIS_URL=localhost\nREDIS_PASSWORD=\nREDIS_EMAIL_TO_ID_SECRET=foo # Come up with your own secret string\n```\n\n\u003e `REDIS_EMAIL_TO_ID_SECRET` will be used to create a hash of the email address, which will be used for the Redis key for each user data (i.e. `id:\u003chash\u003e`). See `lib/redis.ts` for details.\n\n3. Restart the app (`yarn dev`) after editing `.env.local`.\n4. In a separate terminal window, start the Next.js dev server (`yarn dev`) and sign up using the registration form.\n5. In a separate terminal window, run Redis CLI, list keys (`keys *`) and inspect a `id:\u003chash\u003e` key (`hgetall id:\u003chash\u003e`). You should see the newly registered user.\n\n#### Using Redis On Vercel\n\nProvision your own Redis instance and set `REDIS_PORT`, `REDIS_URL`, `REDIS_PASSWORD`, and `REDIS_EMAIL_TO_ID_SECRET` (come up with your own secret string) on [Vercel Project Environment Variables Settings](https://vercel.com/docs/environment-variables) for the production environment.\n\n## More Details\n\n### Stages\n\nThe stages and navigation can be changed through the [admin](https://github.com/strapi/strapi-conf-admin-template). Feel free to update these based on your schedule. Each stage requires the user to enter their email to register with the conference before entering the event. After successfully entering their email and saving the user with your database of choice, the user is able to view the embedded YouTube stream. The login state is persisted as a `httponly` cookie.\n\nOne major feature of the conference platform is a near real-time sync with the CMS. Every five seconds, the stage queries `/api/stages` to fetch the latest information from the CMS. This allows you to make changes on the fly, without the user having the refresh the page. No need for websockets.\n\nThe primary use case for this is updating the YouTube embedded URL. Next.js Conf used this to seamlessly switch between pre-recorded videos running as a live premiere, and truly live content (e.g. panels). Plus, they had a few instances where our schedule needed to be tweaked on the fly. This implementation is fault tolerant, as well. The API route is properly cached and if the CMS was to somewhow go down, it won't break the page.\n\n### Schedule / Speaker Pages\n\nSchedule and speaker information is hosted in the CMS. The demo ([demo.vercel.events](https://demo.vercel.events)) is seeded with images from Unsplash and a placeholder schedule. This template is configured with actual [StrapiConf](https://conf.strapi.io) data. Each speaker has their own page with an image, bio, social media links, and information about their talk. The schedule is also shown as a sidebar on each corresponding stage.\n\n### Sponsor Expo\n\nIf you'd like to have your event sponsored, the Expo provides a platform to showcase sponsors with:\n\n- Their logo\n- Four call-to-action links\n- Embedded YouTube video\n- Link to chat room (Discord)\n- A downloadable sponsor presentation card\n\nFor Next.js Conf, they created a Discord channel for each sponsor.\n\n### Career Fair\n\nNetworking is vital for in-person conferences and replicating that environment virtually poses a challege. For the Career Fair, this starter provides the ability to list job postings, as well as an external link to talk with the company's recruiters on Discord.\n\n### Adding Discord Chat\n\nFor Next.js Conf, they used Discord for conference attendees to chat. On each stage, they showed a highlighted message from the corresponding Discord channel. If a user in the allow list used the camera emoji (📸) it would show the message on the stage.\n\nIf you'd like to add similar functionality to your conference, you can use the [API route](https://nextjs.org/docs/api-routes/introduction) below to fetch messages after creating a Discord bot. This API route is set up with the proper caching headers and ensures you won't get rate-limited with high traffic.\n\n```ts\nimport ms from 'ms';\nimport fetch, { Headers, RequestInit } from 'node-fetch';\nimport { NextApiRequest, NextApiResponse } from 'next';\n\ninterface Reaction {\n  emoji: { name: string };\n}\n\ninterface Message {\n  id: string;\n  channel_id: string;\n  content: string;\n  timestamp: string;\n  author: {\n    username: string;\n  };\n  reactions?: Reaction[];\n}\n\ninterface ReactionSelector {\n  id: string;\n}\n\n// After creating a bot, add the token as an environment var\nconst { DISCORD_BOT_TOKEN } = process.env;\n\n// Number of seconds to cache the API response for\nconst EXPIRES_SECONDS = 60;\n\n// Emoji that should be selected by a whitelisted user\n// in order for this API to return the message\nconst EMOJI = '🎥';\n\n// Whitelisted user IDs that are allowed to add the emoji to influence this API\nconst USERS = [\n  '752552204124291104' // username\n];\n\n// Discord base API URL\nconst API = 'https://discordapp.com/api/';\n\n// Map of Stage names to Discord channel IDs\nconst CHANNELS = new Map\u003cstring, string\u003e([\n  ['a', '769350098697191515'],\n  ['c', '769350352226877549'],\n  ['m', '769350396623192074'],\n  ['e', '769350429644685351']\n]);\n\nconst api = (url: string, opts: RequestInit = {}) =\u003e {\n  const headers = new Headers(opts.headers);\n  headers.set('Authorization', `Bot ${DISCORD_BOT_TOKEN}`);\n  headers.set('User-Agent', 'Discord Bot (https://yoursite.com/conf, v0.1)');\n\n  return fetch(`${API}${url}`, {\n    ...opts,\n    headers\n  });\n};\n\nasync function getReactionSelectors(\n  channelId: string,\n  messageId: string,\n  emoji: string\n): Promise\u003cReactionSelector[]\u003e {\n  const res = await api(\n    `channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`\n  );\n  if (!res.ok) {\n    throw new Error(`Failed to get message reactions: ${await res.text()} (${res.status})`);\n  }\n  return res.json();\n}\n\nasync function getLatestMessageWithEmoji(\n  messages: Message[],\n  emoji: string,\n  usersWhitelist: string[]\n) {\n  for (const message of messages) {\n    if (!message.content.trim()) {\n      // Empty message, ignore\n      // You could also filter messages here\n      continue;\n    }\n    for (const reaction of message.reactions || []) {\n      if (reaction.emoji.name === emoji) {\n        const selectors = await getReactionSelectors(message.channel_id, message.id, emoji);\n        const selector = selectors.find(r =\u003e usersWhitelist.includes(r.id));\n        if (selector) {\n          // The correct emoji was added from a whitelisted user\n          return { message, selector };\n        }\n      }\n    }\n  }\n}\n\nexport default async function getDiscordMessage(req: NextApiRequest, res: NextApiResponse) {\n  const { stage } = req.query;\n  if (typeof stage !== 'string') {\n    return res.status(400).json({ error: 'Query parameter \"stage\" must be a string' });\n  }\n\n  const channelId = CHANNELS.get(stage);\n  if (!channelId) {\n    return res.status(400).json({ error: `Invalid \"stage\": ${stage}` });\n  }\n\n  const apiRes = await api(`channels/${channelId}/messages`);\n  let messages: Message[] = [];\n  if (apiRes.status !== 429 \u0026\u0026 apiRes.ok) {\n    messages = await apiRes.json();\n  }\n\n  if (apiRes.status === 429) {\n    const reset = apiRes.headers.get('X-RateLimit-Reset-After') || 5;\n    res.setHeader(\n      'Cache-Control',\n      `s-maxage=${reset}, public, must-revalidate, stale-while-revalidate`\n    );\n  }\n\n  const messageToShow = await getLatestMessageWithEmoji(messages, EMOJI, USERS);\n  if (!messageToShow) {\n    return res.status(404).json({ error: 'Could not find message with emoji' });\n  }\n\n  const body = {\n    username: messageToShow.message.author.username,\n    content: messageToShow.message.content,\n    timestamp: messageToShow.message.timestamp\n  };\n\n  // Set caching headers\n  const expires = new Date(Date.now() + ms(`${EXPIRES_SECONDS}s`));\n  res.setHeader('Expires', expires.toUTCString());\n  res.setHeader(\n    'Cache-Control',\n    `s-maxage=${EXPIRES_SECONDS}, immutable, must-revalidate, stale-while-revalidate`\n  );\n\n  return res.status(200).json(body);\n}\n```\n\n### Demo\n\nThe demo is available at https://conf.strapi.io. The data recorded or used on the demo may be removed by Strapi at any point.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstrapi%2Fstrapi-conf-next-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstrapi%2Fstrapi-conf-next-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstrapi%2Fstrapi-conf-next-template/lists"}