Compare commits
9 Commits
ecfa1b6dcb
...
0479f25db4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0479f25db4 | |||
| 2bbbbc746f | |||
| a99a477985 | |||
| 1878074a95 | |||
| 910b9bf4bc | |||
| 0d517b198d | |||
|
|
236780cf41 | ||
|
|
04017895a0 | ||
|
|
b5ee599b2c |
3
.gitconfig
Normal file
3
.gitconfig
Normal file
@@ -0,0 +1,3 @@
|
||||
[user]
|
||||
email = chloestanton117@gmail.com
|
||||
name = chloe
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,24 +1,8 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
bin/
|
||||
obj/
|
||||
Seasoned.Frontend/node_modules/
|
||||
.nuxt/
|
||||
.output/
|
||||
dist/
|
||||
.nvm/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Stage 1: Build the Nuxt 4 frontend (Debian Slim)
|
||||
FROM node:20-bookworm-slim AS frontend-build
|
||||
WORKDIR /src/frontend
|
||||
COPY Seasoned.Frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY Seasoned.Frontend/ .
|
||||
RUN npx nuxi generate
|
||||
|
||||
# Stage 2: Build the .NET 9 backend (Debian Slim)
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS backend-build
|
||||
WORKDIR /src
|
||||
COPY Seasoned.Backend/*.csproj ./Seasoned.Backend/
|
||||
RUN dotnet restore ./Seasoned.Backend/Seasoned.Backend.csproj
|
||||
COPY . .
|
||||
|
||||
# Copy Nuxt static files into .NET wwwroot
|
||||
COPY --from=frontend-build /src/frontend/.output/public ./Seasoned.Backend/wwwroot
|
||||
|
||||
RUN dotnet publish ./Seasoned.Backend/Seasoned.Backend.csproj -c Release -o /app
|
||||
|
||||
# Stage 3: Final Runtime (Debian Slim)
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
||||
WORKDIR /app
|
||||
COPY --from=backend-build /app .
|
||||
|
||||
# Essential for PGVector and Gemini logic to run smoothly
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENTRYPOINT ["dotnet", "Seasoned.Backend.dll"]
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The Pitch:
|
||||
|
||||
Seasoned is a high-performance, private digital cookbook that bridges the gap between web discovery and kitchen execution. By combining the multimodal power of Gemini 1.5 Flash with a secure, self-hosted PostgreSQL backbone, Seasoned allows users to instantly "distill" messy recipe blogs and food photos into a standardized, searchable, and shareable library they truly own.
|
||||
Seasoned is a high-performance, private digital cookbook that bridges the gap between web discovery and kitchen execution. By combining the multimodal power of Gemini 2.5 Flash with a secure, self-hosted PostgreSQL backbone, Seasoned allows users to instantly "distill" messy recipe blogs and food photos into a standardized, searchable, and shareable library they truly own.
|
||||
|
||||
Target Audience:
|
||||
|
||||
@@ -19,9 +19,9 @@ The Hybrid Tech Stack:
|
||||
| **Hosting** | Private Server (Dockerized on home hardware) |
|
||||
| **CI/CD** | Jenkins server |
|
||||
| **Frontend** | Nuxt 4 + Vuetify + CSS |
|
||||
| **Backend** | Nuxt Nitro |
|
||||
| **Backend** | Dotnet |
|
||||
| **Database** | Postgres + pgvector |
|
||||
| **Intelligence** | Gemini 1.5 Flash |
|
||||
| **Intelligence** | Gemini 2.5 Flash |
|
||||
| **Storage** | Local File System |
|
||||
|
||||
Technical Requirements:
|
||||
|
||||
29
Seasoned.Backend/Controllers/RecipeController.cs
Normal file
29
Seasoned.Backend/Controllers/RecipeController.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Seasoned.Backend.Services;
|
||||
using Seasoned.Backend.DTOs;
|
||||
|
||||
namespace Seasoned.Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/recipe")]
|
||||
public class RecipeController : ControllerBase
|
||||
{
|
||||
private readonly IRecipeService _recipeService;
|
||||
|
||||
public RecipeController(IRecipeService recipeService)
|
||||
{
|
||||
_recipeService = recipeService;
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
public async Task<ActionResult<RecipeResponseDto>> UploadRecipe([FromForm] IFormFile image)
|
||||
{
|
||||
if (image == null || image.Length == 0)
|
||||
{
|
||||
return BadRequest("No image uploaded.");
|
||||
}
|
||||
|
||||
var result = await _recipeService.ParseRecipeImageAsync(image);
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
9
Seasoned.Backend/DTOs/RecipeResponseDto.cs
Normal file
9
Seasoned.Backend/DTOs/RecipeResponseDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Seasoned.Backend.DTOs;
|
||||
|
||||
public class RecipeResponseDto
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> Ingredients { get; set; } = new();
|
||||
public List<string> Instructions { get; set; } = new();
|
||||
}
|
||||
17
Seasoned.Backend/Data/ApplicationDbContext.cs
Normal file
17
Seasoned.Backend/Data/ApplicationDbContext.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Seasoned.Backend.Models;
|
||||
|
||||
namespace Seasoned.Backend.Data;
|
||||
|
||||
public class ApplicationDbContext : DbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<Recipe> Recipes { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasPostgresExtension("vector");
|
||||
}
|
||||
}
|
||||
64
Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs
generated
Normal file
64
Seasoned.Backend/Data/Migrations/20260305044721_SeasonedInit.Designer.cs
generated
Normal file
@@ -0,0 +1,64 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260305044721_SeasonedInit")]
|
||||
partial class SeasonedInit
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Ingredients")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Instructions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Recipes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeasonedInit : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:vector", ",,");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Recipes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Description = table.Column<string>(type: "text", nullable: false),
|
||||
Ingredients = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||
Instructions = table.Column<List<string>>(type: "text[]", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Recipes", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Recipes");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Seasoned.Backend.Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.2")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Seasoned.Backend.Models.Recipe", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Ingredients")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("Instructions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Recipes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Seasoned.Backend/Models/Recipe.cs
Normal file
11
Seasoned.Backend/Models/Recipe.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Seasoned.Backend.Models;
|
||||
|
||||
public class Recipe
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<string> Ingredients { get; set; } = new();
|
||||
public List<string> Instructions { get; set; } = new();
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
51
Seasoned.Backend/Program.cs
Normal file
51
Seasoned.Backend/Program.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Seasoned.Backend.Services;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Seasoned.Backend.Data;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options => {
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"),
|
||||
o => o.UseVector()));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
app.UseDefaultFiles();
|
||||
app.UseStaticFiles();
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
app.Run();
|
||||
14
Seasoned.Backend/Properties/launchSettings.json
Normal file
14
Seasoned.Backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Seasoned.Backend/Seasoned.Backend.csproj
Normal file
22
Seasoned.Backend/Seasoned.Backend.csproj
Normal file
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>71e44981-0c1f-4d90-8e87-317af8bd03ce</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.13" />
|
||||
<PackageReference Include="Mscc.GenerativeAI" Version="2.2.8" />
|
||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
Seasoned.Backend/Seasoned.Backend.http
Normal file
6
Seasoned.Backend/Seasoned.Backend.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@Seasoned.Backend_HostAddress = http://localhost:5243
|
||||
|
||||
GET {{Seasoned.Backend_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
8
Seasoned.Backend/Services/IRecipeService.cs
Normal file
8
Seasoned.Backend/Services/IRecipeService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Seasoned.Backend.DTOs;
|
||||
|
||||
namespace Seasoned.Backend.Services;
|
||||
|
||||
public interface IRecipeService
|
||||
{
|
||||
Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image);
|
||||
}
|
||||
78
Seasoned.Backend/Services/RecipeService.cs
Normal file
78
Seasoned.Backend/Services/RecipeService.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Seasoned.Backend.DTOs;
|
||||
using Mscc.GenerativeAI;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
namespace Seasoned.Backend.Services;
|
||||
|
||||
public class RecipeService : IRecipeService
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
|
||||
public RecipeService(IConfiguration config)
|
||||
{
|
||||
_apiKey = config["GEMINI_API_KEY"] ?? throw new ArgumentNullException("API Key missing");
|
||||
}
|
||||
|
||||
public async Task<RecipeResponseDto> ParseRecipeImageAsync(IFormFile image)
|
||||
{
|
||||
var googleAI = new GoogleAI(_apiKey);
|
||||
var model = googleAI.GenerativeModel("gemini-2.5-flash");
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await image.CopyToAsync(ms);
|
||||
var base64Image = Convert.ToBase64String(ms.ToArray());
|
||||
|
||||
var prompt = @"Extract the recipe details from this image.
|
||||
IMPORTANT: Return ONLY a raw JSON string.
|
||||
DO NOT include markdown formatting (no ```json).
|
||||
DO NOT include any text before or after the JSON.
|
||||
All property names and string values MUST be enclosed in double quotes.
|
||||
JSON structure:
|
||||
{
|
||||
""title"": ""string"",
|
||||
""description"": ""string"",
|
||||
""ingredients"": [""string"", ""string""],
|
||||
""instructions"": [""string"", ""string""]
|
||||
}";
|
||||
|
||||
var config = new GenerationConfig {
|
||||
ResponseMimeType = "application/json",
|
||||
Temperature = 0.1f
|
||||
};
|
||||
|
||||
var request = new GenerateContentRequest(prompt, config);
|
||||
await Task.Run(() => request.AddMedia(base64Image, "image/png"));
|
||||
|
||||
var response = await model.GenerateContent(request);
|
||||
string rawText = response.Text?.Trim() ?? "";
|
||||
|
||||
int start = rawText.IndexOf('{');
|
||||
int end = rawText.LastIndexOf('}');
|
||||
|
||||
if (start == -1 || end == -1)
|
||||
{
|
||||
return new RecipeResponseDto { Title = "Error", Description = "AI failed to generate a valid JSON block." };
|
||||
}
|
||||
|
||||
string cleanJson = rawText.Substring(start, (end - start) + 1);
|
||||
|
||||
try
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
var result = JsonSerializer.Deserialize<RecipeResponseDto>(cleanJson, options);
|
||||
return result ?? new RecipeResponseDto { Title = "Empty Response" };
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Console.WriteLine($"Raw AI Output: {rawText}");
|
||||
Console.WriteLine($"Failed to parse JSON: {ex.Message}");
|
||||
|
||||
return new RecipeResponseDto {
|
||||
Title = "Parsing Error",
|
||||
Description = "The AI response was malformed. Check logs."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Seasoned.Backend/appsettings.Development.json
Normal file
8
Seasoned.Backend/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Seasoned.Backend/appsettings.json
Normal file
9
Seasoned.Backend/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
24
Seasoned.Frontend/.gitignore
vendored
Normal file
24
Seasoned.Frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
1
Seasoned.Frontend/.nuxtrc
Normal file
1
Seasoned.Frontend/.nuxtrc
Normal file
@@ -0,0 +1 @@
|
||||
telemetry.enabled=false
|
||||
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
14
Seasoned.Frontend/Seasoned.code-workspace
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../Seasoned.Backend"
|
||||
},
|
||||
{
|
||||
"path": "../Seasoned.Tests"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
104
Seasoned.Frontend/app/app.vue
Normal file
104
Seasoned.Frontend/app/app.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<v-app class="recipe-bg">
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-card class="recipe-card pa-10 mx-auto mt-10" max-width="950" elevation="1">
|
||||
|
||||
<header class="text-center mb-10">
|
||||
<h1 class="brand-title">Seasoned</h1>
|
||||
<p class="brand-subtitle">A Recipe Collection</p>
|
||||
</header>
|
||||
|
||||
<v-divider class="mb-10 separator"></v-divider>
|
||||
|
||||
<v-row justify="center" class="mb-12">
|
||||
<v-col cols="12" md="8">
|
||||
<v-file-input
|
||||
v-model="files"
|
||||
label="Upload Image"
|
||||
variant="solo-filled"
|
||||
flat
|
||||
accept="image/*"
|
||||
class="custom-input mb-4"
|
||||
></v-file-input>
|
||||
|
||||
<v-btn
|
||||
class="analyze-btn"
|
||||
block
|
||||
size="x-large"
|
||||
elevation="0"
|
||||
:loading="loading"
|
||||
@click="uploadImage"
|
||||
>
|
||||
Analyze Recipe
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-if="recipe" class="recipe-content">
|
||||
<h2 class="recipe-title text-center mb-4">{{ recipe.title }}</h2>
|
||||
<p class="recipe-description text-center mb-12 text-italic">{{ recipe.description }}</p>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="5">
|
||||
<div class="section-header mb-6 px-2">
|
||||
<v-icon icon="mdi-spoon-sugar" class="mr-2" size="small"></v-icon>
|
||||
<span>Ingredients</span>
|
||||
</div>
|
||||
<v-list class="ingredients-list">
|
||||
<v-list-item v-for="(item, i) in recipe.ingredients" :key="i" class="ingredient-item">
|
||||
{{ item }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="7">
|
||||
<div class="section-header mb-6 px-2">
|
||||
<v-icon icon="mdi-pot-steam-outline" class="mr-2" size="small"></v-icon>
|
||||
<span>Instructions</span>
|
||||
</div>
|
||||
<div v-for="(step, i) in recipe.instructions" :key="i" class="instruction-step mb-8">
|
||||
<span class="step-number">{{ i + 1 }}.</span>
|
||||
<p class="step-text">{{ step }}</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</transition>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import '@/assets/css/app-theme.css'
|
||||
const config = useRuntimeConfig()
|
||||
const files = ref([])
|
||||
const loading = ref(false)
|
||||
const recipe = ref(null)
|
||||
|
||||
const uploadImage = async () => {
|
||||
const fileToUpload = Array.isArray(files.value) ? files.value[0] : files.value;
|
||||
if (!fileToUpload) return;
|
||||
|
||||
loading.value = true;
|
||||
const formData = new FormData();
|
||||
formData.append('image', fileToUpload);
|
||||
|
||||
try {
|
||||
const response = await $fetch(`${config.public.apiBase}api/recipe/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
recipe.value = response;
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
116
Seasoned.Frontend/app/assets/css/app-theme.css
Normal file
116
Seasoned.Frontend/app/assets/css/app-theme.css
Normal file
@@ -0,0 +1,116 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Inter:wght@400;600&display=swap');
|
||||
|
||||
.recipe-bg {
|
||||
background-color: #3e2a14 !important;
|
||||
background-image: url("https://www.transparenttextures.com/patterns/dark-wood.png") !important; /* Richer wood texture */
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.recipe-card {
|
||||
background-color: #f4e4bc !important;
|
||||
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png"); /* Stronger linen texture */
|
||||
border: 1px solid #c9b996 !important;
|
||||
border-radius: 12px !important;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-weight: 700;
|
||||
font-size: 2.8rem;
|
||||
color: #2e1e0a;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-style: italic;
|
||||
color: #6d5e4a;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.recipe-title {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-size: 2.4rem;
|
||||
color: #1e1408;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
border-bottom: 2px solid #dccca7;
|
||||
color: #4a3a2a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.custom-input .v-field {
|
||||
background-color: #5d4037 !important;
|
||||
color: #f8f1e0 !important;
|
||||
border-radius: 8px !important;
|
||||
border: 2px solid #5d4037 !important;
|
||||
}
|
||||
|
||||
.custom-input .v-label {
|
||||
color: #f8f1e0 !important;
|
||||
opacity: 1 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-input .v-icon {
|
||||
color: #f8f1e0 !important;
|
||||
}
|
||||
|
||||
.custom-input .v-field--focused {
|
||||
border: 2px solid #556b2f !important;
|
||||
}
|
||||
|
||||
.analyze-btn {
|
||||
background-color: #556b2f !important;
|
||||
color: #ffffff !important;
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
text-transform: none;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ingredients-list {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ingredient-item {
|
||||
border-bottom: 1px dotted #c1b18e;
|
||||
font-style: italic;
|
||||
color: #2c2925;
|
||||
}
|
||||
|
||||
.instruction-step {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
line-height: 1.6;
|
||||
color: #2c2925;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-weight: bold;
|
||||
color: #3b4e1e;
|
||||
font-size: 1.3rem;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-color: #dccca7 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
40
Seasoned.Frontend/nuxt.config.ts
Normal file
40
Seasoned.Frontend/nuxt.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
|
||||
devtools: { enabled: true },
|
||||
|
||||
future: {
|
||||
compatibilityVersion: 4,
|
||||
},
|
||||
|
||||
srcDir: 'app/',
|
||||
|
||||
css: [
|
||||
'vuetify/lib/styles/main.sass',
|
||||
'@mdi/font/css/materialdesignicons.min.css',
|
||||
],
|
||||
|
||||
build: {
|
||||
transpile: ['vuetify'],
|
||||
},
|
||||
|
||||
modules: [
|
||||
'vuetify-nuxt-module'
|
||||
],
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBase: ''
|
||||
}
|
||||
},
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
hmr: process.env.NODE_ENV !== 'production' ? {
|
||||
protocol: 'ws',
|
||||
host: 'localhost',
|
||||
port: 3000
|
||||
} : undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
3254
package-lock.json → Seasoned.Frontend/package-lock.json
generated
3254
package-lock.json → Seasoned.Frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
Seasoned.Frontend/package.json
Normal file
35
Seasoned.Frontend/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "Seasoned",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@prisma/client": "^7.4.2",
|
||||
"axios": "^1.13.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"nuxt": "^4.1.3",
|
||||
"prisma": "^6.19.2",
|
||||
"sass": "^1.97.3",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^4.6.4",
|
||||
"vuetify": "^4.0.1",
|
||||
"vuetify-nuxt-module": "^0.19.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.3.3",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"happy-dom": "^20.8.3",
|
||||
"jsdom": "^28.1.0",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
14
Seasoned.Frontend/plugins/vuetify.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// @ts-nocheck
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const vuetify = createVuetify({
|
||||
ssr: true,
|
||||
components,
|
||||
directives,
|
||||
})
|
||||
|
||||
nuxtApp.vueApp.use(vuetify)
|
||||
})
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
8
Seasoned.Frontend/test/App.spec.ts
Normal file
8
Seasoned.Frontend/test/App.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
describe('Frontend Setup', () => {
|
||||
it('checks that 1 + 1 is 2', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
})
|
||||
16
Seasoned.Frontend/vitest.config.ts
Normal file
16
Seasoned.Frontend/vitest.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/@exodus\/bytes/, /html-encoding-sniffer/],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
27
Seasoned.Tests/Seasoned.Tests.csproj
Normal file
27
Seasoned.Tests/Seasoned.Tests.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Seasoned.Backend\Seasoned.Backend.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
47
Seasoned.Tests/UnitTest1.cs
Normal file
47
Seasoned.Tests/UnitTest1.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Seasoned.Backend.Controllers;
|
||||
using Seasoned.Backend.Services;
|
||||
using Seasoned.Backend.DTOs;
|
||||
|
||||
namespace Seasoned.Tests;
|
||||
|
||||
public class RecipeControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ParseRecipe_ReturnsOk_WhenImageIsValid()
|
||||
{
|
||||
// 1. Arrange: Create a "Fake" Service
|
||||
var mockService = new Mock<IRecipeService>();
|
||||
var fakeRecipe = new RecipeResponseDto { Title = "Test Recipe" };
|
||||
|
||||
mockService.Setup(s => s.ParseImageAsync(It.IsAny<IFormFile>()))
|
||||
.ReturnsAsync(fakeRecipe);
|
||||
|
||||
var controller = new RecipeController(mockService.Object);
|
||||
|
||||
// Create a fake image file
|
||||
var content = "fake image content";
|
||||
var fileName = "test.jpg";
|
||||
var ms = new MemoryStream();
|
||||
var writer = new StreamWriter(ms);
|
||||
writer.Write(content);
|
||||
writer.Flush();
|
||||
ms.Position = 0;
|
||||
var mockFile = new FormFile(ms, 0, ms.Length, "id_from_form", fileName);
|
||||
|
||||
// 2. Act: Call the Controller
|
||||
var result = await controller.ParseRecipe(mockFile);
|
||||
|
||||
// 3. Assert: Check the result
|
||||
var okResult = result as OkObjectResult;
|
||||
okResult.Should().NotBeNull();
|
||||
okResult!.StatusCode.Should().Be(200);
|
||||
|
||||
var returnedRecipe = okResult.Value as RecipeResponseDto;
|
||||
returnedRecipe!.Title.Should().Be("Test Recipe");
|
||||
}
|
||||
}
|
||||
7
Seasoned.code-workspace
Normal file
7
Seasoned.code-workspace
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
]
|
||||
}
|
||||
30
Seasoned.sln
Normal file
30
Seasoned.sln
Normal file
@@ -0,0 +1,30 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seasoned.Tests", "Seasoned.Tests\Seasoned.Tests.csproj", "{FEF19240-CC2D-9BD4-7AC8-277372525020}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seasoned.Backend", "Seasoned.Backend\Seasoned.Backend.csproj", "{874CAB7B-A43B-C80D-494C-C243C69AEE7A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FEF19240-CC2D-9BD4-7AC8-277372525020}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{874CAB7B-A43B-C80D-494C-C243C69AEE7A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {ED9A436F-5793-4685-B738-3AA5653D4AF1}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
</div>
|
||||
</template>
|
||||
34
compose.yaml
Normal file
34
compose.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
# Database: Postgres with pgvector
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: seasoned-db
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./init-db:/docker-entrypoint-initdb.d
|
||||
|
||||
# Seasoned App: C# .NET 9 + Nuxt 4
|
||||
app:
|
||||
build: .
|
||||
container_name: seasoned-main
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:8080"
|
||||
environment:
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
- NUXT_PUBLIC_API_BASE=${NUXT_PUBLIC_API_BASE}
|
||||
- ConnectionStrings__DefaultConnection=Host=db;Database=${DB_NAME};Username=${DB_USER};Password=${DB_PASSWORD}
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- ASPNETCORE_HTTP_PORTS=8080
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -1,9 +0,0 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
future: {
|
||||
compatibilityVersion: 4,
|
||||
},
|
||||
srcDir: 'app/',
|
||||
})
|
||||
17
package.json
17
package.json
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "Seasoned",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^4.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^4.6.4"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user