diff --git a/.gitignore b/.gitignore index b1a10ab..8e17bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -362,3 +362,5 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /content.txt + +/Notes \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 60f7113..ed15a24 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,7 +17,7 @@ // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser "serverReadyAction": { "action": "openExternally", - "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + "pattern": "\\bNow listening on:\\s+(http?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Controllers/ApiController.cs b/Controllers/ApiController.cs index 4cd72ef..88d6c5d 100644 --- a/Controllers/ApiController.cs +++ b/Controllers/ApiController.cs @@ -3,12 +3,14 @@ using BinaryDad.Notes.Services; namespace BinaryDad.Notes.Controllers { + [ApiController] public class ApiController : ControllerBase { private readonly INoteService noteService; public ApiController(INoteService noteService) => this.noteService = noteService; - public string Note() => noteService.Get(); + [Route("note/{noteName}")] + public string Note(string noteName) => noteService.GetText(noteName); } } diff --git a/Controllers/NoteController.cs b/Controllers/NoteController.cs index 53f2540..67401d4 100644 --- a/Controllers/NoteController.cs +++ b/Controllers/NoteController.cs @@ -1,4 +1,5 @@ -using BinaryDad.Notes.Services; +using BinaryDad.Notes.Models; +using BinaryDad.Notes.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -11,10 +12,24 @@ public class NoteController : Controller public NoteController(INoteService noteService) => this.noteService = noteService; - public IActionResult Index() + [Route("{noteName=default}")] + public IActionResult Index(string noteName) { - var content = noteService.Get(); + var model = new ContentModel + { + CurrentNote = noteName, + Text = noteService.GetText(noteName), + NoteNames = noteService.GetNoteNames() + }; - return View((object)content); + return View(model); + } + + [Route("{noteName}/delete")] + public IActionResult Delete(string noteName) + { + noteService.DeleteNote(noteName); + + return Redirect("/"); } } diff --git a/Models/ContentModel.cs b/Models/ContentModel.cs new file mode 100644 index 0000000..3631056 --- /dev/null +++ b/Models/ContentModel.cs @@ -0,0 +1,8 @@ +namespace BinaryDad.Notes.Models; + +public class ContentModel +{ + public string CurrentNote { get; set; } = string.Empty; + public ICollection NoteNames { get; set; } = new List(); + public string Text { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Models/ErrorViewModel.cs b/Models/ErrorViewModel.cs deleted file mode 100644 index 8e3c889..0000000 --- a/Models/ErrorViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace BinaryDad.Notes.Models; - -public class ErrorViewModel -{ - public string? RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); -} diff --git a/NoteContext.cs b/NoteContext.cs new file mode 100644 index 0000000..eaed12f --- /dev/null +++ b/NoteContext.cs @@ -0,0 +1,7 @@ +namespace BinaryDad.Notes +{ + public static class NoteContext + { + public static IDictionary ClientNotes { get; } = new Dictionary(); + } +} \ No newline at end of file diff --git a/NoteHub.cs b/NoteHub.cs index 9293513..50d615a 100644 --- a/NoteHub.cs +++ b/NoteHub.cs @@ -14,11 +14,29 @@ namespace BinaryDad.Notes this.noteService = noteService; } - public async Task SaveNote(string content) + public override Task OnConnectedAsync() { - noteService.Save(content); + var noteName = Context.GetHttpContext().Request.Query["noteName"]; - await Clients.Others.SendAsync("updateNote", content); + NoteContext.ClientNotes[Context.ConnectionId] = noteName; + + return base.OnConnectedAsync(); + } + + public async Task SaveNote(string content, string? noteName) + { + noteService.SaveText(content, noteName); + + // find all other connections except for the current one + var clientConnections = NoteContext.ClientNotes + .Where(c => c.Value == noteName && c.Key != Context.ConnectionId) + .Select(c => c.Key) + .ToList(); + + // update note for all other clients + await Clients + .Clients(clientConnections) + .SendAsync("updateNote", content); } } } diff --git a/Services/FileNoteService.cs b/Services/FileNoteService.cs index cd662be..cc8ffb2 100644 --- a/Services/FileNoteService.cs +++ b/Services/FileNoteService.cs @@ -2,21 +2,64 @@ { public class FileNoteService : INoteService { - private readonly string? filePath; + private readonly string folderPath; + + private static readonly string defaultFolderPath = "notes"; public FileNoteService(IConfiguration configuration) { - filePath = configuration["ContentFilePath"]; + folderPath = configuration["FileNoteService:ContentFolder"]; + + if (string.IsNullOrWhiteSpace(folderPath)) + { + folderPath = defaultFolderPath; + } + } + + public string GetText(string noteName) + { + CheckFile(noteName); + + return File.ReadAllText(GetFilePath(noteName)); + } + + public ICollection GetNoteNames() + { + return Directory.GetFiles(folderPath) + .Select(f => Path.GetFileName(f)) + .ToList(); + } + + public void SaveText(string content, string noteName) + { + File.WriteAllText(GetFilePath(noteName), content); + } + + public void DeleteNote(string noteName) + { + var filePath = GetFilePath(noteName); + + File.Delete(filePath); + } + + private void CheckFile(string noteName) + { + var filePath = GetFilePath(noteName); // ensure initialized if (!File.Exists(filePath)) { - Save("Hi! Feel free to start typing. Everything will be saved soon after you are done typing."); + Directory.CreateDirectory(folderPath); + + SaveText("Hi! Feel free to start typing. Everything will be saved soon after you are done typing.", noteName); } } - public string Get() => File.ReadAllText(filePath); + private string GetFilePath(string noteName) + { + noteName = noteName.Trim().ToLower(); - public void Save(string content) => File.WriteAllText(filePath, content); + return Path.Combine(folderPath, noteName); + } } } diff --git a/Services/INoteService.cs b/Services/INoteService.cs index 4aac958..9ba6fc7 100644 --- a/Services/INoteService.cs +++ b/Services/INoteService.cs @@ -2,7 +2,9 @@ { public interface INoteService { - string Get(); - void Save(string content); + ICollection GetNoteNames(); + string GetText(string noteName); + void SaveText(string content, string noteName); + void DeleteNote(string noteName); } } diff --git a/Views/Note/Index.cshtml b/Views/Note/Index.cshtml index 086744f..bda3c3f 100644 --- a/Views/Note/Index.cshtml +++ b/Views/Note/Index.cshtml @@ -1,11 +1,22 @@ -@model string +@model ContentModel - + + +
+ @foreach (var note in Model.NoteNames.Order()) + { + var css = note.Equals(Model.CurrentNote, StringComparison.OrdinalIgnoreCase) ? "current" : null; + + @note + } +
Saved
Updated
-@section scripts { - - -} \ No newline at end of file +@section Scripts +{ + +} diff --git a/Views/Shared/Error.cshtml b/Views/Shared/Error.cshtml deleted file mode 100644 index a1e0478..0000000 --- a/Views/Shared/Error.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 58a263d..02823e7 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -11,9 +11,10 @@ @RenderBody() + @RenderSection("scripts", false) + - @RenderSection("scripts", false) diff --git a/appsettings.json b/appsettings.json index 46d38c3..abd5538 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,7 @@ } }, "AllowedHosts": "*", - "ContentFilePath": "content.txt" + "FileNoteService": { + "ContentFolder": "notes" + } } diff --git a/wwwroot/css/site.css b/wwwroot/css/site.css index f26867a..a761322 100644 --- a/wwwroot/css/site.css +++ b/wwwroot/css/site.css @@ -20,6 +20,28 @@ body { min-height: 100%; } +div.note-names { + position: fixed; + bottom: 5px; + left: 0; + font-size: 14px; + opacity: 0.5; +} + + div.note-names a { + color: #666; + padding-left: 10px; + text-decoration: none; + } + + div.note-names a.current { + font-weight: bold; + } + + div .note-names a:not(:last-of-type) { + border-right: 1px solid #666; + } + textarea { width: 100%; height: 100%; diff --git a/wwwroot/js/site.js b/wwwroot/js/site.js index febc9f0..0cc05d1 100644 --- a/wwwroot/js/site.js +++ b/wwwroot/js/site.js @@ -1,6 +1,6 @@ let connection = new signalR.HubConnectionBuilder() .withAutomaticReconnect() - .withUrl("/noteHub") + .withUrl(`/noteHub?noteName=${noteName}`) .build(); function start() { @@ -29,7 +29,7 @@ function saveContent($textarea) { var content = $textarea.val(); - connection.invoke('SaveNote', content).then(function () { + connection.invoke('SaveNote', content, noteName).then(function () { showToast('#saved-indicator'); }).catch(function (err) { console.error(err.toString()); @@ -52,10 +52,11 @@ $(function () { $textarea.val(content); showToast('#update-indicator'); }); + // update content after reconnected connection.onreconnected(function() { - $.get('api/note', function(content) { + $.get(`api/note/${noteName}`, function(content) { $textarea.val(content); showToast('#update-indicator'); console.log('Refreshed after disconnect');