23 - Price tracking opt-in: migration, flag, conditional UI, onboarding checkbox
This commit is contained in:
parent
0861cff8b4
commit
818e8b2276
13 changed files with 119 additions and 74 deletions
|
|
@ -24,6 +24,7 @@ public function current(): JsonResponse
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'asset' => $asset,
|
'asset' => $asset,
|
||||||
|
'price_tracking_enabled' => $user?->price_tracking_enabled ?? false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@ public function update(Request $request)
|
||||||
return back()->withErrors(['asset' => 'Please set an asset first.']);
|
return back()->withErrors(['asset' => 'Please set an asset first.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$assetPrice = AssetPrice::updatePrice($user->asset_id, $validated['date'], $validated['price']);
|
AssetPrice::updatePrice($user->asset_id, $validated['date'], $validated['price']);
|
||||||
|
|
||||||
|
if (!$user->price_tracking_enabled) {
|
||||||
|
$user->update(['price_tracking_enabled' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Asset price updated successfully!');
|
return back()->with('success', 'Asset price updated successfully!');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class User extends Authenticatable
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
'asset_id',
|
'asset_id',
|
||||||
|
'price_tracking_enabled',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -43,6 +44,7 @@ protected function casts(): array
|
||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'price_tracking_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ public function up(): void
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
$table->string('password');
|
$table->string('password');
|
||||||
$table->foreignId('asset_id')->nullable()->constrained()->onDelete('set null');
|
$table->foreignId('asset_id')->nullable()->constrained()->onDelete('set null');
|
||||||
|
$table->boolean('price_tracking_enabled')->default(false);
|
||||||
$table->rememberToken();
|
$table->rememberToken();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,22 +22,6 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
# Copy composer files and install PHP dependencies
|
|
||||||
COPY composer.json ./
|
|
||||||
RUN composer install --no-dev --optimize-autoloader --no-scripts
|
|
||||||
|
|
||||||
# Copy package.json and install Node dependencies
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy application code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
|
||||||
&& chmod -R 755 /var/www/html/storage \
|
|
||||||
&& chmod -R 755 /var/www/html/bootstrap/cache
|
|
||||||
|
|
||||||
# Copy and set up container start script
|
# Copy and set up container start script
|
||||||
COPY docker/dev/podman/container-start.sh /usr/local/bin/container-start.sh
|
COPY docker/dev/podman/container-start.sh /usr/local/bin/container-start.sh
|
||||||
RUN chmod +x /usr/local/bin/container-start.sh
|
RUN chmod +x /usr/local/bin/container-start.sh
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ if ! grep -q "APP_KEY=base64:" /var/www/html/.env; then
|
||||||
sed -i "s/APP_KEY=/APP_KEY=$NEW_KEY/" /var/www/html/.env
|
sed -i "s/APP_KEY=/APP_KEY=$NEW_KEY/" /var/www/html/.env
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install dependencies if needed
|
||||||
|
[ ! -f vendor/autoload.php ] && composer install --no-interaction
|
||||||
|
[ ! -d node_modules/.bin ] && npm install
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
php artisan migrate --force
|
php artisan migrate --force
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ services:
|
||||||
- VITE_PORT=5173
|
- VITE_PORT=5173
|
||||||
volumes:
|
volumes:
|
||||||
- ../../../:/var/www/html:Z
|
- ../../../:/var/www/html:Z
|
||||||
- /var/www/html/node_modules
|
- app_node_modules:/var/www/html/node_modules
|
||||||
- /var/www/html/vendor
|
- app_vendor:/var/www/html/vendor
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
- "5173:5173"
|
- "5173:5173"
|
||||||
|
|
@ -70,3 +70,5 @@ networks:
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
app_node_modules:
|
||||||
|
app_vendor:
|
||||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "site",
|
"name": "html",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ interface StatsBoxProps {
|
||||||
onAddMilestone?: () => void;
|
onAddMilestone?: () => void;
|
||||||
onUpdatePrice?: () => void;
|
onUpdatePrice?: () => void;
|
||||||
assetSymbol?: string;
|
assetSymbol?: string;
|
||||||
|
priceTrackingEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatsBox({
|
export default function StatsBox({
|
||||||
|
|
@ -38,7 +39,8 @@ export default function StatsBox({
|
||||||
onAddPurchase,
|
onAddPurchase,
|
||||||
onAddMilestone,
|
onAddMilestone,
|
||||||
onUpdatePrice,
|
onUpdatePrice,
|
||||||
assetSymbol
|
assetSymbol,
|
||||||
|
priceTrackingEnabled = false,
|
||||||
}: StatsBoxProps) {
|
}: StatsBoxProps) {
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -78,7 +80,7 @@ export default function StatsBox({
|
||||||
<ComponentTitle>Stats</ComponentTitle>
|
<ComponentTitle>Stats</ComponentTitle>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 relative">
|
<div className="flex items-center space-x-2 relative">
|
||||||
{stats.currentPrice && (
|
{priceTrackingEnabled && stats.currentPrice && (
|
||||||
<div className="text-red-500 text-sm font-mono tracking-wider">
|
<div className="text-red-500 text-sm font-mono tracking-wider">
|
||||||
{assetSymbol ?? 'PRICE'}: {formatCurrencyDetailed(stats.currentPrice)}
|
{assetSymbol ?? 'PRICE'}: {formatCurrencyDetailed(stats.currentPrice)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,7 +121,7 @@ export default function StatsBox({
|
||||||
ADD MILESTONE
|
ADD MILESTONE
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onUpdatePrice && (
|
{priceTrackingEnabled && onUpdatePrice && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdatePrice();
|
onUpdatePrice();
|
||||||
|
|
@ -156,8 +158,8 @@ export default function StatsBox({
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left text-red-500 text-xs py-2">DESCRIPTION</th>
|
<th className="text-left text-red-500 text-xs py-2">DESCRIPTION</th>
|
||||||
<th className="text-right text-red-500 text-xs py-2">SHARES</th>
|
<th className="text-right text-red-500 text-xs py-2">SHARES</th>
|
||||||
<th className="text-right text-red-500 text-xs py-2 pr-4">SWR 3%</th>
|
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2 pr-4">SWR 3%</th>}
|
||||||
<th className="text-right text-red-500 text-xs py-2">SWR 4%</th>
|
{priceTrackingEnabled && <th className="text-right text-red-500 text-xs py-2">SWR 4%</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -167,12 +169,16 @@ export default function StatsBox({
|
||||||
<td className="text-right py-1 pr-4">
|
<td className="text-right py-1 pr-4">
|
||||||
{Math.floor(stats.totalShares).toLocaleString()}
|
{Math.floor(stats.totalShares).toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
|
{priceTrackingEnabled && (
|
||||||
<td className="text-right py-1 pr-4">
|
<td className="text-right py-1 pr-4">
|
||||||
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.03) : 'N/A'}
|
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.03) : 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
|
{priceTrackingEnabled && (
|
||||||
<td className="text-right py-1">
|
<td className="text-right py-1">
|
||||||
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'}
|
{stats.currentPrice ? formatCurrency(stats.totalShares * stats.currentPrice * 0.04) : 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{/* Render milestones after current */}
|
{/* Render milestones after current */}
|
||||||
|
|
@ -197,12 +203,16 @@ export default function StatsBox({
|
||||||
<td className="text-right py-1 pr-4">
|
<td className="text-right py-1 pr-4">
|
||||||
{Math.floor(milestone.target).toLocaleString()}
|
{Math.floor(milestone.target).toLocaleString()}
|
||||||
</td>
|
</td>
|
||||||
|
{priceTrackingEnabled && (
|
||||||
<td className="text-right py-1 pr-4">
|
<td className="text-right py-1 pr-4">
|
||||||
{stats.currentPrice ? formatCurrency(swr3) : 'N/A'}
|
{stats.currentPrice ? formatCurrency(swr3) : 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
|
{priceTrackingEnabled && (
|
||||||
<td className="text-right py-1">
|
<td className="text-right py-1">
|
||||||
{stats.currentPrice ? formatCurrency(swr4) : 'N/A'}
|
{stats.currentPrice ? formatCurrency(swr4) : 'N/A'}
|
||||||
</td>
|
</td>
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,44 @@ import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm';
|
||||||
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm';
|
||||||
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm';
|
||||||
|
|
||||||
|
function PriceTrackingStep({ onEnable, onSkip }: { onEnable: () => void; onSkip?: () => void }) {
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={e => setEnabled(e.target.checked)}
|
||||||
|
className="w-4 h-4 accent-red-500"
|
||||||
|
/>
|
||||||
|
<span className="text-red-400 font-mono text-sm uppercase tracking-wider group-hover:text-red-300">
|
||||||
|
Enable price tracking (optional)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p className="text-red-400/60 font-mono text-xs">
|
||||||
|
Track the current market price of your asset to see portfolio value and P&L. You can enable this later in settings.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{enabled && (
|
||||||
|
<div className="border border-red-500/30 p-4">
|
||||||
|
<UpdatePriceForm onSuccess={onEnable} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!enabled && (
|
||||||
|
<button
|
||||||
|
onClick={onSkip}
|
||||||
|
className="w-full py-2 font-mono text-xs uppercase tracking-wider border border-red-500/50 text-red-400 hover:bg-red-950/30 hover:text-red-300 transition-colors"
|
||||||
|
>
|
||||||
|
Skip and finish
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface OnboardingStep {
|
interface OnboardingStep {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -43,9 +81,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
{
|
{
|
||||||
id: 'price',
|
id: 'price',
|
||||||
title: 'CURRENT PRICE',
|
title: 'CURRENT PRICE',
|
||||||
description: 'Set current asset price',
|
description: 'Set current asset price (optional)',
|
||||||
completed: false,
|
completed: false,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -76,26 +114,23 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
const priceData = await priceResponse.json();
|
const priceData = await priceResponse.json();
|
||||||
const hasPrice = !!priceData.current_price;
|
const hasPrice = !!priceData.current_price;
|
||||||
|
|
||||||
setSteps(prev => prev.map(step => ({
|
const freshSteps = steps.map(step => ({
|
||||||
...step,
|
...step,
|
||||||
completed:
|
completed:
|
||||||
(step.id === 'asset' && hasAsset) ||
|
(step.id === 'asset' && hasAsset) ||
|
||||||
(step.id === 'purchases' && hasPurchases) ||
|
(step.id === 'purchases' && hasPurchases) ||
|
||||||
(step.id === 'milestones' && hasMilestones) ||
|
(step.id === 'milestones' && hasMilestones) ||
|
||||||
(step.id === 'price' && hasPrice)
|
(step.id === 'price' && hasPrice)
|
||||||
})));
|
}));
|
||||||
|
|
||||||
// Find first incomplete required step
|
setSteps(freshSteps);
|
||||||
const firstIncompleteStep = steps.findIndex(step =>
|
|
||||||
step.required && !step.completed
|
|
||||||
);
|
|
||||||
|
|
||||||
if (firstIncompleteStep !== -1) {
|
const firstIncompleteRequired = freshSteps.findIndex(s => s.required && !s.completed);
|
||||||
setCurrentStep(firstIncompleteStep);
|
|
||||||
|
if (firstIncompleteRequired !== -1) {
|
||||||
|
setCurrentStep(firstIncompleteRequired);
|
||||||
} else {
|
} else {
|
||||||
// All required steps complete, check if we should call onComplete
|
if (onComplete) {
|
||||||
const allRequiredComplete = steps.filter(s => s.required).every(s => s.completed);
|
|
||||||
if (allRequiredComplete && onComplete) {
|
|
||||||
onComplete();
|
onComplete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,23 +145,8 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
index === currentStep ? { ...step, completed: true } : step
|
index === currentStep ? { ...step, completed: true } : step
|
||||||
));
|
));
|
||||||
|
|
||||||
// Refresh onboarding status
|
// Refresh onboarding status — handles setCurrentStep and onComplete
|
||||||
await checkOnboardingStatus();
|
await checkOnboardingStatus();
|
||||||
|
|
||||||
// Move to next incomplete step or complete onboarding
|
|
||||||
const nextIncompleteStep = steps.findIndex((step, index) =>
|
|
||||||
index > currentStep && step.required && !step.completed
|
|
||||||
);
|
|
||||||
|
|
||||||
if (nextIncompleteStep !== -1) {
|
|
||||||
setCurrentStep(nextIncompleteStep);
|
|
||||||
} else {
|
|
||||||
// All required steps complete
|
|
||||||
const allRequiredComplete = steps.filter(s => s.required).every(s => s.completed);
|
|
||||||
if (allRequiredComplete && onComplete) {
|
|
||||||
onComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStepSelect = (stepIndex: number) => {
|
const handleStepSelect = (stepIndex: number) => {
|
||||||
|
|
@ -157,8 +177,9 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
);
|
);
|
||||||
case 'price':
|
case 'price':
|
||||||
return (
|
return (
|
||||||
<UpdatePriceForm
|
<PriceTrackingStep
|
||||||
onSuccess={handleStepComplete}
|
onEnable={handleStepComplete}
|
||||||
|
onSkip={onComplete}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
@ -177,7 +198,7 @@ export default function OnboardingFlow({ onComplete }: OnboardingFlowProps) {
|
||||||
[SYSTEM] ONBOARDING SEQUENCE
|
[SYSTEM] ONBOARDING SEQUENCE
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-red-400/60 font-mono text-sm">
|
<p className="text-red-400/60 font-mono text-sm">
|
||||||
Initialize your asset tracking system
|
Set up your tracker
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export default function Dashboard() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
||||||
const [currentAsset, setCurrentAsset] = useState<any>(null);
|
const [currentAsset, setCurrentAsset] = useState<any>(null);
|
||||||
|
const [priceTrackingEnabled, setPriceTrackingEnabled] = useState(false);
|
||||||
|
|
||||||
// Fetch purchase summary, current price, milestones, and check onboarding
|
// Fetch purchase summary, current price, milestones, and check onboarding
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -72,6 +73,7 @@ export default function Dashboard() {
|
||||||
if (assetResponse.ok) {
|
if (assetResponse.ok) {
|
||||||
const assetData = await assetResponse.json();
|
const assetData = await assetResponse.json();
|
||||||
setCurrentAsset(assetData.asset);
|
setCurrentAsset(assetData.asset);
|
||||||
|
setPriceTrackingEnabled(assetData.price_tracking_enabled ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if onboarding is needed after all data is loaded
|
// Check if onboarding is needed after all data is loaded
|
||||||
|
|
@ -238,6 +240,7 @@ export default function Dashboard() {
|
||||||
if (assetResponse.ok) {
|
if (assetResponse.ok) {
|
||||||
const assetData = await assetResponse.json();
|
const assetData = await assetResponse.json();
|
||||||
setCurrentAsset(assetData.asset);
|
setCurrentAsset(assetData.asset);
|
||||||
|
setPriceTrackingEnabled(assetData.price_tracking_enabled ?? false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -287,6 +290,7 @@ export default function Dashboard() {
|
||||||
onAddMilestone={() => setActiveForm('milestone')}
|
onAddMilestone={() => setActiveForm('milestone')}
|
||||||
onUpdatePrice={() => setActiveForm('price')}
|
onUpdatePrice={() => setActiveForm('price')}
|
||||||
assetSymbol={currentAsset?.symbol}
|
assetSymbol={currentAsset?.symbol}
|
||||||
|
priceTrackingEnabled={priceTrackingEnabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,9 @@ pkgs.mkShell {
|
||||||
}
|
}
|
||||||
|
|
||||||
dev-rebuild() {
|
dev-rebuild() {
|
||||||
echo "Rebuilding services (down -v + up)..."
|
echo "Rebuilding services (down -v + up --build)..."
|
||||||
podman-compose -f $COMPOSE_FILE down -v
|
podman-compose -f $COMPOSE_FILE down -v
|
||||||
PODMAN_USERNS=keep-id podman-compose -f $COMPOSE_FILE up -d "$@"
|
PODMAN_USERNS=keep-id podman-compose -f $COMPOSE_FILE up -d --build "$@"
|
||||||
echo ""
|
echo ""
|
||||||
podman-compose -f $COMPOSE_FILE ps
|
podman-compose -f $COMPOSE_FILE ps
|
||||||
echo ""
|
echo ""
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,18 @@ import { resolve } from 'node:path';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
hmr: {
|
||||||
|
host: 'localhost',
|
||||||
|
clientPort: 5173,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
ignored: ['**/storage/framework/views/**'],
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
laravel({
|
laravel({
|
||||||
input: ['resources/css/app.css', 'resources/js/app.tsx'],
|
input: ['resources/css/app.css', 'resources/js/app.tsx'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue