From 92e22f9953841be3c200c414ec2a381609fa668b Mon Sep 17 00:00:00 2001 From: bardsley Date: Tue, 10 Sep 2024 15:32:25 +0100 Subject: [PATCH 1/8] Better layered SVG --- docs/mlf-colour-full.svg | 411 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 docs/mlf-colour-full.svg diff --git a/docs/mlf-colour-full.svg b/docs/mlf-colour-full.svg new file mode 100644 index 00000000..1a2baf96 --- /dev/null +++ b/docs/mlf-colour-full.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From adfdd9d182a34c9b2ba48cf74a038f06819807d3 Mon Sep 17 00:00:00 2001 From: Adam Bardsley Date: Tue, 10 Sep 2024 16:13:12 +0100 Subject: [PATCH 2/8] No environment crossover in Serverless --- functions/serverless.yml | 2 - functions/serverless.yml.live | 297 ---------------------------------- serverless.yml | 12 -- 3 files changed, 311 deletions(-) delete mode 100644 functions/serverless.yml.live delete mode 100644 serverless.yml diff --git a/functions/serverless.yml b/functions/serverless.yml index 7d2e5096..18a52c23 100644 --- a/functions/serverless.yml +++ b/functions/serverless.yml @@ -29,12 +29,10 @@ stages: deletionPolicy: Delete skipTables: true githubBranchDestination: "develop" - stripeSecretKey: ${param:testStripeSecretKey} prod: params: isProd: true deletionPolicy: Retain - stripeSecretKey: ${param:prodStripeSecretKey} githubBranchDestination: "main" diff --git a/functions/serverless.yml.live b/functions/serverless.yml.live deleted file mode 100644 index 426042ce..00000000 --- a/functions/serverless.yml.live +++ /dev/null @@ -1,297 +0,0 @@ -org: danceenginesystems # Needs a name for the organization -app: dance-engine # Need to see what connor thinks of name -service: api # Do we need multiple services? -frameworkVersion: '4.1' - -plugins: - - serverless-prune-plugin - -custom: - prune: - automatic: true - includeLayers: true - number: 10 - -package: - individually: true - -stages: - default: - observability: true - params: - attendeesTableName: "${sls:stage}-mlf-attendees" - stripeProductsTableName: "${sls:stage}-mlf-stripe-products" - isProd: false - prod: - params: - isProd: true - - -provider: - name: aws - runtime: nodejs18.x - memorySize: 128 - region: eu-west-1 - iam: - role: - statements: - - Effect: Allow - Action: - - dynamodb:Query - - dynamodb:Scan - - dynamodb:GetItem - - dynamodb:PutItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - Resource: - - Fn::GetAtt: [AttendeesTable, Arn] - - Fn::GetAtt: [StripeProductsTable, Arn] - - { "Fn::Join": [ "/", [ - { "Fn::GetAtt": ["AttendeesTable", "Arn" ] }, "index", "ticket_number-index" - ]]} - - Effect: Allow - Action: - - lambda:InvokeFunction - Resource: - - '*' - -layers: - dynamodb: - path: _layers/dynamodb/python - stripe: - path: _layers/stripe/python - sendmail: - path: _layers/sendmail/python - github: - path: _layers/github/python - -resources: - Resources: - AttendeesTable: - Type: AWS::DynamoDB::Table - DeletionPolicy: Retain - Properties: - DeletionProtectionEnabled: ${param:isProd} - PointInTimeRecoverySpecification: - PointInTimeRecoveryEnabled: ${param:isProd} - AttributeDefinitions: - - AttributeName: email - AttributeType: S - - AttributeName: ticket_number - AttributeType: S - KeySchema: - - AttributeName: email - KeyType: HASH - - AttributeName: ticket_number - KeyType: RANGE - BillingMode: PAY_PER_REQUEST - TableName: ${param:attendeesTableName} - GlobalSecondaryIndexes: - - IndexName: ticket_number-index - KeySchema: - - AttributeName: ticket_number - KeyType: HASH - Projection: - ProjectionType: 'ALL' - StripeProductsTable: - Type: AWS::DynamoDB::Table - DeletionPolicy: Retain - Properties: - DeletionProtectionEnabled: ${param:isProd} - AttributeDefinitions: - - AttributeName: prod_id - AttributeType: S - - AttributeName: price_id - AttributeType: S - KeySchema: - - AttributeName: prod_id - KeyType: HASH - - AttributeName: price_id - KeyType: RANGE - BillingMode: PAY_PER_REQUEST - TableName: ${param:stripeProductsTableName} - -functions: - Example: - handler: example/lambda_function.example - -# CardPayment: -# runtime: python3.11 -# handler: card_payment/lambda_function.lambda_handler -# name: "${sls:stage}-card_payment" -# package: -# patterns: -# - '!**/**' -# - "card_payment/**" -# environment: -# PRODUCTS_TABLE_NAME: ${param:stripeProductsTableName} -# layers: -# - !Ref DynamodbLambdaLayer -# events: -# - httpApi: -# path: /card_payment -# method: post - - # CheckoutComplete: - # handler: checkout_complete/lambda_function.lambda_handler - # name: "${sls:stage}-checkout_complete" - # package: - # patterns: - # - '!**/**' - # - "checkout_complete/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # STRIPE_SECRET_KEY: ${param:stripeSecretKey} - # layers: - # - !Ref DynamodbLambdaLayer - # - !Ref SendmailLambdaLayer - # events: - # - httpApi: - # path: /checkout_complete - # method: post - - # CreateTicket: - # handler: create_ticket/lambda_function.lambda_handler - # name: "${sls:stage}-create_ticket" - # package: - # patterns: - # - '!**/**' - # - "create_ticket/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # STRIPE_SECRET_KEY: ${param:stripeSecretKey} - # layers: - # - !Ref DynamodbLambdaLayer - - # CustomerPreferences: - # handler: customer_preferences/lambda_function.lambda_handler - # name: "${sls:stage}-customer-preferences" - # package: - # patterns: - # - '!**/**' - # - "customer_preferences/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # layers: - # - !Ref DynamodbLambdaLayer - # - !Ref GithubLambdaLayer - # events: - # - httpApi: - # path: /customer_preferences - # method: get - # - httpApi: - # path: /customer_preferences - # method: post - - # GeneratePricingUpdate: - # handler: gen_price_update/lambda_function.lambda_handler - # name: "${sls:stage}-gen_price_update" - # package: - # patterns: - # - '!**/**' - # - "gen_price_update/**" - # environment: - # PRODUCTS_TABLE_NAME: ${param:stripeProductsTableName} - # STRIPE_SECRET_KEY: ${param:stripeSecretKey} - # GITHUB_TOKEN: ${param:githubToken} - # layers: - # - !Ref StripeLambdaLayer - - # GetAttendees: - # handler: get_attendees/lambda_function.lambda_handler - # name: "${sls:stage}-get_attendees" - # package: - # patterns: - # - '!**/**' - # - "get_attendees/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # layers: - # - !Ref DynamodbLambdaLayer - # - !Ref StripeLambdaLayer - # events: - # - httpApi: - # path: /get_attendees - # method: get - - # ScanTicket: - # handler: scan_ticket/lambda_function.lambda_handler - # name: "${sls:stage}-scan_ticket" - # package: - # patterns: - # - '!**/**' - # - "scan_ticket/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # layers: - # - !Ref DynamodbLambdaLayer - # - !Ref GithubLambdaLayer - # events: - # - httpApi: - # path: /scan_ticket - # method: get - # - httpApi: - # path: /scan_ticket - # method: post - - # SendEmail: - # handler: send_email/lambda_function.lambda_handler - # name: "${sls:stage}-send_email" - # package: - # patterns: - # - '!**/**' - # - "send_email/**" - # environment: - # SENDGRID_API_KEY: ${param:sendgridApiKey} - - # SendTicket: - # handler: send_ticket/lambda_function.lambda_handler - # name: "${sls:stage}-send_ticket" - # package: - # patterns: - # - '!**/**' - # - "send_ticket/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # layers: - # - !Ref DynamodbLambdaLayer - # - !Ref SendmailLambdaLayer - # - !Ref GithubLambdaLayer - # events: - # - httpApi: - # path: /send_ticket - # method: post - - # StripePriceUpdate: - # handler: stripe_price_update/lambda_function.lambda_handler - # name: "${sls:stage}-stripe_price_update" - # package: - # patterns: - # - '!**/**' - # - "stripe_price_update/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # PRODUCTS_TABLE_NAME: ${param:stripeProductsTableName} - # STRIPE_SECRET_KEY: ${param:stripeSecretKey} - # layers: - # - !Ref DynamodbLambdaLayer - # events: - # - httpApi: - # path: /stripe_price_update - # method: post - - # TransferOwner: - # handler: transfer_owner/lambda_function.lambda_handler - # name: "${sls:stage}-transfer_owner" - # package: - # patterns: - # - '!**/**' - # - "transfer_owner/**" - # environment: - # ATTENDEES_TABLE_NAME: ${param:attendeesTableName} - # layers: - # - !Ref DynamodbLambdaLayer - # events: - # - httpApi: - # path: /transfer_owner - # method: post \ No newline at end of file diff --git a/serverless.yml b/serverless.yml deleted file mode 100644 index dd6e6e11..00000000 --- a/serverless.yml +++ /dev/null @@ -1,12 +0,0 @@ -org: danceenginesystems # Needs a name for the organization -app: dance-engine # Need to see what connor thinks of name -service: api # Do we need multiple services? -frameworkVersion: '4.1' - -provider: - name: aws - runtime: python3.11 - -functions: - hello: - handler: scan_ticket/lambda_function.lambda_handler From 57e8fa09cb92c693d93744f9f05029baa523767d Mon Sep 17 00:00:00 2001 From: Connor Monaghan Date: Tue, 10 Sep 2024 21:40:00 +0200 Subject: [PATCH 3/8] remove "not included" from description (#174) --- components/ticketing/PassCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ticketing/PassCard.tsx b/components/ticketing/PassCard.tsx index ad8711b3..cde3a63d 100644 --- a/components/ticketing/PassCard.tsx +++ b/components/ticketing/PassCard.tsx @@ -26,7 +26,7 @@ export const PassCard = ({passName, clickFunction, pass, priceModel, hasASaving, {basic ? null :

- {pass.description} {included ? "included" : "not included"} + {pass.description}

} From 028f7867f2d9eb6ea3e092831e6399815bab3c6c Mon Sep 17 00:00:00 2001 From: Adam Bardsley Date: Thu, 12 Sep 2024 17:55:04 +0100 Subject: [PATCH 4/8] Basic Permissions --- .gitignore | 2 +- app/admin/layout.tsx | 13 ++++++++++++ app/admin/page.tsx | 2 -- app/admin/scan/scan-client.tsx | 9 ++++++++ components/admin/authCheck.tsx | 19 +++++++++++++++++ components/admin/hub.tsx | 6 +++--- components/admin/ticketList.tsx | 4 +++- components/admin/userList.tsx | 9 ++++++-- lib/authorise.ts | 37 +++++++++++++++++++++++++++++++++ 9 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 app/admin/layout.tsx create mode 100644 components/admin/authCheck.tsx create mode 100644 lib/authorise.ts diff --git a/.gitignore b/.gitignore index ffef62da..765c606e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ .env.local .env.production .idea - +desktop.ini .vercel sendgrid.env diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx new file mode 100644 index 00000000..3ce43b06 --- /dev/null +++ b/app/admin/layout.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import AuthCheck from "@components/admin/authCheck"; + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode; +}) { + return + {children} + + +} \ No newline at end of file diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 1f76b7e3..06395542 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -27,8 +27,6 @@ export default async function AdminDashboardPage() {

Admin dashboard

- - diff --git a/app/admin/scan/scan-client.tsx b/app/admin/scan/scan-client.tsx index 765646be..b9464212 100644 --- a/app/admin/scan/scan-client.tsx +++ b/app/admin/scan/scan-client.tsx @@ -1,4 +1,6 @@ 'use client' +import { useUser } from "@clerk/clerk-react"; +import { authUsage } from "@lib/authorise"; import QrReader from "@components/admin/scan/QRReader" import { useEffect, useState } from "react" import ScanSuccessDialog from "@components/admin/scan/ScanSuccessDialog" @@ -7,6 +9,7 @@ import ScanSuccessDialog from "@components/admin/scan/ScanSuccessDialog" const ScanClient = () => { const [scannedResult, setScannedResult] = useState('') const [scannerActive, setScannerActive] = useState(true) + const { user, isLoaded } = useUser(); useEffect(() => { console.log("Client noticed change") @@ -14,7 +17,13 @@ const ScanClient = () => { setScannerActive(false) } }, [scannedResult]) + const debug = true + + if (!isLoaded) { return
Loading
} + if (!user) { return
Not logged in
} + if(!authUsage(user, "/admin/scan")) { return
Not authorised
} + return (
{debug diff --git a/components/admin/authCheck.tsx b/components/admin/authCheck.tsx new file mode 100644 index 00000000..ce39c9bf --- /dev/null +++ b/components/admin/authCheck.tsx @@ -0,0 +1,19 @@ +'use client' +import { useUser } from "@clerk/clerk-react"; +import { authUsage } from "@lib/authorise"; +import { usePathname } from 'next/navigation' +import { BiSolidHand } from "react-icons/bi"; + +const containerClasses = "flex w-full h-screen justify-center items-center" +const alertClasses = " border border-1 border-gray-500 rounded-md p-6 flex gap-3 items-center justify-center " + +export default function AuthCheck({children}) { + const { user, isLoaded } = useUser(); + const path = usePathname() + + if(path == '/admin') return children + if (!isLoaded) { return
Permission Checking...
} + if (!user) { return
Not Logged.
} + if(!authUsage(user, path)) { return
Not Authorised!
} + return children +} \ No newline at end of file diff --git a/components/admin/hub.tsx b/components/admin/hub.tsx index a52a8ef4..b5df7cb3 100644 --- a/components/admin/hub.tsx +++ b/components/admin/hub.tsx @@ -1,5 +1,6 @@ 'use client' import { useUser } from "@clerk/clerk-react"; +import { authUsage } from "@lib/authorise"; import { admin_ticketing_url } from "@lib/urls"; import { @@ -94,11 +95,10 @@ function classNames(...classes) { export default function Hub() { const { user, isLoaded } = useUser(); if (!isLoaded) { return
Loading
} - if (!user) { return
Not logged in
} - + if (!user) { return
Not logged in
} return user.publicMetadata.admin ? (
- {actions.map((action, actionIdx) => ( + {actions.filter((action) => authUsage(user,action.href)).map((action, actionIdx) => (
Loading...

} // else if (isValidating) { return

Validating...plz

} else if (error) { return

Error on fetch {JSON.stringify(error)}

} diff --git a/components/admin/userList.tsx b/components/admin/userList.tsx index f738ef93..67689f78 100644 --- a/components/admin/userList.tsx +++ b/components/admin/userList.tsx @@ -33,6 +33,7 @@ export default function UserList({loggedInUser}) { console.log("person:",person) const admin = person.metadata?.public?.admin; const title = person.metadata?.public?.title; + const roles = person.metadata?.public?.roles || []; const isMe = person.id === loggedInUser return (
  • Title
    {title}
    -
    Role
    +
    User Type
    + px-3 pb-1 pt-1 text-xs font-medium ml-3 ring-1 ring-inset `}> {admin ? "Admin" : "User"}
    +
    Roles
    +
    + {roles.join(", ")} +
  • diff --git a/lib/authorise.ts b/lib/authorise.ts new file mode 100644 index 00000000..2f8599a9 --- /dev/null +++ b/lib/authorise.ts @@ -0,0 +1,37 @@ +const superuser = "superadmin" +const grantUsage = { + "all-admins": ["/admin"], + "developer": ["#","/admin/users","/admin/stripe","/admin/import"], + "content-manager": ['/admin/content'], + "door-staff": ['/admin/ticketing','/admin/scan', '/admin/epos'], +} +// const grantView = { +// "developer": { +// "thing": ["create","update","read","delete"], +// "otherthing": ["create","update","read","delete"] +// } +// } + + +export const authUsage = (user,path) => { + // Check could ever be allowed to do anything + if(!authBasic(user)) return false + + const pathWithoutQueryString = path.split('?')[0] + const roles = user.publicMetadata.roles + // Superuser gets set to true always + if(roles.includes(superuser)) return true + // check through roles to see if any of them allow access to path + return roles.some((role) => { + if(!grantUsage[role]) return false // Roles doesn't exist in permissions + return grantUsage[role] && grantUsage[role].includes(pathWithoutQueryString) + }) +} + +const authBasic = (user) => { + // Make sure has metadata, admin and some roles + if(!user || !user.publicMetadata || !user.publicMetadata.admin || !user.publicMetadata.roles ) return false + // Does it have roles? + if(!Array.isArray(user.publicMetadata.roles)) return false + return true +} \ No newline at end of file From bcba9e47925c0afa63cc750bef83cd81602f3983 Mon Sep 17 00:00:00 2001 From: Adam Bardsley Date: Thu, 12 Sep 2024 21:33:16 +0100 Subject: [PATCH 5/8] Blank seating preferences finished --- app/api/preferences/route.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/preferences/route.ts b/app/api/preferences/route.ts index 5873df31..83ffcd32 100644 --- a/app/api/preferences/route.ts +++ b/app/api/preferences/route.ts @@ -6,9 +6,10 @@ export async function POST(request: Request) { const data = await request.formData() const ticket = cookies().get('ticket') const email = cookies().get('email') - // console.log(data) + console.log(data) const courseInfo = Array.from(data.entries()).filter((item)=>{ return /course/.test(item[0]) ? true : false }).map((item)=>{ return parseInt(item[1].toString()) }) const dietChoices = Array.from(data.entries()).filter((item)=>{ return /selected\[.*\]/.test(item[0]) ? true : false }).map((item)=>{ return item[0].replace('selected[','').replace(']','') }) + console.log("courseInfo", courseInfo) console.log("Diet",dietChoices) const apiRequestBody = { ticket_number: ticket.value, @@ -19,7 +20,7 @@ export async function POST(request: Request) { selected: [...dietChoices], other: data.get('other'), }, - seating_preference: data.get('seating_preference').toString().split(',').filter((item)=>{ return item.length > 0}), + seating_preference: data.get('seating_preference') ? data.get('seating_preference').toString().split(',').filter((item)=>{ return item.length > 0}) : [], } } console.log("POST -> Conor: ",apiRequestBody) From 3ed9df580e583299e23a212d657f383b0732a7c1 Mon Sep 17 00:00:00 2001 From: Adam Bardsley Date: Thu, 12 Sep 2024 21:44:10 +0100 Subject: [PATCH 6/8] Ticket emails use correct env for email links --- functions/send_email/lambda_function.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/functions/send_email/lambda_function.py b/functions/send_email/lambda_function.py index ef141f33..bb98b734 100644 --- a/functions/send_email/lambda_function.py +++ b/functions/send_email/lambda_function.py @@ -49,12 +49,13 @@ def generate_standard_ticket_body(data): with open("./send_email/ticket_body.html", "r") as body_file: body_tmpl = Template(body_file.read()) + subdomain = "www" if os.environ.get("STAGE_NAME") == "prod" else os.environ.get("STAGE_NAME") body = body_tmpl.substitute({ 'fullname':data['name'], 'email':data['email'], 'ticketnumber':data['ticket_number'], 'rows':rows, - 'ticket_link':"http://app.merseysidelatinfestival.co.uk/preferences?email={}&ticket_number={}".format(data['email'], data['ticket_number']), + 'ticket_link':"http://{}.merseysidelatinfestival.co.uk/preferences?email={}&ticket_number={}".format(subdomain,data['email'], data['ticket_number']), 'total_row':total_row, 'heading_message':data['heading_message'], }) From 10ffe104c8ccbeb56280134b516550e4a5903b77 Mon Sep 17 00:00:00 2001 From: Adam Bardsley Date: Wed, 18 Sep 2024 14:10:00 +0100 Subject: [PATCH 7/8] Header Restructure --- components/layout/layout.tsx | 2 +- components/nav/header.tsx | 161 ++-------------------------- components/nav/logo.tsx | 6 ++ components/nav/nav-mobile-items.tsx | 25 +++++ components/nav/nav-mobile.tsx | 64 +++++++---- 5 files changed, 83 insertions(+), 175 deletions(-) create mode 100644 components/nav/logo.tsx create mode 100644 components/nav/nav-mobile-items.tsx diff --git a/components/layout/layout.tsx b/components/layout/layout.tsx index babef881..5343895a 100644 --- a/components/layout/layout.tsx +++ b/components/layout/layout.tsx @@ -16,7 +16,7 @@ export default async function Layout({ children, rawPageData }: LayoutProps) { return ( -
    +
    - {/* */} - { - router.push(`${origPath}?draft=yup`) - }}> + {" "} @@ -81,14 +55,8 @@ export default function Header() { - + +
    - -
    - -
    - - {header.name} - - - -
    - - - -
    -
    +
    ); } - -// 'use client' - -// import { useState } from 'react' -// import { Dialog, DialogPanel } from '@headlessui/react' -// import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' - -// const navigation = [ -// { name: 'Product', href: '#' }, -// { name: 'Features', href: '#' }, -// { name: 'Marketplace', href: '#' }, -// { name: 'Company', href: '#' }, -// ] - -// export default function Example() { -// const [mobileMenuOpen, setMobileMenuOpen] = useState(false) - -// return ( -//
    -// -// -//
    -// -//
    -// -// Your Company -// -// -// -//
    -//
    -//
    -//
    -// {navigation.map((item) => ( -// -// {item.name} -// -// ))} -//
    -// -//
    -//
    -//
    -//
    -//
    -// ) -// } diff --git a/components/nav/logo.tsx b/components/nav/logo.tsx new file mode 100644 index 00000000..72841c86 --- /dev/null +++ b/components/nav/logo.tsx @@ -0,0 +1,6 @@ +'use client' +import MlfLogo from '@public/mlf-2.svg'; + +export default function Logo({className}) { + return ; +} \ No newline at end of file diff --git a/components/nav/nav-mobile-items.tsx b/components/nav/nav-mobile-items.tsx new file mode 100644 index 00000000..186449c8 --- /dev/null +++ b/components/nav/nav-mobile-items.tsx @@ -0,0 +1,25 @@ +'use client' +import { useSearchParams } from 'next/navigation' + +export default function NavMobileItems({ navs }: { navs: any }) { + const searchParams = useSearchParams() + const draft = searchParams.get('draft') + const filteredNavs = draft ? navs : navs.filter((item)=>{return item.visible}) + return ( +
    +
    +
    + {filteredNavs.map((item) => ( + + {item.label} { item.visible ? null : "draft"} + + ))} +
    +
    +
    + ) +} \ No newline at end of file diff --git a/components/nav/nav-mobile.tsx b/components/nav/nav-mobile.tsx index f0bf6369..13d1319d 100644 --- a/components/nav/nav-mobile.tsx +++ b/components/nav/nav-mobile.tsx @@ -1,25 +1,49 @@ 'use client' -import { useSearchParams } from 'next/navigation' -export default function NavMobile({ navs }: { navs: any }) { - const searchParams = useSearchParams() - const draft = searchParams.get('draft') - const filteredNavs = draft ? navs : navs.filter((item)=>{return item.visible}) +import React, { Suspense, useState } from "react"; +import Link from "next/link"; +import NavMobileItems from "./nav-mobile-items"; +import Logo from '@public/mlf-2.svg'; +import { Dialog, DialogPanel } from '@headlessui/react' +import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline' + +export default function NavMobile({ title, navs }: { title: string, navs: any}) { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + return ( -
    - + + + + + + ) -} \ No newline at end of file +} + From f51afce465a16b2e1be4a9e3bff84174b63ba590 Mon Sep 17 00:00:00 2001 From: bardsley Date: Tue, 24 Sep 2024 01:49:29 +0100 Subject: [PATCH 8/8] Feature/attendee-list (#182) * Date formatting safety and ordering * Live Stats --- app/admin/ticketing/page.tsx | 11 ++------- app/api/admin/attendees/route.ts | 6 ++--- components/admin/attendeeStats.tsx | 36 ++++++++++++++++++++++++++++ components/admin/lists/ticketRow.tsx | 3 ++- components/admin/statBlock.tsx | 1 - components/admin/ticket.tsx | 5 ++-- components/admin/ticketList.tsx | 3 ++- lib/useful.ts | 12 ++++++++++ 8 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 components/admin/attendeeStats.tsx diff --git a/app/admin/ticketing/page.tsx b/app/admin/ticketing/page.tsx index 1105cda1..33923037 100644 --- a/app/admin/ticketing/page.tsx +++ b/app/admin/ticketing/page.tsx @@ -3,7 +3,7 @@ import Layout from "@components/layout/layout"; import { Container } from "@components/layout/container"; import Navigation from "@components/admin/navigation"; import TicketList from "@components/admin/ticketList"; -import StatBlock, {StatLine} from "@components/admin/statBlock"; +import AttendeeStats from "@components/admin/attendeeStats"; import { admin_ticketing_url } from "@lib/urls"; export default async function AdminDashboardPage() { @@ -13,13 +13,6 @@ export default async function AdminDashboardPage() { { name: 'Ticketing', href: admin_ticketing_url, current: true }, ] - const stats = [ - { name: 'Total', value: '405', unit: 'tickets' }, - { name: 'Today', value: '10', unit: 'tickets' }, - { name: 'Meal prefs', value: '3', unit: 'set' }, - { name: 'Dinner Prefs', value: '2.5%', unit: 'complete' }, - ] as StatLine[] - return (
    {" "} @@ -32,7 +25,7 @@ export default async function AdminDashboardPage() {

    See recent sales, mark as attended and give pass etc

    - + diff --git a/app/api/admin/attendees/route.ts b/app/api/admin/attendees/route.ts index 7a163b45..cbd1ba8e 100644 --- a/app/api/admin/attendees/route.ts +++ b/app/api/admin/attendees/route.ts @@ -2,6 +2,7 @@ import { createClerkClient } from '@clerk/backend'; import { auth } from '@clerk/nextjs/server'; import { NextRequest } from 'next/server'; import { currentUser } from '@clerk/nextjs/server'; +import { guaranteeISOstringFromDate } from '@lib/useful'; export async function GET() { const {userId} = auth(); @@ -32,13 +33,12 @@ export async function GET() { const name_changed = attendee.transferred && attendee.transferred.ticket_number == attendee.ticket_number const transferred_in = !name_changed && attendee.history // console.log(attendee.full_name,attendee.ticket_number,attendee.transferred?.ticket_number, attendee.transferred?.ticket_number != attendee.ticket_number) - return { name: attendee.full_name, email: attendee.email, checkin_at: attendee.ticket_used, passes: attendee.line_items.map((item) => item.description), - purchased_at: new Date(parseInt(attendee.purchase_date) * 1000).toISOString(), + purchased_date: guaranteeISOstringFromDate(attendee.purchase_date), ticket_number: attendee.ticket_number, active: attendee.active, status: attendee.status, @@ -48,7 +48,7 @@ export async function GET() { name_changed: name_changed, transferred: attendee.transferred, history: attendee.history, - + meal_preferences: attendee.meal_preferences, }; }) diff --git a/components/admin/attendeeStats.tsx b/components/admin/attendeeStats.tsx new file mode 100644 index 00000000..c8db2219 --- /dev/null +++ b/components/admin/attendeeStats.tsx @@ -0,0 +1,36 @@ +'use client' +import useSWR from "swr"; +import { fetcher } from "@lib/fetchers"; +import StatBlock, { StatLine } from "@components/admin/statBlock"; +import { subWeeks } from 'date-fns' +import { guaranteeTimestampFromDate } from '@lib/useful'; + + +export default function AttendeeStats() { + const {data, error, isLoading} = useSWR("/api/admin/attendees", fetcher, { keepPreviousData: false }); + + const stats = isLoading ? + [ + { name: 'Total', value: 'Loading...', unit: '' }, + { name: 'Today', value: 'Loading...', unit: '' }, + { name: 'Meal prefs', value: 'Loading...', unit: '' }, + { name: 'Dinner Prefs', value: 'Loading...', unit: '' } + ] as StatLine[] + : error ? [ + { name: 'Total', value: "Error", unit: '' }, + { name: 'Today', value: 'Error', unit: '' }, + { name: 'Meal prefs', value: 'Error', unit: '' }, + { name: 'Dinner Prefs', value: 'Error', unit: '' } + ] as StatLine[] + : [ + { name: 'Total', value: data.attendees.length, unit: 'tickets' }, + { name: 'This week', value: data.attendees.filter((row)=>{ + return guaranteeTimestampFromDate(row.purchased_date) > subWeeks(new Date(),1).getTime() + }).length, unit: 'tickets' }, + { name: 'Meal prefs', value: data.attendees.filter((row)=>{return row.meal_preferences}).length, unit: 'set' }, + { name: 'Dinner Prefs', value: 'Unknown', unit: '' } + ] as StatLine[] + + return + // return
    {JSON.stringify(stats,null,2)}
    +} \ No newline at end of file diff --git a/components/admin/lists/ticketRow.tsx b/components/admin/lists/ticketRow.tsx index f8db5548..370dbcf8 100644 --- a/components/admin/lists/ticketRow.tsx +++ b/components/admin/lists/ticketRow.tsx @@ -24,7 +24,8 @@ export const TicketRow = ({attendee,setActiveTicket, setNameChangeModalActive, s {attendee.name}
    - #{attendee.ticket_number} + #{attendee.ticket_number}
    + {format(Date.parse(attendee.purchased_date),'h:mmaaa EEE do LLL yyyy')}
    Email
    diff --git a/components/admin/statBlock.tsx b/components/admin/statBlock.tsx index 15919e92..546ef9d5 100644 --- a/components/admin/statBlock.tsx +++ b/components/admin/statBlock.tsx @@ -9,7 +9,6 @@ export type StatLine = { export default function StatBlock({stats}: {stats: StatLine[]}) { const [hidden,setHidden] = useState(false); - const toggelButton =
    const statBlock = (
    diff --git a/components/admin/ticket.tsx b/components/admin/ticket.tsx index 14c89b84..be26f4f3 100644 --- a/components/admin/ticket.tsx +++ b/components/admin/ticket.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; import NameChangeModal from './modals/nameChangeModal'; import TicketTransferModal from './modals/ticketTransferModal'; import { fetcher } from "@lib/fetchers"; +import { guaranteeTimestampFromDate } from "@lib/useful"; const accessToThings = (access:number[],) => { let products = [] @@ -52,7 +53,7 @@ export default function TicketView({ticket_number, email}: {ticket_number: strin if(ticket) { const ticketUsage = ticket.active ? ticket.ticket_used ? `Used @ ${format(ticket.ticket_used,' eee')}` : "Active & Unused" : "Deactivated" - const purchaseData = fromUnixTime(ticket.purchase_date) + const purchaseDate = fromUnixTime(guaranteeTimestampFromDate(ticket.purchase_date) / 1000) const purchasedThings = ticket.line_items ? ticket.line_items.map((item) => {return `${item.description} £${item.amount_total / 100}`}): [] return ( data &&
    @@ -104,7 +105,7 @@ export default function TicketView({ticket_number, email}: {ticket_number: strin

    Purchase

    - + { ticket.promo_code ? : null } diff --git a/components/admin/ticketList.tsx b/components/admin/ticketList.tsx index 4d635eae..e312488f 100644 --- a/components/admin/ticketList.tsx +++ b/components/admin/ticketList.tsx @@ -85,6 +85,7 @@ export default function TicketList() { else { const sortedAttendees = attendees.sort((a, b) => { + // console.log("Sorting",sortByField,a,b) if (sortByDirection === 'desc') { return (0 - (a[sortByField] > b[sortByField] ? 1 : -1)) } else { @@ -120,7 +121,7 @@ export default function TicketList() { Name & Details - & Email + & Email diff --git a/lib/useful.ts b/lib/useful.ts index b82de024..1c9d5d71 100644 --- a/lib/useful.ts +++ b/lib/useful.ts @@ -1,3 +1,15 @@ export const deepCopy = (object: any) => { return JSON.parse(JSON.stringify(object)) +} + +export const guaranteeISOstringFromDate = (date: string | number) => { + return isNaN(Date.parse(date as string)) + ? new Date(parseInt(date as string) * 1000).toISOString() + : new Date(Date.parse(date as string)) +} + +export const guaranteeTimestampFromDate = (date: string | number) => { + return isNaN(Date.parse(date as string)) + ? parseInt(date as string) * 1000 + : Date.parse(date as string) } \ No newline at end of file