From 327d76cd5239981d9d035c8a773aba28f0554773 Mon Sep 17 00:00:00 2001 From: Josh Flatt Date: Sat, 29 Nov 2025 14:55:01 -0600 Subject: [PATCH] Add project files. --- Weather-App.Server/CHANGELOG.md | 8 ++ .../Controllers/WeatherForecastController.cs | 26 +++++++ Weather-App.Server/Program.cs | 28 +++++++ .../Properties/launchSettings.json | 26 +++++++ Weather-App.Server/Weather-App.Server.csproj | 26 +++++++ Weather-App.Server/Weather-App.Server.http | 6 ++ Weather-App.Server/WeatherForecast.cs | 13 ++++ .../appsettings.Development.json | 8 ++ Weather-App.Server/appsettings.json | 9 +++ Weather-App.slnx | 7 ++ weather-app.client/.gitignore | 24 ++++++ weather-app.client/CHANGELOG.md | 16 ++++ weather-app.client/README.md | 73 +++++++++++++++++++ weather-app.client/eslint.config.js | 23 ++++++ weather-app.client/index.html | 13 ++++ weather-app.client/package.json | 30 ++++++++ weather-app.client/public/vite.svg | 1 + weather-app.client/src/App.css | 11 +++ weather-app.client/src/App.tsx | 58 +++++++++++++++ weather-app.client/src/assets/react.svg | 1 + weather-app.client/src/index.css | 68 +++++++++++++++++ weather-app.client/src/main.tsx | 10 +++ weather-app.client/tsconfig.app.json | 28 +++++++ weather-app.client/tsconfig.json | 7 ++ weather-app.client/tsconfig.node.json | 26 +++++++ weather-app.client/vite.config.ts | 61 ++++++++++++++++ weather-app.client/weather-app.client.esproj | 11 +++ 27 files changed, 618 insertions(+) create mode 100644 Weather-App.Server/CHANGELOG.md create mode 100644 Weather-App.Server/Controllers/WeatherForecastController.cs create mode 100644 Weather-App.Server/Program.cs create mode 100644 Weather-App.Server/Properties/launchSettings.json create mode 100644 Weather-App.Server/Weather-App.Server.csproj create mode 100644 Weather-App.Server/Weather-App.Server.http create mode 100644 Weather-App.Server/WeatherForecast.cs create mode 100644 Weather-App.Server/appsettings.Development.json create mode 100644 Weather-App.Server/appsettings.json create mode 100644 Weather-App.slnx create mode 100644 weather-app.client/.gitignore create mode 100644 weather-app.client/CHANGELOG.md create mode 100644 weather-app.client/README.md create mode 100644 weather-app.client/eslint.config.js create mode 100644 weather-app.client/index.html create mode 100644 weather-app.client/package.json create mode 100644 weather-app.client/public/vite.svg create mode 100644 weather-app.client/src/App.css create mode 100644 weather-app.client/src/App.tsx create mode 100644 weather-app.client/src/assets/react.svg create mode 100644 weather-app.client/src/index.css create mode 100644 weather-app.client/src/main.tsx create mode 100644 weather-app.client/tsconfig.app.json create mode 100644 weather-app.client/tsconfig.json create mode 100644 weather-app.client/tsconfig.node.json create mode 100644 weather-app.client/vite.config.ts create mode 100644 weather-app.client/weather-app.client.esproj diff --git a/Weather-App.Server/CHANGELOG.md b/Weather-App.Server/CHANGELOG.md new file mode 100644 index 0000000..03c2619 --- /dev/null +++ b/Weather-App.Server/CHANGELOG.md @@ -0,0 +1,8 @@ +This file explains how Visual Studio created the project. + +The following steps were used to generate this project: +- Create new ASP\.NET Core Web API project. +- Update project file to add a reference to the frontend project and set SPA properties. +- Update `launchSettings.json` to register the SPA proxy as a startup assembly. +- Add project to the startup projects list. +- Write this file. diff --git a/Weather-App.Server/Controllers/WeatherForecastController.cs b/Weather-App.Server/Controllers/WeatherForecastController.cs new file mode 100644 index 0000000..2072be8 --- /dev/null +++ b/Weather-App.Server/Controllers/WeatherForecastController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Weather_App.Server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = + [ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + ]; + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/Weather-App.Server/Program.cs b/Weather-App.Server/Program.cs new file mode 100644 index 0000000..1b52868 --- /dev/null +++ b/Weather-App.Server/Program.cs @@ -0,0 +1,28 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.MapStaticAssets(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.MapFallbackToFile("/index.html"); + +app.Run(); diff --git a/Weather-App.Server/Properties/launchSettings.json b/Weather-App.Server/Properties/launchSettings.json new file mode 100644 index 0000000..78a73ef --- /dev/null +++ b/Weather-App.Server/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5238", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7120;http://localhost:5238", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" + } + } + } +} + diff --git a/Weather-App.Server/Weather-App.Server.csproj b/Weather-App.Server/Weather-App.Server.csproj new file mode 100644 index 0000000..7e972b6 --- /dev/null +++ b/Weather-App.Server/Weather-App.Server.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + Weather_App.Server + ..\weather-app.client + npm run dev + https://localhost:62525 + + + + + + 10.*-* + + + + + + false + + + + diff --git a/Weather-App.Server/Weather-App.Server.http b/Weather-App.Server/Weather-App.Server.http new file mode 100644 index 0000000..8e484cf --- /dev/null +++ b/Weather-App.Server/Weather-App.Server.http @@ -0,0 +1,6 @@ +@Weather_App.Server_HostAddress = http://localhost:5238 + +GET {{Weather_App.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Weather-App.Server/WeatherForecast.cs b/Weather-App.Server/WeatherForecast.cs new file mode 100644 index 0000000..ebf3239 --- /dev/null +++ b/Weather-App.Server/WeatherForecast.cs @@ -0,0 +1,13 @@ +namespace Weather_App.Server +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/Weather-App.Server/appsettings.Development.json b/Weather-App.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Weather-App.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Weather-App.Server/appsettings.json b/Weather-App.Server/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Weather-App.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Weather-App.slnx b/Weather-App.slnx new file mode 100644 index 0000000..b4983d5 --- /dev/null +++ b/Weather-App.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/weather-app.client/.gitignore b/weather-app.client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/weather-app.client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/weather-app.client/CHANGELOG.md b/weather-app.client/CHANGELOG.md new file mode 100644 index 0000000..390c29e --- /dev/null +++ b/weather-app.client/CHANGELOG.md @@ -0,0 +1,16 @@ +This file explains how Visual Studio created the project. + +The following tools were used to generate this project: +- create-vite + +The following steps were used to generate this project: +- Create react project with create-vite: `npm init --yes vite@latest weather-app.client -- --template=react-ts`. +- Update `vite.config.ts` to set up proxying and certs. +- Add `@type/node` for `vite.config.js` typing. +- Update `App` component to fetch and display weather information. +- Create project file (`weather-app.client.esproj`). +- Create `launch.json` to enable debugging. +- Add project to solution. +- Update proxy endpoint to be the backend server endpoint. +- Add project to the startup projects list. +- Write this file. diff --git a/weather-app.client/README.md b/weather-app.client/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/weather-app.client/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/weather-app.client/eslint.config.js b/weather-app.client/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/weather-app.client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/weather-app.client/index.html b/weather-app.client/index.html new file mode 100644 index 0000000..e90af27 --- /dev/null +++ b/weather-app.client/index.html @@ -0,0 +1,13 @@ + + + + + + + weather-app.client + + +
+ + + diff --git a/weather-app.client/package.json b/weather-app.client/package.json new file mode 100644 index 0000000..5c78956 --- /dev/null +++ b/weather-app.client/package.json @@ -0,0 +1,30 @@ +{ + "name": "weather-app.client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} \ No newline at end of file diff --git a/weather-app.client/public/vite.svg b/weather-app.client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/weather-app.client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/weather-app.client/src/App.css b/weather-app.client/src/App.css new file mode 100644 index 0000000..32d0d22 --- /dev/null +++ b/weather-app.client/src/App.css @@ -0,0 +1,11 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +th, td { + padding-left: 1rem; + padding-right: 1rem; +} \ No newline at end of file diff --git a/weather-app.client/src/App.tsx b/weather-app.client/src/App.tsx new file mode 100644 index 0000000..3b53b26 --- /dev/null +++ b/weather-app.client/src/App.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import './App.css'; + +interface Forecast { + date: string; + temperatureC: number; + temperatureF: number; + summary: string; +} + +function App() { + const [forecasts, setForecasts] = useState(); + + useEffect(() => { + populateWeatherData(); + }, []); + + const contents = forecasts === undefined + ?

Loading... Please refresh once the ASP.NET backend has started. See https://aka.ms/jspsintegrationreact for more details.

+ : + + + + + + + + + + {forecasts.map(forecast => + + + + + + + )} + +
DateTemp. (C)Temp. (F)Summary
{forecast.date}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
; + + return ( +
+

Weather forecast

+

This component demonstrates fetching data from the server.

+ {contents} +
+ ); + + async function populateWeatherData() { + const response = await fetch('weatherforecast'); + if (response.ok) { + const data = await response.json(); + setForecasts(data); + } + } +} + +export default App; \ No newline at end of file diff --git a/weather-app.client/src/assets/react.svg b/weather-app.client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/weather-app.client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/weather-app.client/src/index.css b/weather-app.client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/weather-app.client/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/weather-app.client/src/main.tsx b/weather-app.client/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/weather-app.client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/weather-app.client/tsconfig.app.json b/weather-app.client/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/weather-app.client/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/weather-app.client/tsconfig.json b/weather-app.client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/weather-app.client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/weather-app.client/tsconfig.node.json b/weather-app.client/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/weather-app.client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/weather-app.client/vite.config.ts b/weather-app.client/vite.config.ts new file mode 100644 index 0000000..bd7aee4 --- /dev/null +++ b/weather-app.client/vite.config.ts @@ -0,0 +1,61 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { defineConfig } from 'vite'; +import plugin from '@vitejs/plugin-react'; +import fs from 'fs'; +import path from 'path'; +import child_process from 'child_process'; +import { env } from 'process'; + +const baseFolder = + env.APPDATA !== undefined && env.APPDATA !== '' + ? `${env.APPDATA}/ASP.NET/https` + : `${env.HOME}/.aspnet/https`; + +const certificateName = "weather-app.client"; +const certFilePath = path.join(baseFolder, `${certificateName}.pem`); +const keyFilePath = path.join(baseFolder, `${certificateName}.key`); + +if (!fs.existsSync(baseFolder)) { + fs.mkdirSync(baseFolder, { recursive: true }); +} + +if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { + if (0 !== child_process.spawnSync('dotnet', [ + 'dev-certs', + 'https', + '--export-path', + certFilePath, + '--format', + 'Pem', + '--no-password', + ], { stdio: 'inherit', }).status) { + throw new Error("Could not create certificate."); + } +} + +const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : + env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'https://localhost:7120'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [plugin()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '^/weatherforecast': { + target, + secure: false + } + }, + port: parseInt(env.DEV_SERVER_PORT || '62525'), + https: { + key: fs.readFileSync(keyFilePath), + cert: fs.readFileSync(certFilePath), + } + } +}) diff --git a/weather-app.client/weather-app.client.esproj b/weather-app.client/weather-app.client.esproj new file mode 100644 index 0000000..cfdcb84 --- /dev/null +++ b/weather-app.client/weather-app.client.esproj @@ -0,0 +1,11 @@ + + + npm run dev + src\ + Vitest + + false + + $(MSBuildProjectDirectory)\dist + + \ No newline at end of file