diff --git a/Unbinder/BuildTools/build.ps1 b/Unbinder/BuildTools/build.ps1 new file mode 100644 index 0000000..3a13edc --- /dev/null +++ b/Unbinder/BuildTools/build.ps1 @@ -0,0 +1,2 @@ +npm run build +docker compose build \ No newline at end of file diff --git a/Unbinder/BuildTools/build.sh b/Unbinder/BuildTools/build.sh new file mode 100644 index 0000000..3a13edc --- /dev/null +++ b/Unbinder/BuildTools/build.sh @@ -0,0 +1,2 @@ +npm run build +docker compose build \ No newline at end of file diff --git a/Unbinder/Docker/DOTNET.Dockerfile b/Unbinder/Docker/DOTNET.Dockerfile index b088b02..ac9dc20 100644 --- a/Unbinder/Docker/DOTNET.Dockerfile +++ b/Unbinder/Docker/DOTNET.Dockerfile @@ -2,6 +2,9 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app +# install nodejs for typescript support +RUN apt-get update && apt-get -y install nodejs + ENV CLIENT_PORT=${CLIENT_PORT:-8080} ENV AWS_S3_URL=$AWS_S3_URL ENV AWS_ACCESS_KEY=$AWS_ACCESS_KEY diff --git a/Unbinder/Program.cs b/Unbinder/Program.cs index 3a3f289..c9e443c 100644 --- a/Unbinder/Program.cs +++ b/Unbinder/Program.cs @@ -59,13 +59,6 @@ builder.Services.AddServerSideBlazor(); // build app var app = builder.Build(); -// apply most recent migration to db -using (IServiceScope scope = app.Services.CreateScope()) -{ - var db = scope.ServiceProvider.GetRequiredService(); - db.Database.Migrate(); -} - // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { @@ -83,4 +76,14 @@ app.MapDefaultControllerRoute(); Initializer.Seed(app); +// apply most recent migration to db +using (IServiceScope scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + if (db.Database.GetPendingMigrations().Any()) + { + db.Database.Migrate(); + } +} + app.Run(); diff --git a/Unbinder/Unbinder.csproj b/Unbinder/Unbinder.csproj index 82a9c69..5f7ae34 100644 --- a/Unbinder/Unbinder.csproj +++ b/Unbinder/Unbinder.csproj @@ -21,10 +21,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Unbinder/Views/Recipe/Create.cshtml b/Unbinder/Views/Recipe/Create.cshtml index 40fa6af..4a77543 100644 --- a/Unbinder/Views/Recipe/Create.cshtml +++ b/Unbinder/Views/Recipe/Create.cshtml @@ -36,4 +36,6 @@ - \ No newline at end of file +@section Scripts { + +} diff --git a/Unbinder/Views/Recipe/Search.cshtml b/Unbinder/Views/Recipe/Search.cshtml index dd2850d..1fb2818 100644 --- a/Unbinder/Views/Recipe/Search.cshtml +++ b/Unbinder/Views/Recipe/Search.cshtml @@ -19,4 +19,7 @@ } - \ No newline at end of file + +@section Scripts { + +} diff --git a/Unbinder/Views/Shared/_Layout.cshtml b/Unbinder/Views/Shared/_Layout.cshtml index a7f6b71..74fa64a 100644 --- a/Unbinder/Views/Shared/_Layout.cshtml +++ b/Unbinder/Views/Shared/_Layout.cshtml @@ -2,10 +2,7 @@ @ViewBag.Title - - - @@ -16,4 +13,6 @@ + + @RenderSection("Scripts", required: true) diff --git a/Unbinder/package.json b/Unbinder/package.json index 6d8f0d4..9de1065 100644 --- a/Unbinder/package.json +++ b/Unbinder/package.json @@ -4,18 +4,15 @@ "description": "", "main": "wwwroot/js/index.js", "scripts": { - "build": "tsc", + "build": "webpack", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Mikayla Dobson", "license": "ISC", "devDependencies": { - "eslint": "^8.55.0", - "eslint-config-semistandard": "^17.0.0", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.29.0", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-promise": "^6.1.1", - "typescript": "^5.3.2" + "ts-loader": "^9.5.1", + "typescript": "^5.3.2", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4" } } diff --git a/Unbinder/tsconfig.json b/Unbinder/tsconfig.json index 147ce57..0a24e99 100644 --- a/Unbinder/tsconfig.json +++ b/Unbinder/tsconfig.json @@ -1,14 +1,13 @@ { - "compileOnSave": true, "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": true, - "lib": ["ES2015", "DOM"], - "module": "CommonJS", + "esModuleInterop": true, + "moduleResolution": "Bundler", + "module": "ESNext", "target": "ES5", - "moduleResolution": "Node", "outDir": "wwwroot/js" }, "include": [ diff --git a/Unbinder/webpack.config.js b/Unbinder/webpack.config.js new file mode 100644 index 0000000..f0b470d --- /dev/null +++ b/Unbinder/webpack.config.js @@ -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'), + } +} \ No newline at end of file diff --git a/Unbinder/wwwroot/ts/createRecipe.ts b/Unbinder/wwwroot/ts/createRecipe.ts index e3cb921..fc09210 100644 --- a/Unbinder/wwwroot/ts/createRecipe.ts +++ b/Unbinder/wwwroot/ts/createRecipe.ts @@ -1,4 +1,6 @@ -async function getAllIngredients () { +console.log("imported createRecipe"); + +async function getAllIngredients() { const ingredients = await fetch('/api/ingredients'); return await ingredients.json(); } diff --git a/Unbinder/wwwroot/ts/index.ts b/Unbinder/wwwroot/ts/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/Unbinder/wwwroot/ts/searchRecipes.ts b/Unbinder/wwwroot/ts/searchRecipes.ts index 4a093dd..2bfe929 100644 --- a/Unbinder/wwwroot/ts/searchRecipes.ts +++ b/Unbinder/wwwroot/ts/searchRecipes.ts @@ -1,52 +1,70 @@ -import { isRecipeArray, type Recipe } from "./types"; - -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 += ` -

