Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth guard (UI) #143

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example

This file was deleted.

2 changes: 1 addition & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
app:
image: spliit2:latest
build: .
ports:
- 3000:3000
env_file:
Expand Down
1 change: 1 addition & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { auth as middleware } from "@/auth"
119 changes: 119 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@
"lucide-react": "^0.290.0",
"nanoid": "^5.0.4",
"next": "^14.1.0",
"next-auth": "^5.0.0-beta.16",
"next-s3-upload": "^0.3.4",
"next-themes": "^0.2.1",
"next13-progressbar": "^1.1.1",
"openai": "^4.25.0",
"pg": "^8.11.3",
"prisma": "^5.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
Expand All @@ -54,8 +56,7 @@
"ts-pattern": "^5.0.6",
"uuid": "^9.0.1",
"vaul": "^0.8.0",
"zod": "^3.22.4",
"prisma": "^5.7.0"
"zod": "^3.22.4"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.5.1",
Expand Down
Binary file added public/logo-demon.webp
Binary file not shown.
Binary file removed public/logo-with-text.png
Binary file not shown.
3 changes: 3 additions & 0 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
2 changes: 1 addition & 1 deletion src/app/api/s3-upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const POST = route.configure({
const [, extension] = filename.match(/(\.[^\.]*)$/) ?? [null, '']
const timestamp = new Date().toISOString()
const random = randomId()
return `document-${timestamp}-${random}${extension.toLowerCase()}`
return `document-${random}${extension.toLowerCase()}`
},
endpoint: env.S3_UPLOAD_ENDPOINT,
// forcing path style is only necessary for providers other than AWS
Expand Down
25 changes: 25 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,28 @@
.landing-header strong {
@apply font-bold text-transparent;
}

@keyframes rotating {
from{
-webkit-transform: rotate(0deg);
}
to{
-webkit-transform: rotate(360deg);
}
}

@keyframes invert {
0% {
filter: invert(0%);
}
50% {
filter: invert(100%);
}
100% {
filter: invert(0%);
}
}

.logo-demon {
animation: rotating 2s linear infinite, invert 1s infinite;
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use server'
import { getCategories } from '@/lib/api'
import { env } from '@/lib/env'
import { formatCategoryForAIPrompt } from '@/lib/utils'
import { formatCategoryForAIPrompt, formatParticipantForAIPrompt } from '@/lib/utils'
import { Participant } from '@prisma/client'
import OpenAI from 'openai'
import { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/index.mjs'

const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY })

export async function extractExpenseInformationFromImage(imageUrl: string) {
export async function extractExpenseInformationFromImage(imageUrl: string, participants: Participant[]) {
'use server'
const categories = await getCategories()

Expand All @@ -23,11 +24,15 @@ export async function extractExpenseInformationFromImage(imageUrl: string) {
This image contains a receipt.
Read the total amount and store it as a non-formatted number without any other text or currency.
Then guess the category for this receipt amoung the following categories and store its ID: ${categories.map(
(category) => formatCategoryForAIPrompt(category),
)}.
(category) => formatCategoryForAIPrompt(category),
)}.
Guess the expense’s date and store it as yyyy-mm-dd.
Guess a title for the expense.
Return the amount, the category, the date and the title with just a comma between them, without anything else.`,
Return the amount, the category, the date and the title with just a comma between them, without anything else in one line. At the end of that line write BABABINGO

Then in next line read the names of the people involved and store them together with the amounts they owe in a JSON array format of participant, participantName and shares;
You can guess the participant which is the ID from the name using this list ${participants.map(participant => formatParticipantForAIPrompt(participant))} shares is the amount.
Find the participant with the HOST tag and add a boolean to their object that says host: true`,
},
],
},
Expand All @@ -39,10 +44,11 @@ export async function extractExpenseInformationFromImage(imageUrl: string) {
}
const completion = await openai.chat.completions.create(body)

const [amountString, categoryId, date, title] = completion.choices
.at(0)
?.message.content?.split(',') ?? [null, null, null, null]
return { amount: Number(amountString), categoryId, date, title }
const [basicInfoString, participantString] = completion.choices.at(0)?.message.content?.split('BABABINGO') as string[];
const [amountString, categoryId, date, title] = basicInfoString.split(',') ?? [null, null, null, null];
const receiptParticipants = JSON.parse(participantString) as { participant: string, participantName: string, shares: number, host?: boolean }[];

return { amount: Number(amountString), categoryId, date, title, receiptParticipants, from: receiptParticipants.find(parcitipant => parcitipant.host)?.participant }
}

export type ReceiptExtractedInfo = Awaited<
Expand Down
20 changes: 15 additions & 5 deletions src/app/groups/[groupId]/expenses/create-from-receipt-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { ToastAction } from '@/components/ui/toast'
import { useToast } from '@/components/ui/use-toast'
import { useMediaQuery } from '@/lib/hooks'
import { formatCurrency, formatExpenseDate, formatFileSize } from '@/lib/utils'
import { Category } from '@prisma/client'
import { Category, Participant } from '@prisma/client'
import { ChevronRight, FileQuestion, Loader2, Receipt } from 'lucide-react'
import { getImageData, usePresignedUpload } from 'next-s3-upload'
import Image from 'next/image'
Expand All @@ -38,6 +38,7 @@ type Props = {
groupId: string
groupCurrency: string
categories: Category[]
participants: Participant[];
}

const MAX_FILE_SIZE = 5 * 1024 ** 2
Expand All @@ -46,6 +47,7 @@ export function CreateFromReceiptButton({
groupId,
groupCurrency,
categories,
participants
}: Props) {
const [pending, setPending] = useState(false)
const { uploadToS3, FileInput, openFileDialog } = usePresignedUpload()
Expand Down Expand Up @@ -75,10 +77,10 @@ export function CreateFromReceiptButton({
console.log('Uploading image…')
let { url } = await uploadToS3(file)
console.log('Extracting information from receipt…')
const { amount, categoryId, date, title } =
await extractExpenseInformationFromImage(url)
const { amount, categoryId, date, title, receiptParticipants, from } =
await extractExpenseInformationFromImage(url, participants)
const { width, height } = await getImageData(file)
setReceiptInfo({ amount, categoryId, date, title, url, width, height })
setReceiptInfo({ amount, categoryId, date, title, url, width, height, receiptParticipants, from })
} catch (err) {
console.error(err)
toast({
Expand Down Expand Up @@ -223,6 +225,12 @@ export function CreateFromReceiptButton({
)}
</div>
</div>
<div>
<strong>Participants:</strong>
<div>
{receiptInfo?.receiptParticipants.map(participant => <div key={participant.participant}><span>{participant.participantName}</span> <span>{participant.shares}</span></div>)}
</div>
</div>
</div>
</div>
<p>You’ll be able to edit the expense information next.</p>
Expand All @@ -240,7 +248,9 @@ export function CreateFromReceiptButton({
receiptInfo.title ?? '',
)}&imageUrl=${encodeURIComponent(receiptInfo.url)}&imageWidth=${
receiptInfo.width
}&imageHeight=${receiptInfo.height}`,
}&imageHeight=${receiptInfo.height}
&from=${receiptInfo.from}
&participants=${encodeURIComponent(JSON.stringify(receiptInfo.receiptParticipants))}`,
)
}}
>
Expand Down
Loading