├── src └── app │ ├── globals.css │ ├── favicon.ico │ ├── events │ └── [slug] │ │ └── page.tsx │ ├── add-event │ └── page.tsx │ ├── page.tsx │ ├── layout.tsx │ ├── components │ ├── SearchResultsList.tsx │ ├── SearchResultCard.tsx │ ├── Calendar.tsx │ ├── CalendarNav.tsx │ ├── SearchBox.tsx │ ├── SearchPagination.tsx │ ├── Events.tsx │ ├── EventDetail.tsx │ ├── ImportEvent.tsx │ ├── CalendarGrid.tsx │ ├── Header.tsx │ ├── Footer.tsx │ └── AddEvent.tsx │ ├── api │ ├── feed.ics │ │ └── route.ts │ └── search │ │ └── route.ts │ ├── actions.ts │ └── search │ └── page.tsx ├── .vscode └── settings.json ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── tailwind.config.js ├── next.config.ts ├── lib ├── prisma.ts ├── types.ts ├── zod.ts ├── hash.ts ├── dates.ts └── db.ts ├── prisma └── schema.prisma ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── hooks └── useSearch.ts /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flite999/techpdx-calendar/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | 6 | } 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | fontFamily: { 5 | reddit: ['Reddit Mono', 'monospace'], 6 | }, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | turbopack: { 6 | root: __dirname, 7 | }, 8 | }; 9 | 10 | export default nextConfig; 11 | -------------------------------------------------------------------------------- /src/app/events/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import EventDetail from '@/app/components/EventDetail' 2 | 3 | export default async function Page({ 4 | params, 5 | }: { 6 | params: Promise<{ slug: string }> 7 | }) { 8 | const { slug } = await params 9 | return ( 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /src/app/add-event/page.tsx: -------------------------------------------------------------------------------- 1 | import AddEvent from '../components/AddEvent' 2 | import ImportEvent from '../components/ImportEvent' 3 | 4 | 5 | export default async function Page() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | let prisma: PrismaClient; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | prisma = new PrismaClient(); 7 | } else { 8 | const globalWithPrisma = global as typeof globalThis & { 9 | prisma: PrismaClient; 10 | }; 11 | if (!globalWithPrisma.prisma) { 12 | globalWithPrisma.prisma = new PrismaClient(); 13 | } 14 | prisma = globalWithPrisma.prisma; 15 | } 16 | 17 | export default prisma; -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | id: string 3 | title: string 4 | description?: string 5 | start_time: Date 6 | end_time: Date 7 | website?: string 8 | location?: string 9 | slug: string 10 | } 11 | 12 | export interface SearchResult { 13 | results: Event[] 14 | pagination: { 15 | page: number 16 | limit: number 17 | totalCount: number 18 | totalPages: number 19 | } 20 | } 21 | 22 | export interface SearchParams { 23 | q?: string 24 | page?: string 25 | } -------------------------------------------------------------------------------- /lib/zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const schema = z.object({ 4 | title: z.string({ message: 'Invalid Title' }), 5 | start_time: z.string({ message: 'Invalid Date' }), 6 | end_time: z.string({ message: 'Invalid Date' }), 7 | website: z.string({ message: 'Invalid Website' }), 8 | description: z.string({ message: 'Invalid Description' }), 9 | location: z.string({ message: 'Invalid Location' }) 10 | }) 11 | 12 | export const importSchema = z.object({ 13 | ics_url: z.string().url({ message: 'Invalid ICS URL' }) 14 | }) -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // schema.prisma 2 | 3 | generator client { 4 | provider = "prisma-client-js" 5 | } 6 | 7 | datasource db { 8 | provider = "postgresql" 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | model Event { 13 | id String @id @default(cuid()) 14 | slug String @unique 15 | createdAt DateTime @default(now()) @map(name: "created_at") 16 | updatedAt DateTime @updatedAt @map(name: "updated_at") 17 | title String 18 | start_time DateTime 19 | end_time DateTime 20 | website String? 21 | description String? 22 | location String? 23 | } 24 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: [ 16 | "node_modules/**", 17 | ".next/**", 18 | "out/**", 19 | "build/**", 20 | "next-env.d.ts", 21 | ], 22 | }, 23 | ]; 24 | 25 | export default eslintConfig; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic' // SSR each navigation 2 | export const revalidate = 0 3 | export const fetchCache = 'force-no-store' 4 | 5 | import Calendar from "./components/Calendar"; 6 | 7 | export default async function Home({ searchParams }: { searchParams: Promise<{ ym?: string }> }) { 8 | const sp = await searchParams 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import Header from "./components/Header"; 4 | import Footer from "./components/Footer"; 5 | 6 | export const metadata: Metadata = { 7 | title: "TechPDX Calendar", 8 | description: "TechPDX Calendar", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | {children} 25 |
26 |