${recipe.Name}

-

${recipe.ShortDescription}

- `; - } - - console.log(updatedHTML); - - setSearchResultMessage(newResultJson); - searchResultsContainer.innerHTML = updatedHTML; - } -} +import { isRecipeArray } from "./types/validators"; +import type { Recipe } from "./types/models"; document.addEventListener('DOMContentLoaded', () => { - console.log("Loaded DOM"); + console.log("Loaded searchRecipes.ts"); // dom elements const searchButton = document.getElementById("execute-search"); - - const inputNodes = document.querySelectorAll("input"); + const message = document.getElementById("query-message"); let textField: HTMLInputElement | null = null; + const inputNodes = document.querySelectorAll("input"); - inputNodes.forEach(node => { - if (node.id == "new-search-query") { - textField = node; - console.log("Found text field"); + // functions + function setSearchResultMessage(model: Recipe[], query?: string): void { + message.innerHTML = `Viewing ${model.length} results for: ${query ?? window.location.search.split("=")[1]}`; + } + + 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 - searchButton.onclick = async () => handleSearch(textField); + console.log("found text field, searching..."); + + 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 += ` +

${recipe.name}

+

${recipe.shortDescription}

+ `; + } + + console.log({ updatedHTML }); + + setSearchResultMessage(newResultJson, textField.value); + searchResultsContainer.innerHTML = updatedHTML; + } + } + + // perform actions and attach event listeners + checkForTextField(); + searchButton.onclick = async () => handleSearch(); }); \ No newline at end of file diff --git a/Unbinder/wwwroot/ts/types/index.ts b/Unbinder/wwwroot/ts/types/index.ts index 05df7f4..e07a6d5 100644 --- a/Unbinder/wwwroot/ts/types/index.ts +++ b/Unbinder/wwwroot/ts/types/index.ts @@ -1,2 +1,14 @@ -export * from "./models"; -export * from "./validators"; \ No newline at end of file +export { + isRecipe, + isArrayOfType, + isIngredient, + isIngredientArray, + isRecipeArray +} from "./validators"; + +export type { + Recipe, + Ingredient, + RecipeIngredient, + IngredientWithDetails, +} from "./models"; \ No newline at end of file diff --git a/Unbinder/wwwroot/ts/types/models.ts b/Unbinder/wwwroot/ts/types/models.ts index 73d47f2..dee1e63 100644 --- a/Unbinder/wwwroot/ts/types/models.ts +++ b/Unbinder/wwwroot/ts/types/models.ts @@ -1,25 +1,25 @@ export type Recipe = { - RecipeId: number, - Name: string, - ShortDescription: string, - S3Url?: string, - Author?: string, - RecipeText?: string, - MainImageUrl?: string, + recipeId: number, + name: string, + shortDescription: string, + s3Url?: string, + author?: string, + recipeText?: string, + mainImageUrl?: string, } export type Ingredient = { - IngredientId: number, - Name: string, - Description?: string + ingredientId: number, + name: string, + description?: string } export type RecipeIngredient = { - RecipeIngredientId: number, - RecipeId: number, - IngredientId: number, - Amount?: number, - Unit?: string, + recipeIngredientId: number, + recipeId: number, + ingredientId: number, + amount?: number, + unit?: string, } -export type IngredientWithDetails = Ingredient & Pick \ No newline at end of file +export type IngredientWithDetails = Ingredient & Pick \ No newline at end of file diff --git a/Unbinder/wwwroot/ts/types/validators.ts b/Unbinder/wwwroot/ts/types/validators.ts index 6c854ec..4c3d601 100644 --- a/Unbinder/wwwroot/ts/types/validators.ts +++ b/Unbinder/wwwroot/ts/types/validators.ts @@ -4,12 +4,12 @@ export function isRecipe(input: unknown): input is Recipe { return ( typeof input == 'object' && input != null - && 'RecipeId' in input - && 'Name' in input - && 'ShortDescription' in input - && (input as Recipe).RecipeId != undefined - && (input as Recipe).Name != undefined - && (input as Recipe).ShortDescription != undefined + && 'recipeId' in input + && 'name' in input + && 'shortDescription' in input + && (input as Recipe).recipeId != undefined + && (input as Recipe).name != undefined + && (input as Recipe).shortDescription != undefined ); } @@ -25,10 +25,10 @@ export function isIngredient(input: unknown): input is Ingredient { return ( typeof input == 'object' && input != null - && 'IngredientId' in input - && 'Name' in input - && (input as Ingredient).IngredientId != undefined - && (input as Ingredient).Name != undefined + && 'ingredientId' in input + && 'name' in input + && (input as Ingredient).ingredientId != undefined + && (input as Ingredient).name != undefined ); }