Recreate schema to pass in uuid for courses/resources upfront, starting publishing classified, fixed nostr publishing

This commit is contained in:
austinkelsay 2024-03-20 19:42:28 -05:00
parent 7629d1c46b
commit 3113a9d0ee
9 changed files with 292 additions and 111 deletions

21
package-lock.json generated
View File

@ -24,7 +24,8 @@
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-typist": "^2.0.5", "react-typist": "^2.0.5",
"redux": "^5.0.1", "redux": "^5.0.1",
"rehype-raw": "^7.0.0" "rehype-raw": "^7.0.0",
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.11.21", "@types/node": "20.11.21",
@ -4498,6 +4499,14 @@
} }
} }
}, },
"node_modules/next-auth/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@ -6483,9 +6492,13 @@
"dev": true "dev": true
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }

View File

@ -25,7 +25,8 @@
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-typist": "^2.0.5", "react-typist": "^2.0.5",
"redux": "^5.0.1", "redux": "^5.0.1",
"rehype-raw": "^7.0.0" "rehype-raw": "^7.0.0",
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.11.21", "@types/node": "20.11.21",

View File

@ -1,10 +1,10 @@
-- CreateTable -- CreateTable
CREATE TABLE "User" ( CREATE TABLE "User" (
"id" SERIAL NOT NULL, "id" TEXT NOT NULL,
"pubkey" TEXT NOT NULL, "pubkey" TEXT NOT NULL,
"username" TEXT, "username" TEXT,
"avatar" TEXT, "avatar" TEXT,
"roleId" INTEGER, "roleId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
@ -13,7 +13,7 @@ CREATE TABLE "User" (
-- CreateTable -- CreateTable
CREATE TABLE "Role" ( CREATE TABLE "Role" (
"id" SERIAL NOT NULL, "id" TEXT NOT NULL,
"subscribed" BOOLEAN NOT NULL DEFAULT false, "subscribed" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id") CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
@ -21,10 +21,10 @@ CREATE TABLE "Role" (
-- CreateTable -- CreateTable
CREATE TABLE "Purchase" ( CREATE TABLE "Purchase" (
"id" SERIAL NOT NULL, "id" TEXT NOT NULL,
"userId" INTEGER NOT NULL, "userId" TEXT NOT NULL,
"courseId" INTEGER, "courseId" TEXT,
"resourceId" INTEGER, "resourceId" TEXT,
"amountPaid" INTEGER NOT NULL, "amountPaid" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL, "updatedAt" TIMESTAMP(3) NOT NULL,
@ -34,8 +34,8 @@ CREATE TABLE "Purchase" (
-- CreateTable -- CreateTable
CREATE TABLE "Course" ( CREATE TABLE "Course" (
"id" SERIAL NOT NULL, "id" TEXT NOT NULL,
"userId" INTEGER NOT NULL, "userId" TEXT NOT NULL,
"price" INTEGER NOT NULL DEFAULT 0, "price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT, "noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -46,9 +46,9 @@ CREATE TABLE "Course" (
-- CreateTable -- CreateTable
CREATE TABLE "Resource" ( CREATE TABLE "Resource" (
"id" SERIAL NOT NULL, "id" TEXT NOT NULL,
"userId" INTEGER NOT NULL, "userId" TEXT NOT NULL,
"courseId" INTEGER, "courseId" TEXT,
"price" INTEGER NOT NULL DEFAULT 0, "price" INTEGER NOT NULL DEFAULT 0,
"noteId" TEXT, "noteId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@ -8,7 +8,7 @@ generator client {
} }
model User { model User {
id Int @id @default(autoincrement()) id String @id @default(uuid())
pubkey String @unique pubkey String @unique
username String? @unique username String? @unique
avatar String? avatar String?
@ -16,35 +16,34 @@ model User {
courses Course[] // Relation field added for courses created by the user courses Course[] // Relation field added for courses created by the user
resources Resource[] // Relation field added for resources created by the user resources Resource[] // Relation field added for resources created by the user
role Role? @relation(fields: [roleId], references: [id]) role Role? @relation(fields: [roleId], references: [id])
roleId Int? roleId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Role { model Role {
id Int @id @default(autoincrement()) id String @id @default(uuid())
subscribed Boolean @default(false) subscribed Boolean @default(false)
users User[] users User[]
} }
model Purchase { model Purchase {
id Int @id @default(autoincrement()) id String @id @default(uuid())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
userId Int userId String
course Course? @relation(fields: [courseId], references: [id]) course Course? @relation(fields: [courseId], references: [id])
courseId Int? courseId String?
resource Resource? @relation(fields: [resourceId], references: [id]) resource Resource? @relation(fields: [resourceId], references: [id])
resourceId Int? resourceId String?
amountPaid Int // in satoshis amountPaid Int // in satoshis
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Course { model Course {
id Int @id @default(autoincrement()) id String @id // Client generates UUID
userId Int // Field added to link a course to a user userId String
user User @relation(fields: [userId], references: [id]) // Relation setup for user user User @relation(fields: [userId], references: [id])
price Int @default(0) price Int @default(0)
resources Resource[] resources Resource[]
purchases Purchase[] purchases Purchase[]
@ -54,11 +53,11 @@ model Course {
} }
model Resource { model Resource {
id Int @id @default(autoincrement()) id String @id // Client generates UUID
userId Int // Field added to link a resource to a user userId String
user User @relation(fields: [userId], references: [id]) // Relation setup for user user User @relation(fields: [userId], references: [id])
course Course? @relation(fields: [courseId], references: [id]) course Course? @relation(fields: [courseId], references: [id])
courseId Int? courseId String?
price Int @default(0) price Int @default(0)
purchases Purchase[] purchases Purchase[]
noteId String? @unique noteId String? @unique

View File

@ -1,9 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import axios from "axios";
import { InputText } from "primereact/inputtext"; import { InputText } from "primereact/inputtext";
import { InputNumber } from "primereact/inputnumber"; import { InputNumber } from "primereact/inputnumber";
import { InputSwitch } from "primereact/inputswitch"; import { InputSwitch } from "primereact/inputswitch";
import { Editor } from "primereact/editor"; import { Editor } from "primereact/editor";
import { Button } from "primereact/button"; import { Button } from "primereact/button";
import { nip04, verifyEvent, nip19 } from "nostr-tools";
import { useNostr } from "@/hooks/useNostr";
import { v4 as uuidv4 } from 'uuid';
import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
const ResourceForm = () => { const ResourceForm = () => {
@ -12,6 +17,11 @@ const ResourceForm = () => {
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [text, setText] = useState(''); const [text, setText] = useState('');
const [topics, setTopics] = useState(['']); // Initialize with an empty string to show one input by default
const [user] = useLocalStorageWithEffect('user', {});
const { publishAll } = useNostr();
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); // Prevents the default form submission mechanism e.preventDefault(); // Prevents the default form submission mechanism
@ -20,10 +30,83 @@ const ResourceForm = () => {
summary, summary,
isPaidResource: checked, isPaidResource: checked,
price: checked ? price : null, price: checked ? price : null,
content: text content: text,
topics: topics.map(topic => topic.trim().toLowerCase()) // Process topics as they are
}; };
console.log(payload); buildPaidResource(payload);
};
// For images, whether included in the markdown content or not, clients SHOULD use image tags as described in NIP-58. This allows clients to display images in carousel format more easily.
const buildPaidResource = async (payload) => {
// encrypt the content with NEXT_PUBLIC_APP_PRIV_KEY to NEXT_PUBLIC_APP_PUBLIC_KEY
const encryptedContent = await nip04.encrypt(process.env.NEXT_PUBLIC_APP_PRIV_KEY ,process.env.NEXT_PUBLIC_APP_PUBLIC_KEY, payload.content);
const newresourceId = uuidv4();
const event = {
kind: 30402,
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000),
tags: [
['title', payload.title],
['summary', payload.summary],
['t', ...topics],
['image', ''],
['d', newresourceId],
['location', `https://plebdevs.com/resource/${newresourceId}`],
['published_at', Math.floor(Date.now() / 1000).toString()],
['price', payload.price]
]
};
console.log('event:', event);
// Will need to add d tag from db id
// will need to add url/id as location tag
// first sign the event
const signedEvent = await window.nostr.signEvent(event);
const eventVerification = await verifyEvent(signedEvent);
console.log('eventVerification:', eventVerification);
console.log('signedEvent:', signedEvent);
const nAddress = nip19.naddrEncode({
pubkey: signedEvent.pubkey,
kind: signedEvent.kind,
identifier: newresourceId,
})
console.log('nAddress:', nAddress);
const userResponse = await axios.get(`/api/users/${user.pubkey}`)
console.log('userResponse:', userResponse);
const resourcePayload = {
id: newresourceId,
userId: userResponse.data.id,
price: payload.price || 0,
noteId: nAddress,
} }
const response = await axios.post(`/api/resources`, resourcePayload);
console.log('response:', response);
const publishResponse = await publishAll(signedEvent);
console.log('publishResponse:', publishResponse);
}
const handleTopicChange = (index, value) => {
const updatedTopics = topics.map((topic, i) => i === index ? value : topic);
setTopics(updatedTopics);
};
const addTopic = () => {
setTopics([...topics, '']); // Add an empty string to the topics array
};
const removeTopic = (index) => {
const updatedTopics = topics.filter((_, i) => i !== index);
setTopics(updatedTopics);
};
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -37,21 +120,31 @@ const ResourceForm = () => {
<div className="p-inputgroup flex-1 mt-8 flex-col"> <div className="p-inputgroup flex-1 mt-8 flex-col">
<p className="py-2">Paid Resource</p> <p className="py-2">Paid Resource</p>
<InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} /> <InputSwitch checked={checked} onChange={(e) => setChecked(e.value)} />
<div className="p-inputgroup flex-1 py-4">
{checked && ( {checked && (
<> <div className="p-inputgroup flex-1 py-4">
<i className="pi pi-bolt p-inputgroup-addon text-2xl text-yellow-500"></i>
<InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" /> <InputNumber value={price} onValueChange={(e) => setPrice(e.value)} placeholder="Price (sats)" />
</>
)}
</div> </div>
)}
</div> </div>
<div className="p-inputgroup flex-1 flex-col mt-8"> <div className="p-inputgroup flex-1 flex-col mt-8">
<span>Content</span> <span>Content</span>
<Editor value={text} onTextChange={(e) => setText(e.htmlValue)} style={{ height: '320px' }} /> <Editor value={text} onTextChange={(e) => setText(e.htmlValue)} style={{ height: '320px' }} />
</div> </div>
<div className="mt-8 flex-col w-full">
{topics.map((topic, index) => (
<div className="p-inputgroup flex-1" key={index}>
<InputText value={topic} onChange={(e) => handleTopicChange(index, e.target.value)} placeholder="Topic" className="w-full mt-2" />
{index > 0 && (
<Button icon="pi pi-times" className="p-button-danger mt-2" onClick={() => removeTopic(index)} />
)}
</div>
))}
<div className="w-full flex flex-row items-end justify-end py-2">
<Button icon="pi pi-plus" onClick={addTopic} />
</div>
</div>
<div className="flex justify-center mt-8"> <div className="flex justify-center mt-8">
<Button type="submit" severity="success" outlined label="Submit" /> <Button type="submit" label="Submit" className="p-button-success" />
</div> </div>
</form> </form>
); );

View File

@ -6,14 +6,14 @@ import { useImageProxy } from '@/hooks/useImageProxy';
import { Button } from 'primereact/button'; import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu'; import { Menu } from 'primereact/menu';
import useWindowWidth from '@/hooks/useWindowWidth'; import useWindowWidth from '@/hooks/useWindowWidth';
import useLocalStorage from '@/hooks/useLocalStorage'; import {useLocalStorageWithEffect} from '@/hooks/useLocalStorage';
import 'primereact/resources/primereact.min.css'; import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
import styles from '../navbar.module.css'; import styles from '../navbar.module.css';
const UserAvatar = () => { const UserAvatar = () => {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useLocalStorage('user', {}); const [user, setUser] = useLocalStorageWithEffect('user', {});
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth(); const windowWidth = useWindowWidth();
@ -35,7 +35,6 @@ const UserAvatar = () => {
if (!isClient) { if (!isClient) {
return null; // Or return a loader/spinner/placeholder return null; // Or return a loader/spinner/placeholder
} else if (user && Object.keys(user).length > 0) { } else if (user && Object.keys(user).length > 0) {
console.log('ahhhhh s:', user);
// User exists, show username or pubkey // User exists, show username or pubkey
const displayName = user.username || user.pubkey.slice(0, 10) + '...'; const displayName = user.username || user.pubkey.slice(0, 10) + '...';

View File

@ -1,28 +1,16 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
// This version of the hook initializes state without immediately attempting to read from localStorage
function useLocalStorage(key, initialValue) { function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => { const [storedValue, setStoredValue] = useState(initialValue);
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
// Added a check to ensure the item is not only present but also a valid JSON string.
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
// Consider removing or correcting the invalid item in localStorage here.
window.localStorage.removeItem(key); // Optional: remove the item that caused the error.
return initialValue; // Revert to initial value if parsing fails.
}
});
// Function to update localStorage and state
const setValue = value => { const setValue = value => {
try { try {
const valueToStore = value instanceof Function ? value(storedValue) : value; const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore); setStoredValue(valueToStore); // Update state
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore)); window.localStorage.setItem(key, JSON.stringify(valueToStore)); // Update localStorage
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@ -32,4 +20,26 @@ function useLocalStorage(key, initialValue) {
return [storedValue, setValue]; return [storedValue, setValue];
} }
// Custom hook to handle fetching and setting data from localStorage
export function useLocalStorageWithEffect(key, initialValue) {
const [storedValue, setStoredValue] = useLocalStorage(key, initialValue);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
const item = window.localStorage.getItem(key);
// Only update if the item exists to prevent overwriting the initial value with null
if (item !== null) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.log(error);
}
}, [key]); // Dependencies array ensures this runs once on mount
return [storedValue, setStoredValue];
}
export default useLocalStorage; export default useLocalStorage;

View File

@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { SimplePool } from "nostr-tools"; import { SimplePool, nip19, verifyEvent } from "nostr-tools";
import { Relay } from 'nostr-tools/relay'
const initialRelays = [ const initialRelays = [
"wss://nos.lol/", "wss://nos.lol/",
@ -140,15 +141,77 @@ export const useNostr = () => {
}); });
} }
const publishEvent = async (event) => { const publishEvent = async (relay, signedEvent) => {
console.log('publishing event to', relay);
return new Promise((resolve, reject) => {
const timeout = 3000
const wsRelay = new window.WebSocket(relay)
let timer
let isMessageSentSuccessfully = false
function timedout () {
clearTimeout(timer)
wsRelay.close()
reject(new Error(`relay timeout for ${relay}`))
}
timer = setTimeout(timedout, timeout)
wsRelay.onopen = function () {
clearTimeout(timer)
timer = setTimeout(timedout, timeout)
wsRelay.send(JSON.stringify(['EVENT', signedEvent]))
}
wsRelay.onmessage = function (msg) {
const m = JSON.parse(msg.data)
if (m[0] === 'OK') {
isMessageSentSuccessfully = true
clearTimeout(timer)
wsRelay.close()
console.log('Successfully sent event to', relay)
resolve()
}
}
wsRelay.onerror = function (error) {
clearTimeout(timer)
console.log(error)
reject(new Error(`relay error: Failed to send to ${relay}`))
}
wsRelay.onclose = function () {
clearTimeout(timer)
if (!isMessageSentSuccessfully) {
reject(new Error(`relay error: Failed to send to ${relay}`))
}
}
})
};
const publishAll = async (signedEvent) => {
try { try {
const publishPromises = pool.current.publish(relays, event); const promises = relays.map(relay => publishEvent(relay, signedEvent));
await Promise.all(publishPromises); const results = await Promise.allSettled(promises)
const successfulRelays = []
const failedRelays = []
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
successfulRelays.push(relays[i])
} else {
failedRelays.push(relays[i])
}
})
return { successfulRelays, failedRelays }
} catch (error) { } catch (error) {
console.error("Error publishing event:", error); console.error('Error publishing event:', error);
} }
}; };
useEffect(() => { useEffect(() => {
getRelayStatuses(); // Get initial statuses on mount getRelayStatuses(); // Get initial statuses on mount
@ -164,7 +227,7 @@ export const useNostr = () => {
return { return {
updateRelays, updateRelays,
fetchSingleEvent, fetchSingleEvent,
publishEvent, publishAll,
fetchKind0, fetchKind0,
fetchResources, fetchResources,
fetchCourses, fetchCourses,

View File

@ -1,16 +1,17 @@
import React, {useRef} from "react"; import React, { useRef, useState, useEffect } from 'react';
import { Button } from "primereact/button"; import { Button } from "primereact/button";
import { DataTable } from 'primereact/datatable'; import { DataTable } from 'primereact/datatable';
import { Menu } from 'primereact/menu'; import { Menu } from 'primereact/menu';
import { Column } from 'primereact/column'; import { Column } from 'primereact/column';
import { useImageProxy } from "@/hooks/useImageProxy"; import { useImageProxy } from "@/hooks/useImageProxy";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useLocalStorage from "@/hooks/useLocalStorage"; import { useLocalStorageWithEffect } from "@/hooks/useLocalStorage";
import MenuTab from "@/components/menutab/MenuTab"; import MenuTab from "@/components/menutab/MenuTab";
import Image from "next/image"; import Image from "next/image";
const Profile = () => { const Profile = () => {
const [user, setUser] = useLocalStorage('user', {}); const [activeIndex, setActiveIndex] = useState(0);
const [user, setUser] = useLocalStorageWithEffect('user', {});
const { returnImageProxy } = useImageProxy(); const { returnImageProxy } = useImageProxy();
const menu = useRef(null); const menu = useRef(null);
const router = useRouter(); const router = useRouter();
@ -74,6 +75,7 @@ const Profile = () => {
return ( return (
user && (
<div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]"> <div className="w-[90vw] mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
<div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]"> <div className="w-[85vw] flex flex-col justify-center mx-auto max-tab:w-[100vw] max-mob:w-[100vw]">
<div className="relative flex w-full items-center justify-center"> <div className="relative flex w-full items-center justify-center">
@ -98,7 +100,7 @@ const Profile = () => {
<Button label="Subscribe" className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]" /> <Button label="Subscribe" className="p-button-raised p-button-success w-auto my-2 text-[#f8f8ff]" />
</div> </div>
</div> </div>
<DataTable emptyMessage="No purchases" value={purchases} tableStyle={{ minWidth: '100%'}} header={header}> <DataTable emptyMessage="No purchases" value={purchases} tableStyle={{ minWidth: '100%' }} header={header}>
<Column field="cost" header="Cost"></Column> <Column field="cost" header="Cost"></Column>
<Column field="name" header="Name"></Column> <Column field="name" header="Name"></Column>
<Column field="category" header="Category"></Column> <Column field="category" header="Category"></Column>
@ -108,11 +110,12 @@ const Profile = () => {
<h2 className="text-center my-4">Your Content</h2> <h2 className="text-center my-4">Your Content</h2>
</div> </div>
<div className="flex flex-row w-full justify-between px-4"> <div className="flex flex-row w-full justify-between px-4">
<MenuTab items={homeItems} /> <MenuTab items={homeItems} activeIndex={activeIndex} onTabChange={setActiveIndex} />
<Button onClick={() => router.push('/create')} label="Create" severity="success" outlined className="mt-2" /> <Button onClick={() => router.push('/create')} label="Create" severity="success" outlined className="mt-2" />
</div> </div>
</div> </div>
) )
)
} }
export default Profile export default Profile