mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9052c9f37a |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.2.4
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
* admin can select user to view coins for that user
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* fix disable password in demo instance (#74)
|
||||||
|
|
||||||
## Version 0.2.3
|
## Version 0.2.3
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export async function loadCoinsData(): Promise<CoinsData> {
|
|||||||
const data = await loadData<CoinsData>('coins')
|
const data = await loadData<CoinsData>('coins')
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
transactions: data.transactions.filter(x => x.userId === user.id)
|
transactions: user.isAdmin ? data.transactions : data.transactions.filter(x => x.userId === user.id)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return getDefaultCoinsData()
|
return getDefaultCoinsData()
|
||||||
@@ -194,7 +194,7 @@ export async function loadCoinsData(): Promise<CoinsData> {
|
|||||||
|
|
||||||
export async function saveCoinsData(data: CoinsData): Promise<void> {
|
export async function saveCoinsData(data: CoinsData): Promise<void> {
|
||||||
const user = await getCurrentUser()
|
const user = await getCurrentUser()
|
||||||
|
|
||||||
// Create clones of the data
|
// Create clones of the data
|
||||||
const newData = _.cloneDeep(data)
|
const newData = _.cloneDeep(data)
|
||||||
newData.transactions = newData.transactions.map(transaction => ({
|
newData.transactions = newData.transactions.map(transaction => ({
|
||||||
@@ -219,12 +219,14 @@ export async function addCoins({
|
|||||||
type = 'MANUAL_ADJUSTMENT',
|
type = 'MANUAL_ADJUSTMENT',
|
||||||
relatedItemId,
|
relatedItemId,
|
||||||
note,
|
note,
|
||||||
|
userId,
|
||||||
}: {
|
}: {
|
||||||
amount: number
|
amount: number
|
||||||
description: string
|
description: string
|
||||||
type?: TransactionType
|
type?: TransactionType
|
||||||
relatedItemId?: string
|
relatedItemId?: string
|
||||||
note?: string
|
note?: string
|
||||||
|
userId?: string
|
||||||
}): Promise<CoinsData> {
|
}): Promise<CoinsData> {
|
||||||
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
|
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
|
||||||
const data = await loadCoinsData()
|
const data = await loadCoinsData()
|
||||||
@@ -235,7 +237,8 @@ export async function addCoins({
|
|||||||
description,
|
description,
|
||||||
timestamp: d2t({ dateTime: getNow({}) }),
|
timestamp: d2t({ dateTime: getNow({}) }),
|
||||||
...(relatedItemId && { relatedItemId }),
|
...(relatedItemId && { relatedItemId }),
|
||||||
...(note && note.trim() !== '' && { note })
|
...(note && note.trim() !== '' && { note }),
|
||||||
|
userId: userId || await getCurrentUserId()
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData: CoinsData = {
|
const newData: CoinsData = {
|
||||||
@@ -270,12 +273,14 @@ export async function removeCoins({
|
|||||||
type = 'MANUAL_ADJUSTMENT',
|
type = 'MANUAL_ADJUSTMENT',
|
||||||
relatedItemId,
|
relatedItemId,
|
||||||
note,
|
note,
|
||||||
|
userId,
|
||||||
}: {
|
}: {
|
||||||
amount: number
|
amount: number
|
||||||
description: string
|
description: string
|
||||||
type?: TransactionType
|
type?: TransactionType
|
||||||
relatedItemId?: string
|
relatedItemId?: string
|
||||||
note?: string
|
note?: string
|
||||||
|
userId?: string
|
||||||
}): Promise<CoinsData> {
|
}): Promise<CoinsData> {
|
||||||
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
|
await verifyPermission('coins', type === 'MANUAL_ADJUSTMENT' ? 'write' : 'interact')
|
||||||
const data = await loadCoinsData()
|
const data = await loadCoinsData()
|
||||||
@@ -286,7 +291,8 @@ export async function removeCoins({
|
|||||||
description,
|
description,
|
||||||
timestamp: d2t({ dateTime: getNow({}) }),
|
timestamp: d2t({ dateTime: getNow({}) }),
|
||||||
...(relatedItemId && { relatedItemId }),
|
...(relatedItemId && { relatedItemId }),
|
||||||
...(note && note.trim() !== '' && { note })
|
...(note && note.trim() !== '' && { note }),
|
||||||
|
userId: userId || await getCurrentUserId()
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData: CoinsData = {
|
const newData: CoinsData = {
|
||||||
@@ -478,6 +484,6 @@ export async function deleteUser(userId: string): Promise<void> {
|
|||||||
|
|
||||||
export async function loadServerSettings(): Promise<ServerSettings> {
|
export async function loadServerSettings(): Promise<ServerSettings> {
|
||||||
return {
|
return {
|
||||||
isDemo: !!process.env.NEXT_PUBLIC_DEMO,
|
isDemo: !!process.env.DEMO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { TransactionNoteEditor } from './TransactionNoteEditor'
|
|||||||
import { useHelpers } from '@/lib/client-helpers'
|
import { useHelpers } from '@/lib/client-helpers'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
|
const { currentUser } = useHelpers()
|
||||||
|
const [selectedUser, setSelectedUser] = useState<string>()
|
||||||
const {
|
const {
|
||||||
add,
|
add,
|
||||||
remove,
|
remove,
|
||||||
@@ -28,14 +30,13 @@ export default function CoinsManager() {
|
|||||||
totalSpent,
|
totalSpent,
|
||||||
coinsSpentToday,
|
coinsSpentToday,
|
||||||
transactionsToday
|
transactionsToday
|
||||||
} = useCoins()
|
} = useCoins({selectedUser})
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
const DEFAULT_AMOUNT = '0'
|
const DEFAULT_AMOUNT = '0'
|
||||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||||
const [pageSize, setPageSize] = useState(50)
|
const [pageSize, setPageSize] = useState(50)
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const { currentUser } = useHelpers()
|
|
||||||
|
|
||||||
const [note, setNote] = useState('')
|
const [note, setNote] = useState('')
|
||||||
|
|
||||||
@@ -62,7 +63,22 @@ export default function CoinsManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Coins Management</h1>
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold mr-6">Coins Management</h1>
|
||||||
|
{currentUser?.isAdmin && (
|
||||||
|
<select
|
||||||
|
className="border rounded p-2"
|
||||||
|
value={selectedUser}
|
||||||
|
onChange={(e) => setSelectedUser(e.target.value)}
|
||||||
|
>
|
||||||
|
{usersData.users.map(user => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.username}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
|||||||
id="disable-password"
|
id="disable-password"
|
||||||
checked={disablePassword}
|
checked={disablePassword}
|
||||||
onCheckedChange={setDisablePassword}
|
onCheckedChange={setDisablePassword}
|
||||||
|
disabled={serverSettings.isDemo}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="disable-password">Disable password</Label>
|
<Label htmlFor="disable-password">Disable password</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { checkPermission } from '@/lib/utils'
|
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
coinsAtom,
|
coinsAtom,
|
||||||
coinsEarnedTodayAtom,
|
// coinsEarnedTodayAtom,
|
||||||
totalEarnedAtom,
|
// totalEarnedAtom,
|
||||||
totalSpentAtom,
|
// totalSpentAtom,
|
||||||
coinsSpentTodayAtom,
|
// coinsSpentTodayAtom,
|
||||||
transactionsTodayAtom,
|
// transactionsTodayAtom,
|
||||||
coinsBalanceAtom
|
// coinsBalanceAtom,
|
||||||
|
settingsAtom,
|
||||||
|
usersAtom
|
||||||
} from '@/lib/atoms'
|
} from '@/lib/atoms'
|
||||||
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
|
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
|
||||||
import { CoinsData } from '@/lib/types'
|
import { CoinsData, User } from '@/lib/types'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
import { useHelpers } from '@/lib/client-helpers'
|
||||||
|
|
||||||
function handlePermissionCheck(
|
function handlePermissionCheck(
|
||||||
user: any,
|
user: User | undefined,
|
||||||
resource: 'habit' | 'wishlist' | 'coins',
|
resource: 'habit' | 'wishlist' | 'coins',
|
||||||
action: 'write' | 'interact'
|
action: 'write' | 'interact'
|
||||||
): boolean {
|
): boolean {
|
||||||
@@ -40,18 +42,30 @@ function handlePermissionCheck(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCoins() {
|
export function useCoins(options?: { selectedUser?: string }) {
|
||||||
const { currentUser: user } = useHelpers()
|
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [totalEarned] = useAtom(totalEarnedAtom)
|
const [users] = useAtom(usersAtom)
|
||||||
const [totalSpent] = useAtom(totalSpentAtom)
|
const { currentUser } = useHelpers()
|
||||||
const [coinsSpentToday] = useAtom(coinsSpentTodayAtom)
|
let user: User | undefined;
|
||||||
const [transactionsToday] = useAtom(transactionsTodayAtom)
|
if (!options?.selectedUser) {
|
||||||
const [balance] = useAtom(coinsBalanceAtom)
|
user = currentUser;
|
||||||
|
} else {
|
||||||
|
user = users.users.find(u => u.id === options.selectedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter transactions for the selectd user
|
||||||
|
const transactions = coins.transactions.filter(t => t.userId === user?.id)
|
||||||
|
|
||||||
|
const balance = transactions.reduce((sum, t) => sum + t.amount, 0)
|
||||||
|
const coinsEarnedToday = calculateCoinsEarnedToday(transactions, settings.system.timezone)
|
||||||
|
const totalEarned = calculateTotalEarned(transactions)
|
||||||
|
const totalSpent = calculateTotalSpent(transactions)
|
||||||
|
const coinsSpentToday = calculateCoinsSpentToday(transactions, settings.system.timezone)
|
||||||
|
const transactionsToday = calculateTransactionsToday(transactions, settings.system.timezone)
|
||||||
|
|
||||||
const add = async (amount: number, description: string, note?: string) => {
|
const add = async (amount: number, description: string, note?: string) => {
|
||||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||||
if (isNaN(amount) || amount <= 0) {
|
if (isNaN(amount) || amount <= 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "Invalid amount",
|
title: "Invalid amount",
|
||||||
@@ -64,7 +78,8 @@ export function useCoins() {
|
|||||||
amount,
|
amount,
|
||||||
description,
|
description,
|
||||||
type: 'MANUAL_ADJUSTMENT',
|
type: 'MANUAL_ADJUSTMENT',
|
||||||
note
|
note,
|
||||||
|
userId: user?.id
|
||||||
})
|
})
|
||||||
setCoins(data)
|
setCoins(data)
|
||||||
toast({ title: "Success", description: `Added ${amount} coins` })
|
toast({ title: "Success", description: `Added ${amount} coins` })
|
||||||
@@ -72,7 +87,7 @@ export function useCoins() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const remove = async (amount: number, description: string, note?: string) => {
|
const remove = async (amount: number, description: string, note?: string) => {
|
||||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||||
const numAmount = Math.abs(amount)
|
const numAmount = Math.abs(amount)
|
||||||
if (isNaN(numAmount) || numAmount <= 0) {
|
if (isNaN(numAmount) || numAmount <= 0) {
|
||||||
toast({
|
toast({
|
||||||
@@ -86,7 +101,8 @@ export function useCoins() {
|
|||||||
amount: numAmount,
|
amount: numAmount,
|
||||||
description,
|
description,
|
||||||
type: 'MANUAL_ADJUSTMENT',
|
type: 'MANUAL_ADJUSTMENT',
|
||||||
note
|
note,
|
||||||
|
userId: user?.id
|
||||||
})
|
})
|
||||||
setCoins(data)
|
setCoins(data)
|
||||||
toast({ title: "Success", description: `Removed ${numAmount} coins` })
|
toast({ title: "Success", description: `Removed ${numAmount} coins` })
|
||||||
@@ -94,7 +110,7 @@ export function useCoins() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateNote = async (transactionId: string, note: string) => {
|
const updateNote = async (transactionId: string, note: string) => {
|
||||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||||
const transaction = coins.transactions.find(t => t.id === transactionId)
|
const transaction = coins.transactions.find(t => t.id === transactionId)
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
toast({
|
toast({
|
||||||
@@ -128,7 +144,7 @@ export function useCoins() {
|
|||||||
remove,
|
remove,
|
||||||
updateNote,
|
updateNote,
|
||||||
balance,
|
balance,
|
||||||
transactions: coins.transactions,
|
transactions: transactions,
|
||||||
coinsEarnedToday,
|
coinsEarnedToday,
|
||||||
totalEarned,
|
totalEarned,
|
||||||
totalSpent,
|
totalSpent,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { wishlistAtom, coinsAtom, coinsBalanceAtom } from '@/lib/atoms'
|
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
|
||||||
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { WishlistItemType } from '@/lib/types'
|
import { WishlistItemType } from '@/lib/types'
|
||||||
import { celebrations } from '@/utils/celebrations'
|
import { celebrations } from '@/utils/celebrations'
|
||||||
import { checkPermission } from '@/lib/utils'
|
import { checkPermission } from '@/lib/utils'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
import { useHelpers } from '@/lib/client-helpers'
|
||||||
|
import { useCoins } from './useCoins'
|
||||||
|
|
||||||
function handlePermissionCheck(
|
function handlePermissionCheck(
|
||||||
user: any,
|
user: any,
|
||||||
@@ -37,7 +38,7 @@ export function useWishlist() {
|
|||||||
const { currentUser: user } = useHelpers()
|
const { currentUser: user } = useHelpers()
|
||||||
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [balance] = useAtom(coinsBalanceAtom)
|
const { balance } = useCoins()
|
||||||
|
|
||||||
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
||||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||||
|
|||||||
66
lib/atoms.ts
66
lib/atoms.ts
@@ -49,44 +49,44 @@ export const coinsAtom = atom(getDefaultCoinsData());
|
|||||||
export const wishlistAtom = atom(getDefaultWishlistData());
|
export const wishlistAtom = atom(getDefaultWishlistData());
|
||||||
export const serverSettingsAtom = atom(getDefaultServerSettings());
|
export const serverSettingsAtom = atom(getDefaultServerSettings());
|
||||||
|
|
||||||
// Derived atom for coins earned today
|
// // Derived atom for coins earned today
|
||||||
export const coinsEarnedTodayAtom = atom((get) => {
|
// export const coinsEarnedTodayAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
const settings = get(settingsAtom);
|
// const settings = get(settingsAtom);
|
||||||
return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
|
// return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Derived atom for total earned
|
// // Derived atom for total earned
|
||||||
export const totalEarnedAtom = atom((get) => {
|
// export const totalEarnedAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
return calculateTotalEarned(coins.transactions);
|
// return calculateTotalEarned(coins.transactions);
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Derived atom for total spent
|
// // Derived atom for total spent
|
||||||
export const totalSpentAtom = atom((get) => {
|
// export const totalSpentAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
return calculateTotalSpent(coins.transactions);
|
// return calculateTotalSpent(coins.transactions);
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Derived atom for coins spent today
|
// // Derived atom for coins spent today
|
||||||
export const coinsSpentTodayAtom = atom((get) => {
|
// export const coinsSpentTodayAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
const settings = get(settingsAtom);
|
// const settings = get(settingsAtom);
|
||||||
return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
|
// return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Derived atom for transactions today
|
// // Derived atom for transactions today
|
||||||
export const transactionsTodayAtom = atom((get) => {
|
// export const transactionsTodayAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
const settings = get(settingsAtom);
|
// const settings = get(settingsAtom);
|
||||||
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
|
// return calculateTransactionsToday(coins.transactions, settings.system.timezone);
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Derived atom for current balance from all transactions
|
// // Derived atom for current balance from all transactions
|
||||||
export const coinsBalanceAtom = atom((get) => {
|
// export const coinsBalanceAtom = atom((get) => {
|
||||||
const coins = get(coinsAtom);
|
// const coins = get(coinsAtom);
|
||||||
return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0);
|
// return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0);
|
||||||
});
|
// });
|
||||||
|
|
||||||
/* transient atoms */
|
/* transient atoms */
|
||||||
interface PomodoroAtom {
|
interface PomodoroAtom {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { z } from "zod"
|
|||||||
|
|
||||||
const zodEnv = z.object({
|
const zodEnv = z.object({
|
||||||
AUTH_SECRET: z.string(),
|
AUTH_SECRET: z.string(),
|
||||||
NEXT_PUBLIC_DEMO: z.string().optional(),
|
DEMO: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ProcessEnv extends z.TypeOf<typeof zodEnv> {
|
interface ProcessEnv extends z.TypeOf<typeof zodEnv> {
|
||||||
AUTH_SECRET: string;
|
AUTH_SECRET: string;
|
||||||
NEXT_PUBLIC_DEMO?: string;
|
DEMO?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
Reference in New Issue
Block a user