include webpack, build configuration for typescript

This commit is contained in:
2023-12-05 12:55:59 -06:00
parent 6476972508
commit dc8ed06663
17 changed files with 175 additions and 99 deletions

View File

@@ -0,0 +1,2 @@
npm run build
docker compose build

View File

@@ -0,0 +1,2 @@
npm run build
docker compose build

View File

@@ -2,6 +2,9 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app WORKDIR /app
# install nodejs for typescript support
RUN apt-get update && apt-get -y install nodejs
ENV CLIENT_PORT=${CLIENT_PORT:-8080} ENV CLIENT_PORT=${CLIENT_PORT:-8080}
ENV AWS_S3_URL=$AWS_S3_URL ENV AWS_S3_URL=$AWS_S3_URL
ENV AWS_ACCESS_KEY=$AWS_ACCESS_KEY ENV AWS_ACCESS_KEY=$AWS_ACCESS_KEY

View File

@@ -59,13 +59,6 @@ builder.Services.AddServerSideBlazor();
// build app // build app
var app = builder.Build(); var app = builder.Build();
// apply most recent migration to db
using (IServiceScope scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<UnbinderDbContext>();
db.Database.Migrate();
}
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
@@ -83,4 +76,14 @@ app.MapDefaultControllerRoute();
Initializer.Seed(app); Initializer.Seed(app);
// apply most recent migration to db
using (IServiceScope scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<UnbinderDbContext>();
if (db.Database.GetPendingMigrations().Any())
{
db.Database.Migrate();
}
}
app.Run(); app.Run();

View File

@@ -21,10 +21,6 @@
<PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" /> <PackageReference Include="Microsoft.Identity.Web" Version="2.15.3" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="2.15.3" /> <PackageReference Include="Microsoft.Identity.Web.UI" Version="2.15.3" />
<PackageReference Include="Microsoft.SqlServer.Server" Version="1.0.0" /> <PackageReference Include="Microsoft.SqlServer.Server" Version="1.0.0" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.3.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -36,4 +36,6 @@
</div> </div>
</form> </form>
<script src="~/js/createRecipe.js"></script> @section Scripts {
<script src="~/js/createRecipe.js" type="module" asp-append-version="true"></script>
}

View File

@@ -19,4 +19,7 @@
} }
</div> </div>
</div> </div>
<script href="~/js/searchRecipes.js"></script>
@section Scripts {
<script src="~/js/searchRecipes.js" type="module" asp-append-version="true"></script>
}

View File

@@ -2,10 +2,7 @@
<head> <head>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title> <title>@ViewBag.Title</title>
<script type="text/javascript">var exports = {};</script>
<script src="~/js/site.js"></script>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="~/lib/pdf.js/pdf.min.mjs"></script>
<link href="~/css/site.css" rel="stylesheet" /> <link href="~/css/site.css" rel="stylesheet" />
<base href="/" /> <base href="/" />
</head> </head>
@@ -16,4 +13,6 @@
</main> </main>
<partial name="_Footer" /> <partial name="_Footer" />
</body> </body>
@RenderSection("Scripts", required: true)
</html> </html>

View File

@@ -4,18 +4,15 @@
"description": "", "description": "",
"main": "wwwroot/js/index.js", "main": "wwwroot/js/index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Mikayla Dobson", "author": "Mikayla Dobson",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"eslint": "^8.55.0", "ts-loader": "^9.5.1",
"eslint-config-semistandard": "^17.0.0", "typescript": "^5.3.2",
"eslint-config-standard": "^17.1.0", "webpack": "^5.89.0",
"eslint-plugin-import": "^2.29.0", "webpack-cli": "^5.1.4"
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"typescript": "^5.3.2"
} }
} }

View File

