├── .vscode
├── launch.json
└── tasks.json
├── LICENSE
├── README.md
├── client
├── babel.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── favicon.ico
│ └── index.html
└── src
│ ├── App.vue
│ ├── components
│ ├── add-answer-modal.vue
│ ├── add-question-modal.vue
│ ├── question-preview.vue
│ └── question-score.vue
│ ├── main.js
│ ├── question-hub.js
│ ├── router.js
│ └── views
│ ├── home.vue
│ └── question.vue
└── server
├── Controllers
└── QuestionController.cs
├── Hubs
└── QuestionHub.cs
├── Models
├── Answer.cs
└── Question.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── Startup.cs
├── appsettings.Development.json
├── appsettings.json
└── server.csproj
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to find out which attributes exist for C# debugging
3 | // Use hover for the description of the existing attributes
4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/server/bin/Debug/netcoreapp2.2/server.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}/server",
16 | "stopAtEntry": false,
17 | "internalConsoleOptions": "openOnSessionStart",
18 | "launchBrowser": {
19 | "enabled": true,
20 | "args": "${auto-detect-url}",
21 | "windows": {
22 | "command": "cmd.exe",
23 | "args": "/C start ${auto-detect-url}"
24 | },
25 | "osx": {
26 | "command": "open"
27 | },
28 | "linux": {
29 | "command": "xdg-open"
30 | }
31 | },
32 | "env": {
33 | "ASPNETCORE_ENVIRONMENT": "Development"
34 | },
35 | "sourceFileMap": {
36 | "/Views": "${workspaceFolder}/Views"
37 | }
38 | },
39 | {
40 | "name": ".NET Core Attach",
41 | "type": "coreclr",
42 | "request": "attach",
43 | "processId": "${command:pickProcess}"
44 | }
45 | ,]
46 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/server/server.csproj"
11 | ],
12 | "problemMatcher": "$msCompile"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Amine Smahi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # stackunderflow
2 | A minimalistic version of StackOverflow.com using an ASP.NET Core backend, and a Vue.js frontend.
3 |
4 | ### What is stackoverflow
5 | Stack Overflow is a question and answer site for professional and enthusiast programmers. for the info stackoverflow is built with ASP.NET Core *-*
6 |
7 | ## Project setup
8 | ```
9 | cd client
10 | npm install
11 | ```
12 |
13 | ### Compiles and hot-reloads for the client project
14 | ```
15 | npm run serve
16 | ```
17 |
18 | ### Compiles and minifies for production
19 | ```
20 | npm run build
21 | ```
22 |
23 | ### Run your tests
24 | ```
25 | npm run test
26 | ```
27 |
28 | ### Lints and fixes files
29 | ```
30 | npm run lint
31 | ```
32 |
33 | ### Run the web api
34 | ```
35 | cd server
36 | dotnet restore
37 | dotnet run
38 | ```
39 |
40 | ### Screenshots
41 |
42 | 
43 | 
44 |
--------------------------------------------------------------------------------
/client/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "so-signalr",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@aspnet/signalr": "^1.1.0",
12 | "vue": "^2.5.21",
13 | "vue-router": "^3.0.1"
14 | },
15 | "devDependencies": {
16 | "@fortawesome/fontawesome-free": "^5.6.3",
17 | "@vue/cli-plugin-babel": "^3.2.0",
18 | "@vue/cli-plugin-eslint": "^3.2.0",
19 | "@vue/cli-service": "^3.2.0",
20 | "@vue/eslint-config-standard": "^4.0.0",
21 | "axios": "^0.18.0",
22 | "babel-eslint": "^10.0.1",
23 | "bootstrap-vue": "^2.0.0-rc.11",
24 | "eslint": "^5.8.0",
25 | "eslint-plugin-vue": "^5.0.0",
26 | "vue-markdown": "^2.2.4",
27 | "vue-template-compiler": "^2.5.21"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Amine-Smahi/stackunderflow/b8ea5f7befdf571fc99d77da6ab889c31269b64d/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | stackunderflow
9 |
10 |
11 |
14 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/client/src/components/add-answer-modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
54 |
--------------------------------------------------------------------------------
/client/src/components/add-question-modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
56 |
--------------------------------------------------------------------------------
/client/src/components/question-preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
51 |
--------------------------------------------------------------------------------
/client/src/components/question-score.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ question.score }}
5 |
6 |
7 |
8 |
9 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/client/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App'
3 | import router from './router'
4 | import axios from 'axios'
5 | import BootstrapVue from 'bootstrap-vue'
6 | import QuestionHub from './question-hub'
7 | import 'bootstrap/dist/css/bootstrap.css'
8 | import 'bootstrap-vue/dist/bootstrap-vue.css'
9 | import '@fortawesome/fontawesome-free/css/all.css'
10 |
11 | Vue.config.productionTip = false
12 |
13 | // Setup axios as the Vue default $http library
14 | axios.defaults.baseURL = 'http://localhost:5100' // same as the Url the server listens to
15 | Vue.prototype.$http = axios
16 |
17 | // Install Vue extensions
18 | Vue.use(BootstrapVue)
19 | Vue.use(QuestionHub)
20 |
21 | new Vue({
22 | router,
23 | render: h => h(App)
24 | }).$mount('#app')
25 |
--------------------------------------------------------------------------------
/client/src/question-hub.js:
--------------------------------------------------------------------------------
1 | import { HubConnectionBuilder, LogLevel } from '@aspnet/signalr'
2 |
3 | export default {
4 | install (Vue) {
5 | const connection = new HubConnectionBuilder()
6 | .withUrl(`${Vue.prototype.$http.defaults.baseURL}/question-hub`)
7 | .configureLogging(LogLevel.Information)
8 | .build()
9 |
10 | const questionHub = new Vue() // use new Vue instance as an event bus
11 |
12 | // Forward hub events through the event, so we can listen for them in the Vue components
13 | connection.on('QuestionScoreChange', (questionId, score) => {
14 | questionHub.$emit('score-changed', { questionId, score })
15 | })
16 | connection.on('AnswerCountChange', (questionId, answerCount) => {
17 | questionHub.$emit('answer-count-changed', { questionId, answerCount })
18 | })
19 | connection.on('AnswerAdded', answer => {
20 | questionHub.$emit('answer-added', answer)
21 | })
22 |
23 | // Provide methods for components to send messages back to server
24 | // Make sure no invocation happens until the connection is established
25 | questionHub.questionOpened = (questionId) => {
26 | return startedPromise
27 | .then(() => connection.invoke('JoinQuestionGroup', questionId))
28 | .catch(console.error)
29 | }
30 | questionHub.questionClosed = (questionId) => {
31 | return startedPromise
32 | .then(() => connection.invoke('LeaveQuestionGroup', questionId))
33 | .catch(console.error)
34 | }
35 |
36 | // Add the hub to the Vue prototype, so every component can listen to events or send new events using this.$questionHub
37 | Vue.prototype.$questionHub = questionHub
38 |
39 | // You need to call connection.start() to establish the connection
40 | // the client wont handle reconnecting for you! Docs recommend listening onclose
41 | // and handling it there. This is the simplest of the strategies
42 | let startedPromise = null
43 | function start () {
44 | startedPromise = connection.start()
45 | .catch(err => {
46 | console.error('Failed to connect with hub', err)
47 | return new Promise((resolve, reject) => setTimeout(() => start().then(resolve).catch(reject), 5000))
48 | })
49 | return startedPromise
50 | }
51 | connection.onclose(() => start())
52 |
53 | start()
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import HomePage from '@/views/home'
4 | import QuestionPage from '@/views/question'
5 |
6 | Vue.use(Router)
7 |
8 | export default new Router({
9 | routes: [
10 | {
11 | path: '/',
12 | name: 'Home',
13 | component: HomePage
14 | },
15 | {
16 | path: '/question/:id',
17 | name: 'Question',
18 | component: QuestionPage
19 | }
20 | ]
21 | })
22 |
--------------------------------------------------------------------------------
/client/src/views/home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This totally looks like Stack Overflow
5 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/client/src/views/question.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ question.title }}
6 |
7 |
8 | {{ question.body }}
9 |
10 |
11 | -
12 | {{ answer.body }}
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
75 |
--------------------------------------------------------------------------------
/server/Controllers/QuestionController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Concurrent;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.AspNetCore.SignalR;
9 | using server.Hubs;
10 | using server.Models;
11 |
12 | namespace server.Controllers
13 | {
14 | [Route("api/[controller]")]
15 | [ApiController]
16 | public class QuestionController : ControllerBase
17 | {
18 | private readonly IHubContext hubContext;
19 | private static ConcurrentBag questions = new ConcurrentBag {
20 | new Question {
21 | Id = Guid.Parse("b00c58c0-df00-49ac-ae85-0a135f75e01b"),
22 | Title = "Welcome",
23 | Body = "Welcome to the _mini Stack Overflow_ rip-off!\nThis will help showcasing **SignalR** and its integration with **Vue**",
24 | Answers = new List{ new Answer { Body = "Sample answer" }}
25 | }
26 | };
27 |
28 | public QuestionController(IHubContext questionHub)
29 | {
30 | this.hubContext = questionHub;
31 | }
32 |
33 | [HttpGet()]
34 | public IEnumerable GetQuestions()
35 | {
36 | return questions.Where(t => !t.Deleted).Select(q => new {
37 | Id = q.Id,
38 | Title = q.Title,
39 | Body = q.Body,
40 | Score = q.Score,
41 | AnswerCount = q.Answers.Count
42 | });
43 | }
44 |
45 | [HttpGet("{id}")]
46 | public ActionResult GetQuestion(Guid id)
47 | {
48 | var question = questions.SingleOrDefault(t => t.Id == id && !t.Deleted);
49 | if (question == null) return NotFound();
50 |
51 | return new JsonResult(question);
52 | }
53 |
54 | [HttpPost()]
55 | public Question AddQuestion([FromBody]Question question)
56 | {
57 | question.Id = Guid.NewGuid();
58 | question.Deleted = false;
59 | question.Answers = new List();
60 | questions.Add(question);
61 | return question;
62 | }
63 |
64 | [HttpPost("{id}/answer")]
65 | public async Task AddAnswerAsync(Guid id, [FromBody]Answer answer)
66 | {
67 | var question = questions.SingleOrDefault(t => t.Id == id && !t.Deleted);
68 | if (question == null) return NotFound();
69 |
70 | answer.Id = Guid.NewGuid();
71 | answer.QuestionId = id;
72 | answer.Deleted = false;
73 | question.Answers.Add(answer);
74 |
75 | // Notify anyone connected to the group for this answer
76 | await this.hubContext.Clients.Group(id.ToString()).AnswerAdded(answer);
77 | // Notify every client
78 | await this.hubContext.Clients.All.AnswerCountChange(question.Id, question.Answers.Count);
79 |
80 | return new JsonResult(answer);
81 | }
82 |
83 | [HttpPatch("{id}/upvote")]
84 | public async Task UpvoteQuestionAsync(Guid id)
85 | {
86 | var question = questions.SingleOrDefault(t => t.Id == id && !t.Deleted);
87 | if (question == null) return NotFound();
88 |
89 | // Warning, this isnt really atomic!
90 | question.Score++;
91 |
92 | // Notify every client
93 | await this.hubContext.Clients.All.QuestionScoreChange(question.Id, question.Score);
94 |
95 | return new JsonResult(question);
96 | }
97 |
98 | [HttpPatch("{id}/downvote")]
99 | public async Task DownvoteQuestionAsync(Guid id)
100 | {
101 | var question = questions.SingleOrDefault(t => t.Id == id && !t.Deleted);
102 | if (question == null) return NotFound();
103 |
104 | // Warning, this isnt really atomic
105 | question.Score--;
106 |
107 | // Notify every client
108 | await this.hubContext.Clients.All.QuestionScoreChange(question.Id, question.Score);
109 |
110 | return new JsonResult(question);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/server/Hubs/QuestionHub.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.SignalR;
4 | using server.Models;
5 |
6 | namespace server.Hubs
7 | {
8 | public interface IQuestionHub
9 | {
10 | Task QuestionScoreChange(Guid questionId, int score);
11 | Task AnswerCountChange(Guid questionId, int answerCount);
12 | Task AnswerAdded(Answer answer);
13 | }
14 |
15 | public class QuestionHub: Hub
16 | {
17 | // No need to implement here the methods defined by IQuestionHub, their purpose is simply
18 | // to provide a strongly typed interface.
19 | // Users of IHubContext still have to decide to whom should the events be sent
20 | // as in: await this.hubContext.Clients.All.SendQuestionScoreChange(question.Id, question.Score);
21 |
22 | // These 2 methods will be called from the client
23 | public async Task JoinQuestionGroup(Guid questionId)
24 | {
25 | await Groups.AddToGroupAsync(Context.ConnectionId, questionId.ToString());
26 | }
27 | public async Task LeaveQuestionGroup(Guid questionId)
28 | {
29 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, questionId.ToString());
30 |
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/server/Models/Answer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace server.Models
4 | {
5 | public class Answer
6 | {
7 | public Guid Id { get; set; }
8 | public Guid QuestionId { get; set; }
9 | public string Body { get; set; }
10 | public int Score { get; set; }
11 | public bool Deleted { get;set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/server/Models/Question.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace server.Models
5 | {
6 | public class Question
7 | {
8 | public Guid Id { get; set; }
9 | public string Title { get; set; }
10 | public string Body { get; set; }
11 | public int Score { get; set; }
12 | public bool Deleted { get;set; }
13 | public List Answers { get;set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/server/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace server
12 | {
13 | public class Program
14 | {
15 | public static void Main(string[] args)
16 | {
17 | CreateWebHostBuilder(args).Build().Run();
18 | }
19 |
20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
21 | WebHost.CreateDefaultBuilder(args)
22 | .UseStartup()
23 | .UseUrls("http://localhost:5100");
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:59496",
8 | "sslPort": 44368
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "api/values",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "server": {
21 | "commandName": "Project",
22 | "launchBrowser": true,
23 | "launchUrl": "api/values",
24 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/server/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Builder;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.HttpsPolicy;
8 | using Microsoft.AspNetCore.Mvc;
9 | using Microsoft.Extensions.Configuration;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.Logging;
12 | using Microsoft.Extensions.Options;
13 | using server.Hubs;
14 |
15 | namespace server
16 | {
17 | public class Startup
18 | {
19 | public Startup(IConfiguration configuration)
20 | {
21 | Configuration = configuration;
22 | }
23 |
24 | public IConfiguration Configuration { get; }
25 |
26 | // This method gets called by the runtime. Use this method to add services to the container.
27 | public void ConfigureServices(IServiceCollection services)
28 | {
29 | services.AddCors();
30 | services.AddSignalR();
31 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
32 | }
33 |
34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
35 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
36 | {
37 | if (env.IsDevelopment())
38 | {
39 | app.UseDeveloperExceptionPage();
40 | }
41 | else
42 | {
43 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
44 | app.UseHsts();
45 | }
46 |
47 | // Enable CORS so the Vue client can send requests
48 | app.UseCors(builder =>
49 | builder
50 | .WithOrigins("http://localhost:8080")
51 | .AllowAnyMethod()
52 | .AllowAnyHeader()
53 | .AllowCredentials()
54 | );
55 |
56 | // Register SignalR hubs
57 | app.UseSignalR(route =>
58 | {
59 | route.MapHub("/question-hub");
60 | });
61 |
62 | app.UseHttpsRedirection();
63 | app.UseMvc();
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/server/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/server/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*"
8 | }
9 |
--------------------------------------------------------------------------------
/server/server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.2
5 | InProcess
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------