@@ -1,14 +1,13 @@
{ {
"compileOnSave": true,
"compilerOptions": { "compilerOptions": {
"noImplicitAny": false, "noImplicitAny": false,
"noEmitOnError": true, "noEmitOnError": true,
"removeComments": false, "removeComments": false,
"sourceMap": true, "sourceMap": true,
"lib": ["ES2015", "DOM"], "esModuleInterop": true,
"module": "CommonJS", "moduleResolution": "Bundler",
"module": "ESNext",
"target": "ES5", "target": "ES5",
"moduleResolution": "Node",
"outDir": "wwwroot/js" "outDir": "wwwroot/js"
}, },
"include": [ "include": [

View File

@@ -0,0 +1,38 @@
const path = require('path');
const fs = require('fs');
const jsRoot = path.resolve(__dirname, 'wwwroot/js');
if (fs.existsSync(jsRoot)) {
fs.rmdirSync(jsRoot, { recursive: true });
}
module.exports = {
entry: {
searchRecipes: "./wwwroot/ts/searchRecipes.ts",
createRecipe: "./wwwroot/ts/createRecipe.ts",
"types/validators": "./wwwroot/ts/types/validators.ts",
"types/models": "./wwwroot/ts/types/models.ts",
types: "./wwwroot/ts/types/index.ts",
},
mode: 'production',
optimization: {
minimize: false,
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
]
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'wwwroot/js'),
}
}

View File

@@ -1,4 +1,6 @@
async function getAllIngredients () { console.log("imported createRecipe");
async function getAllIngredients() {
const ingredients = await fetch('/api/ingredients'); const ingredients = await fetch('/api/ingredients');
return await ingredients.json(); return await ingredients.json();
} }

View File

@@ -1,52 +1,70 @@
import { isRecipeArray, type Recipe } from "./types"; import { isRecipeArray } from "./types/validators";
import type { Recipe } from "./types/models";
function setSearchResultMessage(model: Recipe[]): void {
const message = document.getElementById("query-message");
message.innerHTML = `Viewing ${model.length} results for: ${window.location.search.split("=")[1]}`;
}
async function handleSearch(textField?: HTMLInputElement) {
if (!textField) {
console.log("NO TEXT FIELD!");
return;
}
const newResult = await fetch(`/api/recipe/search?q=${textField.value}`);
const newResultJson = await newResult.json();
const searchResultsContainer = document.getElementById("search-results");
if (isRecipeArray(newResultJson)) {
let updatedHTML = "";
for (const recipe of newResultJson) {
updatedHTML += `
<p>${recipe.Name}</p>
<p>${recipe.ShortDescription}</p>
`;
}
console.log(updatedHTML);
setSearchResultMessage(newResultJson);
searchResultsContainer.innerHTML = updatedHTML;
}
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log("Loaded DOM"); console.log("Loaded searchRecipes.ts");
// dom elements // dom elements
const searchButton = document.getElementById("execute-search"); const searchButton = document.getElementById("execute-search");
const message = document.getElementById("query-message");
const inputNodes = document.querySelectorAll("input");
let textField: HTMLInputElement | null = null; let textField: HTMLInputElement | null = null;
const inputNodes = document.querySelectorAll("input");
inputNodes.forEach(node => { // functions
if (node.id == "new-search-query") { function setSearchResultMessage(model: Recipe[], query?: string): void {
textField = node; message.innerHTML = `Viewing ${model.length} results for: ${query ?? window.location.search.split("=")[1]}`;
console.log("Found text field"); }
function checkForTextField() {
inputNodes.forEach(node => {
if (node.id == "new-search-query") {
console.log('found text field');
textField = node;
}
})
}
async function handleSearch() {
console.log("__SEARCH HANDLER__");
if (!textField) {
checkForTextField();
if (!textField) {
console.log("text field was not found :(");
return;
}
} }
});
// handle search updates on client side console.log("found text field, searching...");
searchButton.onclick = async () => handleSearch(textField);
const newResult = await fetch(`/api/recipe/search?q=${textField.value}`);
console.log({ newResult });
const newResultJson = await newResult.json();
console.log({ newResultJson });
const searchResultsContainer = document.getElementById("search-results");
const isRecipeArr = isRecipeArray(newResultJson);
console.log({ isRecipeArr });
if (isRecipeArr) {
let updatedHTML = "";
for (const recipe of newResultJson) {
updatedHTML += `
<p>${recipe.name}</p>
<p>${recipe.shortDescription}</p>
`;
}
console.log({ updatedHTML });
setSearchResultMessage(newResultJson, textField.value);
searchResultsContainer.innerHTML = updatedHTML;
}
}
// perform actions and attach event listeners
checkForTextField();
searchButton.onclick = async () => handleSearch();
}); });

View File

@@ -1,2 +1,14 @@
export * from "./models"; export {
export * from "./validators"; isRecipe,
isArrayOfType,
isIngredient,
isIngredientArray,
isRecipeArray
} from "./validators";
export type {
Recipe,
Ingredient,
RecipeIngredient,
IngredientWithDetails,
} from "./models";

View File

@@ -1,25 +1,25 @@
export type Recipe = { export type Recipe = {
RecipeId: number, recipeId: number,
Name: string, name: string,
ShortDescription: string, shortDescription: string,
S3Url?: string, s3Url?: string,
Author?: string, author?: string,
RecipeText?: string, recipeText?: string,
MainImageUrl?: string, mainImageUrl?: string,
} }
export type Ingredient = { export type Ingredient = {
IngredientId: number, ingredientId: number,
Name: string, name: string,
Description?: string description?: string
} }
export type RecipeIngredient = { export type RecipeIngredient = {
RecipeIngredientId: number, recipeIngredientId: number,
RecipeId: number, recipeId: number,
IngredientId: number, ingredientId: number,
Amount?: number, amount?: number,
Unit?: string, unit?: string,
} }
export type IngredientWithDetails = Ingredient & Pick<RecipeIngredient, 'Amount' | 'Unit'> export type IngredientWithDetails = Ingredient & Pick<RecipeIngredient, 'amount' | 'unit'>

View File

@@ -4,12 +4,12 @@ export function isRecipe(input: unknown): input is Recipe {
return ( return (
typeof input == 'object' typeof input == 'object'
&& input != null && input != null
&& 'RecipeId' in input && 'recipeId' in input
&& 'Name' in input && 'name' in input
&& 'ShortDescription' in input && 'shortDescription' in input
&& (input as Recipe).RecipeId != undefined && (input as Recipe).recipeId != undefined
&& (input as Recipe).Name != undefined && (input as Recipe).name != undefined
&& (input as Recipe).ShortDescription != undefined && (input as Recipe).shortDescription != undefined
); );
} }
@@ -25,10 +25,10 @@ export function isIngredient(input: unknown): input is Ingredient {
return ( return (
typeof input == 'object' typeof input == 'object'
&& input != null && input != null
&& 'IngredientId' in input && 'ingredientId' in input
&& 'Name' in input && 'name' in input
&& (input as Ingredient).IngredientId != undefined && (input as Ingredient).ingredientId != undefined
&& (input as Ingredient).Name != undefined && (input as Ingredient).name != undefined
); );
} }