├── src
└── main
│ ├── webapp
│ ├── .babelrc
│ ├── resources
│ │ ├── css
│ │ │ ├── bundle.css.map
│ │ │ ├── sprites.png
│ │ │ ├── loader-default.css
│ │ │ ├── prism.css
│ │ │ ├── my.css
│ │ │ ├── bundle.css
│ │ │ └── question.css
│ │ ├── preview.png
│ │ └── fonts
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ ├── fontawesome-webfont.woff2
│ │ │ ├── 1_daFS3X6gkNOcmGmHl7UiEAvth_LlrfE80CYdSH47w.woff2
│ │ │ ├── G-mm5mDezDSs-RvEL7XAECEAvth_LlrfE80CYdSH47w.woff2
│ │ │ ├── N2U74xxQEyaTBF6QLZRr1CEAvth_LlrfE80CYdSH47w.woff2
│ │ │ ├── Q47Ro23nlKqZrOLipd3-SyEAvth_LlrfE80CYdSH47w.woff2
│ │ │ ├── eCpfeMZI7q4jLksXVRWPQ_k_vArhqVIZ0nv9q090hN8.woff2
│ │ │ ├── fVu1p3782bqS2z-CaJvp9iEAvth_LlrfE80CYdSH47w.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPRHJTnCUrjaAm2S9z52xC3Y.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPRquHyap-BLkxbFhcQRhghg.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPTh33M2A-6X0bdu871ruAGs.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPTyJJ3dJfU6-XWVNf-DPRbs.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPYWiMMZ7xLd792ULpGE4W_Y.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPbpHcMS0zZe4mIYvDKG2oeM.woff2
│ │ │ ├── lJAvZoKA5NttpPc9yc6lPede9INZm0R8ZMJUtfOsxrw.woff2
│ │ │ ├── qkE6YsKPRiYUugBb1_QwHCEAvth_LlrfE80CYdSH47w.woff2
│ │ │ └── notoserif.css
│ ├── app
│ │ ├── services
│ │ │ ├── tag.js
│ │ │ ├── answer.js
│ │ │ ├── user.js
│ │ │ ├── question.js
│ │ │ └── request.js
│ │ ├── utils
│ │ │ ├── number-dec.js
│ │ │ ├── time-ago.js
│ │ │ └── format-str.js
│ │ ├── components
│ │ │ ├── pages
│ │ │ │ ├── home.jsx
│ │ │ │ ├── contact.jsx
│ │ │ │ ├── logout.jsx
│ │ │ │ ├── stuff.jsx
│ │ │ │ ├── question-by-tag.jsx
│ │ │ │ ├── dashboard.jsx
│ │ │ │ ├── tags.jsx
│ │ │ │ ├── signup.jsx
│ │ │ │ ├── login.jsx
│ │ │ │ ├── change-password.jsx
│ │ │ │ ├── user.jsx
│ │ │ │ ├── question.jsx
│ │ │ │ └── add-question.jsx
│ │ │ ├── layout
│ │ │ │ ├── footer.jsx
│ │ │ │ └── header.jsx
│ │ │ ├── items
│ │ │ │ ├── tag.jsx
│ │ │ │ ├── tags.jsx
│ │ │ │ ├── answers.jsx
│ │ │ │ ├── answer.jsx
│ │ │ │ ├── questions.jsx
│ │ │ │ ├── question-list-small.jsx
│ │ │ │ ├── answer-list-small.jsx
│ │ │ │ ├── question.jsx
│ │ │ │ └── vote.jsx
│ │ │ ├── utils
│ │ │ │ ├── loader.jsx
│ │ │ │ ├── language-switcher.jsx
│ │ │ │ └── user-sign.jsx
│ │ │ ├── app.jsx
│ │ │ └── index.jsx
│ │ ├── index.jsx
│ │ ├── routers
│ │ │ └── routers.jsx
│ │ ├── messages
│ │ │ ├── en.json
│ │ │ └── ru.json
│ │ └── auth.js
│ ├── webpack.config.js
│ ├── package.json
│ ├── WEB-INF
│ │ └── views
│ │ │ └── jsp
│ │ │ └── index.jsp
│ └── .eslintrc
│ ├── java
│ └── com
│ │ └── mkyong
│ │ ├── web
│ │ ├── entity
│ │ │ ├── enums
│ │ │ │ ├── VoteMark.java
│ │ │ │ ├── VoteModule.java
│ │ │ │ └── UserRole.java
│ │ │ ├── Vote.java
│ │ │ ├── Tag.java
│ │ │ ├── Answer.java
│ │ │ ├── User.java
│ │ │ └── Question.java
│ │ ├── jsonview
│ │ │ └── Views.java
│ │ ├── service
│ │ │ ├── VoteService.java
│ │ │ ├── UserService.java
│ │ │ ├── AnswerService.java
│ │ │ ├── TagService.java
│ │ │ ├── QuestionService.java
│ │ │ └── impl
│ │ │ │ ├── VoteServiceImpl.java
│ │ │ │ ├── UserServiceImpl.java
│ │ │ │ ├── AnswerServiceImpl.java
│ │ │ │ ├── TagServiecImpl.java
│ │ │ │ └── QuestionServiceImpl.java
│ │ ├── util
│ │ │ ├── CustomErrorType.java
│ │ │ ├── MD5.java
│ │ │ ├── AuthService.java
│ │ │ └── TimeAgo.java
│ │ ├── repository
│ │ │ ├── VoteRepository.java
│ │ │ ├── UserRepository.java
│ │ │ ├── AnswerRepository.java
│ │ │ ├── TagRepository.java
│ │ │ └── QuestionRepository.java
│ │ ├── model
│ │ │ ├── SearchCriteria.java
│ │ │ ├── LoginModel.java
│ │ │ ├── AnswerModel.java
│ │ │ ├── AjaxResponseBody.java
│ │ │ ├── ChangePasswordModel.java
│ │ │ ├── QuestionModel.java
│ │ │ ├── VoteModel.java
│ │ │ ├── LoginResponseBody.java
│ │ │ └── User.java
│ │ └── controller
│ │ │ ├── WelcomeController.java
│ │ │ ├── api
│ │ │ ├── TagController.java
│ │ │ ├── AuthorizationController.java
│ │ │ ├── VoteController.java
│ │ │ ├── AnswerController.java
│ │ │ ├── QuestionController.java
│ │ │ └── UserController.java
│ │ │ └── AjaxController.java
│ │ ├── servlet3
│ │ └── MyWebInitializer.java
│ │ └── config
│ │ └── SpringWebConfig.java
│ └── resources
│ ├── app.properties
│ └── logback.xml
├── .gitignore
├── LICENSE
├── README.md
└── pom.xml
/src/main/webapp/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/bundle.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"dist/bundle.css","sourceRoot":""}
--------------------------------------------------------------------------------
/src/main/webapp/resources/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/preview.png
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/css/sprites.png
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/enums/VoteMark.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity.enums;
2 |
3 | public enum VoteMark {
4 | UP,
5 | DOWN;
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/jsonview/Views.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.jsonview;
2 |
3 | public class Views {
4 | public static class Public {}
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/enums/VoteModule.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity.enums;
2 |
3 | public enum VoteModule {
4 | QUESTION,
5 | ANSWER;
6 | }
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *~
3 | target
4 | *.iml
5 | node_modules
6 | dist
7 | /.sass-cache
8 |
9 | src/main/webapp/resources/dist/bundle.js
10 | src/main/webapp/resources/js/bundle.js
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/enums/UserRole.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity.enums;
2 |
3 | /**
4 | * Created by Johnny on 06.01.2017.
5 | */
6 | public class UserRole {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/1_daFS3X6gkNOcmGmHl7UiEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/1_daFS3X6gkNOcmGmHl7UiEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/G-mm5mDezDSs-RvEL7XAECEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/G-mm5mDezDSs-RvEL7XAECEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/N2U74xxQEyaTBF6QLZRr1CEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/N2U74xxQEyaTBF6QLZRr1CEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/Q47Ro23nlKqZrOLipd3-SyEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/Q47Ro23nlKqZrOLipd3-SyEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/eCpfeMZI7q4jLksXVRWPQ_k_vArhqVIZ0nv9q090hN8.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/eCpfeMZI7q4jLksXVRWPQ_k_vArhqVIZ0nv9q090hN8.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/fVu1p3782bqS2z-CaJvp9iEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/fVu1p3782bqS2z-CaJvp9iEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPRHJTnCUrjaAm2S9z52xC3Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPRHJTnCUrjaAm2S9z52xC3Y.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPRquHyap-BLkxbFhcQRhghg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPRquHyap-BLkxbFhcQRhghg.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPTh33M2A-6X0bdu871ruAGs.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPTh33M2A-6X0bdu871ruAGs.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPTyJJ3dJfU6-XWVNf-DPRbs.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPTyJJ3dJfU6-XWVNf-DPRbs.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPYWiMMZ7xLd792ULpGE4W_Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPYWiMMZ7xLd792ULpGE4W_Y.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPbpHcMS0zZe4mIYvDKG2oeM.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPbpHcMS0zZe4mIYvDKG2oeM.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPede9INZm0R8ZMJUtfOsxrw.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/lJAvZoKA5NttpPc9yc6lPede9INZm0R8ZMJUtfOsxrw.woff2
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/qkE6YsKPRiYUugBb1_QwHCEAvth_LlrfE80CYdSH47w.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/noveogroup-amorgunov/spring-mvc-react/HEAD/src/main/webapp/resources/fonts/qkE6YsKPRiYUugBb1_QwHCEAvth_LlrfE80CYdSH47w.woff2
--------------------------------------------------------------------------------
/src/main/webapp/app/services/tag.js:
--------------------------------------------------------------------------------
1 | import Request from './request';
2 |
3 | class TagService {
4 | get() {
5 | const request = new Request();
6 | return request.get('tags');
7 | }
8 |
9 | };
10 |
11 | export default TagService;
12 | export { TagService };
13 |
--------------------------------------------------------------------------------
/src/main/webapp/app/utils/number-dec.js:
--------------------------------------------------------------------------------
1 | export default function declOfNum(number, titles) {
2 | const cases = [2, 0, 1, 1, 1, 2];
3 | return titles[ (number%100>4 && number%100<20)? 2 : cases[(number%10<5)?number%10:5] ];
4 | }
5 |
6 | // declOfNum(count, ['найдена', 'найдено', 'найдены']);
--------------------------------------------------------------------------------
/src/main/webapp/app/services/answer.js:
--------------------------------------------------------------------------------
1 | import Request from './request';
2 |
3 | class AnswerService {
4 | getByUsername(name) {
5 | const request = new Request();
6 | return request.get('answer/user/{name}', { name });
7 | }
8 |
9 | };
10 |
11 | export default AnswerService;
12 | export { AnswerService };
13 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Questions from '../items/questions';
3 |
4 | var HomePage = React.createClass({
5 | render() {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 | });
13 |
14 | export default HomePage;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/VoteService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service;
2 |
3 | import com.mkyong.web.entity.Vote;
4 | import java.util.List;
5 |
6 | public interface VoteService {
7 | Vote addVote(Vote vote);
8 | void delete(long id);
9 | Vote getById(Long id);
10 | Vote editVote(Vote vote);
11 | List getAll();
12 | }
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/util/CustomErrorType.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.util;
2 |
3 | public class CustomErrorType {
4 |
5 | private String errorMessage;
6 |
7 | public CustomErrorType(String errorMessage){
8 | this.errorMessage = errorMessage;
9 | }
10 |
11 | public String getErrorMessage() {
12 | return errorMessage;
13 | }
14 |
15 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/contact.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | var ContactPage = React.createClass({
4 | render() {
5 | return (
6 |
7 |
GOT QUESTIONS?
8 |
The easiest thing to do is post on
9 | our forums.
10 |
11 |
12 | );
13 | }
14 | });
15 |
16 | export default ContactPage;
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/logout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import auth from '../../auth';
3 | import { t } from 'localizify';
4 |
5 | const LogoutPage = React.createClass({
6 | componentDidMount() {
7 | auth.logout()
8 | },
9 |
10 | render() {
11 | return (
12 | {t('You logout succesfully')}
13 | );
14 | }
15 | });
16 |
17 | export default LogoutPage;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/UserService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service;
2 |
3 | import com.mkyong.web.entity.User;
4 | import java.util.List;
5 |
6 | public interface UserService {
7 | User addUser(User user);
8 | void delete(long id);
9 | User getByUsername(String name);
10 | User getById(Long id);
11 | User editUser(User user);
12 | List getAll();
13 | boolean isUserExist(User user);
14 | }
--------------------------------------------------------------------------------
/src/main/resources/app.properties:
--------------------------------------------------------------------------------
1 | #DB properties:
2 | db.driver=com.mysql.jdbc.Driver
3 | db.url=jdbc:mysql://localhost:3306/exportdefault_beta?characterEncoding=UTF-8&useUnicode=true
4 | db.username=root
5 | db.password=root
6 |
7 | #Hibernate Configuration:
8 | hibernate.dialect=org.hibernate.dialect.MySQLDialect
9 | hibernate.show_sql=true
10 | db.entitymanager.packages.to.scan=com.mkyong.web
11 | hibernate.hbm2ddl.auto=update
12 |
13 | jwt.secret=mysecretkey
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/AnswerService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service;
2 |
3 | import com.mkyong.web.entity.Answer;
4 | import com.mkyong.web.entity.User;
5 |
6 | import java.util.List;
7 |
8 | public interface AnswerService {
9 | Answer addAnswer(Answer answer);
10 | void delete(long id);
11 | List getByUser(User user);
12 | Answer getById(Long id);
13 | Answer editAnswer(Answer answer);
14 | List getAll();
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/webapp/app/services/user.js:
--------------------------------------------------------------------------------
1 | import Request from './request';
2 |
3 | class UserService {
4 | getByUsername(name) {
5 | const request = new Request();
6 | return request.get('users/name/{username}', { username: name });
7 | }
8 |
9 | changePassword(data) {
10 | const request = new Request();
11 | return request.post('user/changepassword', {}, data);
12 | }
13 |
14 | };
15 |
16 | export default UserService;
17 | export { UserService };
18 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/TagService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service;
2 |
3 | import com.mkyong.web.entity.Tag;
4 |
5 | import java.util.List;
6 |
7 | public interface TagService {
8 | Tag addTag(Tag tag);
9 | void delete(long id);
10 | Tag getByName(String name);
11 | Tag getById(Long id);
12 | Tag editTag(Tag tag);
13 |
14 | List getAll();
15 | // List findAllByOrderByPopularDesc();
16 | List getByCharacters(String searchTerm);
17 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/components/layout/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { t } from 'localizify';
3 |
4 | var Footer = React.createClass({
5 | render() {
6 | return (
7 |
12 | );
13 | }
14 | });
15 |
16 | export default Footer;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/repository/VoteRepository.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.repository;
2 |
3 | import com.mkyong.web.entity.Question;
4 | import com.mkyong.web.entity.Vote;
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 | import org.springframework.data.jpa.repository.Query;
7 | import org.springframework.data.repository.query.Param;
8 |
9 | public interface VoteRepository extends JpaRepository {
10 | @Query("select t from Vote t where t.id = :id")
11 | Vote findById(@Param("id") Long id);
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/tag.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { Link } from 'react-router';
4 | import { t } from 'localizify';
5 |
6 | const Tag = React.createClass({
7 | render() {
8 | const { name } = this.props.data;
9 |
10 | return (
11 |
15 | {name}
16 |
17 | );
18 | }
19 | });
20 |
21 | export default Tag;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/QuestionService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service;
2 |
3 | import com.mkyong.web.entity.Question;
4 | import com.mkyong.web.entity.Tag;
5 | import com.mkyong.web.entity.User;
6 | import java.util.List;
7 |
8 | public interface QuestionService {
9 | Question addQuestion(Question question);
10 | void delete(long id);
11 | List getByUser(User user);
12 | List getByTag(Tag tag);
13 | Question getById(Long id);
14 | Question editQuestion(Question question);
15 | List getAll();
16 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/tags.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tag from './tag';
3 |
4 | const Tags = React.createClass({
5 | render() {
6 | const { data } = this.props;
7 |
8 | if (!data[0].name) {
9 | return ();
10 | }
11 |
12 | return (
13 |
14 | {data.map((item, index) =>
15 |
16 |
17 |
18 | )}
19 |
20 | );
21 | }
22 | });
23 |
24 | export default Tags;
25 |
--------------------------------------------------------------------------------
/src/main/webapp/app/services/question.js:
--------------------------------------------------------------------------------
1 | import Request from './request';
2 |
3 | class QuestionService {
4 | getByUsername(name) {
5 | const request = new Request();
6 | return request.get('questions/user/{name}', { name });
7 | }
8 |
9 | getByTag(name) {
10 | const request = new Request();
11 | return request.get('questions/tag/{name}', { name });
12 | }
13 |
14 | get() {
15 | const request = new Request();
16 | return request.get('questions');
17 | }
18 |
19 | };
20 |
21 | export default QuestionService;
22 | export { QuestionService };
23 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/utils/loader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { Link } from 'react-router';
4 |
5 | const Loader = React.createClass({
6 | render() {
7 | const { isActive } = this.props.isActive;
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | }
22 | });
23 |
24 | export default Loader;
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/stuff.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | var StuffPage = React.createClass({
4 | render() {
5 | return (
6 |
7 |
STUFF
8 |
Mauris sem velit, vehicula eget sodales vitae,
9 | rhoncus eget sapien:
10 |
11 | - Nulla pulvinar diam
12 | - Facilisis bibendum
13 | - Vestibulum vulputate
14 | - Eget erat
15 | - Id porttitor
16 |
17 |
18 | );
19 | }
20 | });
21 |
22 | export default StuffPage;
23 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/SearchCriteria.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class SearchCriteria {
4 |
5 | String username;
6 | String email;
7 |
8 | public String getUsername() {
9 | return username;
10 | }
11 |
12 | public void setUsername(String username) {
13 | this.username = username;
14 | }
15 |
16 | public String getEmail() {
17 | return email;
18 | }
19 |
20 | public void setEmail(String email) {
21 | this.email = email;
22 | }
23 |
24 | @Override
25 | public String toString() {
26 | return "SearchCriteria [username=" + username + ", email=" + email + "]";
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.repository;
2 |
3 | import com.mkyong.web.entity.User;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 |
8 | public interface UserRepository extends JpaRepository {
9 |
10 | @Query("select t from User t where t.username = :username")
11 | User findByUsername(@Param("username") String username);
12 |
13 | @Query("select t from User t where t.id = :id")
14 | User findById(@Param("id") Long id);
15 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/question-by-tag.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DocumentTitle from 'react-document-title';
3 | import { withRouter } from 'react-router';
4 | import { t } from 'localizify';
5 |
6 | import Questions from '../items/questions';
7 |
8 | const QuestionsByTagPage = withRouter(
9 | React.createClass({
10 | render() {
11 | const tag = this.props.params.name;
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 | })
20 | )
21 |
22 | export default QuestionsByTagPage;
23 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/servlet3/MyWebInitializer.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.servlet3;
2 |
3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
4 |
5 | import com.mkyong.config.SpringWebConfig;
6 |
7 | public class MyWebInitializer extends
8 | AbstractAnnotationConfigDispatcherServletInitializer {
9 |
10 | @Override
11 | protected Class>[] getServletConfigClasses() {
12 | return new Class[] { SpringWebConfig.class };
13 | }
14 |
15 | @Override
16 | protected String[] getServletMappings() {
17 | return new String[] { "/" };
18 | }
19 |
20 | @Override
21 | protected Class>[] getRootConfigClasses() {
22 | return null;
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/util/MD5.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.util;
2 |
3 | public class MD5 {
4 |
5 | public static String getHash(String md5) {
6 | try {
7 | java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
8 | byte[] array = md.digest(md5.getBytes());
9 | StringBuffer sb = new StringBuffer();
10 | for (int i = 0; i < array.length; ++i) {
11 | sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1,3));
12 | }
13 | return sb.toString();
14 | } catch (java.security.NoSuchAlgorithmException e) {
15 | }
16 | return null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/LoginModel.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class LoginModel {
4 | String username;
5 | String password;
6 |
7 | public LoginModel() {
8 | }
9 |
10 | public LoginModel(String username, String password) {
11 | this.username = username;
12 | this.password = password;
13 | }
14 |
15 | public String getUsername() {
16 | return username;
17 | }
18 |
19 | public void setUsername(String username) {
20 | this.username = username;
21 | }
22 |
23 | public String getPassword() {
24 | return password;
25 | }
26 |
27 | public void setPassword(String password) {
28 | this.password = password;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/webapp/app/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import $ from 'jquery';
4 | import { Router, useRouterHistory /*, browserHistory*/ } from 'react-router';
5 | import localizify from 'localizify';
6 | import { createHistory } from 'history';
7 |
8 | import en from './messages/en.json';
9 | import ru from './messages/ru.json';
10 |
11 | import routes from 'routers/routers.jsx';
12 |
13 | localizify
14 | .add('en', en)
15 | .add('ru', ru)
16 | .setLocale(localStorage.locale || 'en');
17 |
18 | // console.log(localizify.getLocale());
19 |
20 | const browserHistory = useRouterHistory(createHistory)({
21 | basename: window.config.basename
22 | });
23 |
24 | ReactDOM.render(
25 | ,
26 | document.getElementById('app')
27 | );
28 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/answers.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 |
4 | import Answer from './answer';
5 | import Loader from '../utils/loader';
6 |
7 | const Answers = React.createClass({
8 | // getInitialState() {
9 | // return {
10 | // questions: [],
11 | // loading: true
12 | // };
13 | // },
14 | componentDidMount() {},
15 |
16 | render() {
17 | // if (this.state.loading) {
18 | // return ( );
19 | // }
20 |
21 | const data = this.props.data;
22 |
23 | return (
24 |
25 | {data.map((item, index) =>
26 |
29 | )}
30 |
31 | );
32 | }
33 | });
34 |
35 | export default Answers;
36 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DocumentTitle from 'react-document-title';
3 | import { Link } from 'react-router';
4 | import { t } from 'localizify';
5 |
6 | import auth from '../../auth';
7 | import User from './user';
8 |
9 | const DashboardPage = React.createClass({
10 | render() {
11 | const token = auth.getToken();
12 | const name = auth.getName();
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | {t('Your token')}: {token}
20 | {t('Change password')}
21 |
22 |
23 |
24 | )
25 | }
26 | });
27 |
28 | export default DashboardPage;
--------------------------------------------------------------------------------
/src/main/webapp/app/components/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { Link } from 'react-router';
4 | import auth from '../auth';
5 |
6 | import Header from './layout/header.jsx';
7 | import Footer from './layout/footer.jsx';
8 |
9 | const App = React.createClass({
10 | getInitialState() {
11 | return {
12 | loggedIn: auth.loggedIn()
13 | }
14 | },
15 |
16 | updateAuth(loggedIn) {
17 | this.setState({
18 | loggedIn
19 | })
20 | },
21 |
22 | componentWillMount() {
23 | auth.onChange = this.updateAuth
24 | auth.login()
25 | },
26 |
27 | render() {
28 | return (
29 |
30 |
31 |
32 | {this.props.children}
33 |
34 |
35 |
36 | );
37 | }
38 | });
39 |
40 | export default App;
41 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/utils/language-switcher.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { Link } from 'react-router';
4 | import localizify, {t} from 'localizify';
5 |
6 | const LanguageSwitcher = React.createClass({
7 |
8 | getClass(locale) {
9 | return localizify.getLocale() === locale ? 'active' : '';
10 | },
11 |
12 | onChangeLocale(event) {
13 | if (!$(event.target).hasClass('active')) {
14 | const locale = $(event.target).data('locale');
15 | localStorage.locale = locale;
16 | location.reload();
17 | }
18 | },
19 |
20 | render() {
21 | return (
22 |
23 | EN
24 | RU
25 |
26 |
27 | );
28 | }
29 | });
30 |
31 | export default LanguageSwitcher;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/repository/AnswerRepository.java:
--------------------------------------------------------------------------------
1 |
2 | package com.mkyong.web.repository;
3 |
4 | import com.mkyong.web.entity.Answer;
5 | import com.mkyong.web.entity.Tag;
6 | import org.springframework.data.jpa.repository.JpaRepository;
7 | import org.springframework.data.jpa.repository.Query;
8 | import org.springframework.data.repository.query.Param;
9 |
10 | import java.util.List;
11 |
12 | public interface AnswerRepository extends JpaRepository {
13 | @Query(value = "SELECT * FROM answer t WHERE t.user_id = :id",
14 | nativeQuery=true
15 | )
16 | List findByUser(@Param("id") Long id);
17 |
18 | @Query("select t from Answer t where t.id = :id")
19 | Answer findById(@Param("id") Long id);
20 |
21 |
22 | @Query(value = "SELECT * FROM tag t WHERE LOWER(t.name) LIKE LOWER(CONCAT('%',:searchTerm, '%')) order by t.popular desc limit 10",
23 | nativeQuery=true
24 | )
25 | List findByQuestionId();
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/AnswerModel.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class AnswerModel {
4 | String message;
5 | long question_id;
6 | String token;
7 |
8 | public AnswerModel() {
9 | }
10 |
11 | public AnswerModel(String message, long question_id, String token) {
12 | this.message = message;
13 | this.question_id = question_id;
14 | this.token = token;
15 | }
16 |
17 | public String getMessage() {
18 | return message;
19 | }
20 |
21 | public void setMessage(String message) {
22 | this.message = message;
23 | }
24 |
25 | public long getQuestion_id() {
26 | return question_id;
27 | }
28 |
29 | public void setQuestion_id(long question_id) {
30 | this.question_id = question_id;
31 | }
32 |
33 | public String getToken() {
34 | return token;
35 | }
36 |
37 | public void setToken(String token) {
38 | this.token = token;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/repository/TagRepository.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.repository;
2 |
3 | import com.mkyong.web.entity.Tag;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 |
8 | import java.util.List;
9 |
10 | public interface TagRepository extends JpaRepository {
11 |
12 | @Query("select t from Tag t where t.name = :name")
13 | Tag findByName(@Param("name") String name);
14 |
15 | @Query("select t from Tag t where t.id = :id")
16 | Tag findById(@Param("id") Long id);
17 |
18 | @Query("select t from Tag t order by t.popular desc")
19 | Tag findTop5PopularTags();
20 |
21 | @Query(value = "SELECT * FROM tag t WHERE LOWER(t.name) LIKE LOWER(CONCAT('%',:searchTerm, '%')) order by t.popular desc limit 10",
22 | nativeQuery=true
23 | )
24 | List findByCharacters(@Param("searchTerm") String searchTerm);
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/AjaxResponseBody.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | import java.util.List;
4 |
5 | import com.fasterxml.jackson.annotation.JsonView;
6 | import com.mkyong.web.jsonview.Views;
7 |
8 | public class AjaxResponseBody {
9 |
10 | @JsonView(Views.Public.class)
11 | String msg;
12 | @JsonView(Views.Public.class)
13 | String code;
14 | @JsonView(Views.Public.class)
15 | List result;
16 |
17 | public String getMsg() {
18 | return msg;
19 | }
20 |
21 | public void setMsg(String msg) {
22 | this.msg = msg;
23 | }
24 |
25 | public String getCode() {
26 | return code;
27 | }
28 |
29 | public void setCode(String code) {
30 | this.code = code;
31 | }
32 |
33 | public List getResult() {
34 | return result;
35 | }
36 |
37 | public void setResult(List result) {
38 | this.result = result;
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return "AjaxResponseResult [msg=" + msg + ", code=" + code + ", result=" + result + "]";
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/repository/QuestionRepository.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.repository;
2 |
3 | import com.mkyong.web.entity.Question;
4 | import com.mkyong.web.entity.User;
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 | import org.springframework.data.jpa.repository.Query;
7 | import org.springframework.data.repository.query.Param;
8 |
9 | import java.util.List;
10 |
11 | public interface QuestionRepository extends JpaRepository {
12 |
13 | @Query(value = "SELECT * FROM question t WHERE t.user_id = :id ",
14 | nativeQuery=true
15 | )
16 | List findByUser(@Param("id") Long id);
17 |
18 | @Query("select t from Question t where t.id = :id")
19 | Question findById(@Param("id") Long id);
20 |
21 |
22 | @Query(value = "SELECT q.* FROM question q, question_tag qt, tag t WHERE qt.question_id = q.id AND qt.tag_id = t.id AND t.id = :id",
23 | nativeQuery=true
24 | )
25 | List findByTag(@Param("id") Long id);
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/utils/user-sign.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import timeAgo from '../../utils/time-ago';
3 | import { Link } from 'react-router';
4 | import { t } from 'localizify';
5 |
6 | const UserSign = React.createClass({
7 | render() {
8 | const user = this.props.data.user;
9 | const created_at = this.props.data.created_at;
10 | const text = this.props.data.text || t('Asked');
11 |
12 | return (
13 |
14 |
15 | {text} {timeAgo(created_at)}
16 |
17 |
18 |
{user.username}
19 |
20 | {user.popular}
21 |
22 |
23 |
24 | )
25 | }
26 | });
27 |
28 | export default UserSign;
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Yong Mook Kim
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 |
23 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/loader-default.css:
--------------------------------------------------------------------------------
1 | .loader{color:#fff;position:fixed;box-sizing:border-box;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;z-index:999999}.loader:after,.loader:before{box-sizing:border-box}.loader.is-active{background-color:rgba(0,0,0,0.85);width:100%;height:100%;left:0;top:0}@keyframes rotation{from{transform:rotate(0)}to{transform:rotate(359deg)}}@keyframes blink{from{opacity:.5}to{opacity:1}}.loader[data-text]:before{position:fixed;left:0;top:50%;color:currentColor;font-family:Helvetica,Arial,sans-serif;text-align:center;width:100%;font-size:14px}.loader[data-text='']:before{content:'Loading'}.loader[data-text]:not([data-text='']):before{content:attr(data-text)}.loader[data-text][data-blink]:before{animation:blink 1s linear infinite alternate}.loader-default[data-text]:before{top:calc(50% - 63px)}.loader-default:after{content:'';position:fixed;width:48px;height:48px;border:solid 8px #fff;border-left-color:transparent;border-radius:50%;top:calc(50% - 24px);left:calc(50% - 24px);animation:rotation 1s linear infinite}.loader-default[data-half]:after{border-right-color:transparent}.loader-default[data-inverse]:after{animation-direction:reverse}
2 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/impl/VoteServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service.impl;
2 |
3 | import com.mkyong.web.entity.Vote;
4 | import com.mkyong.web.repository.VoteRepository;
5 | import com.mkyong.web.service.VoteService;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.stereotype.Service;
8 |
9 | import java.util.List;
10 |
11 | @Service
12 | public class VoteServiceImpl implements VoteService {
13 | @Autowired
14 | private VoteRepository voteRepository;
15 |
16 | @Override
17 | public Vote addVote(Vote tag) {
18 | Vote savedTag = voteRepository.saveAndFlush(tag);
19 |
20 | return savedTag;
21 | }
22 |
23 | @Override
24 | public void delete(long id) {
25 | voteRepository.delete(id);
26 | }
27 |
28 | @Override
29 | public Vote getById(Long id) {
30 | return voteRepository.findById(id);
31 | }
32 |
33 | @Override
34 | public Vote editVote(Vote tag) {
35 | return voteRepository.saveAndFlush(tag);
36 | }
37 |
38 | @Override
39 | public List getAll() {
40 | return voteRepository.findAll();
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/answer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { t } from 'localizify';
3 |
4 | import declOfNum from '../../utils/number-dec';
5 | import timeAgo from '../../utils/time-ago';
6 | import formatText from '../../utils/format-str';
7 | import UserSign from '../utils/user-sign';
8 |
9 | import Vote from './vote';
10 |
11 | import { Link } from 'react-router';
12 |
13 | const Answer = React.createClass({
14 | render() {
15 | const { id, comment, created_at, user, updated_at, votes } = this.props.data;
16 |
17 | const popular = votes.filter(t => t.mark === 'UP').length - votes.filter(t => t.mark === 'DOWN').length;
18 | const popularText = declOfNum(popular, [t('vote'), t('votes'), t('votes-2')]);
19 | const html = formatText(comment);
20 |
21 | const data = { user, created_at, text: t('Answered') };
22 |
23 | return (
24 |
29 | )
30 | }
31 | });
32 |
33 | export default Answer;
34 |
--------------------------------------------------------------------------------
/src/main/webapp/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | const defaults = {
6 | entry: './app/index.jsx',
7 | output: {
8 | path: path.join(__dirname, './resources/js'),
9 | filename: 'bundle.js',
10 | },
11 | module: {
12 | loaders: [{
13 | test: /\.jsx?$/,
14 | exclude: /(node_modules|bower_components|public)/,
15 | loader: "babel"
16 | }, {
17 | test: /\.json$/,
18 | loader: 'json-loader'
19 | }],
20 | },
21 | resolve: {
22 | modulesDirectories: ['node_modules'],
23 | root: path.resolve('./app'),
24 | extensions: ['', '.js', '.jsx'],
25 | },
26 | plugins: [
27 | // Avoid publishing files when compilation fails
28 | new webpack.NoErrorsPlugin(),
29 | new webpack.ProvidePlugin({
30 | '$': 'jquery',
31 | 'jQuery': 'jquery',
32 | 'window.jQuery': 'jquery',
33 | }),
34 | // new HtmlWebpackPlugin({
35 | // template: path.join(__dirname, './app/index.html'),
36 | // filename: 'index.html',
37 | // inject: 'body'
38 | // })
39 | ],
40 | };
41 |
42 | module.exports = defaults;
43 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/ChangePasswordModel.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class ChangePasswordModel {
4 | String password;
5 | String old_password;
6 | String token;
7 |
8 | public String getPassword() {
9 | return password;
10 | }
11 |
12 | public void setPassword(String password) {
13 | this.password = password;
14 | }
15 |
16 | public String getToken() {
17 | return token;
18 | }
19 |
20 | public void setToken(String token) {
21 | this.token = token;
22 | }
23 |
24 | public String getOld_password() {
25 | return old_password;
26 | }
27 |
28 | public void setOld_password(String old_password) {
29 | this.old_password = old_password;
30 | }
31 |
32 | public ChangePasswordModel(String password, String old_password, String token) {
33 | this.password = password;
34 | this.old_password = old_password;
35 | this.token = token;
36 | }
37 |
38 | public ChangePasswordModel() {
39 |
40 | }
41 |
42 | public ChangePasswordModel(String password, String token) {
43 | this.password = password;
44 | this.token = token;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/QuestionModel.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class QuestionModel {
4 | String title;
5 | String comment;
6 | String tags;
7 | String token;
8 |
9 | public QuestionModel() {
10 | }
11 |
12 | public QuestionModel(String title, String comment, String tags) {
13 | this.title = title;
14 | this.comment = comment;
15 | this.tags = tags;
16 | }
17 |
18 | public QuestionModel(String title, String comment, String tags, String token) {
19 | this.title = title;
20 | this.comment = comment;
21 | this.tags = tags;
22 | this.token = token;
23 | }
24 |
25 | public String getToken() {
26 | return token;
27 | }
28 |
29 | public void setToken(String token) {
30 | this.token = token;
31 | }
32 |
33 | public String getTitle() {
34 | return title;
35 | }
36 |
37 | public void setTitle(String title) {
38 | this.title = title;
39 | }
40 |
41 | public String getComment() {
42 | return comment;
43 | }
44 |
45 | public void setComment(String comment) {
46 | this.comment = comment;
47 | }
48 |
49 | public String getTags() {
50 | return tags;
51 | }
52 |
53 | public void setTags(String tags) {
54 | this.tags = tags;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/index.jsx:
--------------------------------------------------------------------------------
1 | import HomePage from './pages/home.jsx';
2 | import ContactPage from './pages/contact.jsx';
3 | import AddQuestionPage from './pages/add-question.jsx';
4 | import LoginPage from './pages/login.jsx';
5 | import LogoutPage from './pages/logout.jsx';
6 | import DashboardPage from './pages/dashboard.jsx';
7 | import QuestionPage from './pages/question.jsx';
8 | import QuestionsByTagPage from './pages/question-by-tag.jsx';
9 | import TagsPage from './pages/tags.jsx';
10 | import SignupPage from './pages/signup.jsx';
11 | import UserPage from './pages/user.jsx';
12 | import ChangePasswordPage from './pages/change-password.jsx';
13 |
14 | import HeaderLayout from './layout/header.jsx';
15 | import FooterLayout from './layout/footer.jsx';
16 |
17 |
18 | export {
19 | HomePage,
20 | ContactPage,
21 | AddQuestionPage,
22 | LoginPage,
23 | LogoutPage,
24 | DashboardPage,
25 | HeaderLayout,
26 | FooterLayout,
27 | QuestionPage,
28 | QuestionsByTagPage,
29 | TagsPage,
30 | SignupPage,
31 | UserPage,
32 | ChangePasswordPage
33 | };
34 |
35 | export default {
36 | HomePage,
37 | ContactPage,
38 | AddQuestionPage,
39 | LoginPage,
40 | LogoutPage,
41 | DashboardPage,
42 | HeaderLayout,
43 | FooterLayout,
44 | QuestionPage,
45 | QuestionsByTagPage,
46 | TagsPage,
47 | SignupPage,
48 | UserPage,
49 | ChangePasswordPage
50 | };
51 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/VoteModel.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | public class VoteModel {
4 | long answer_id;
5 | long question_id;
6 | String token;
7 | String mark;
8 |
9 | public VoteModel() {
10 | }
11 |
12 | public VoteModel(long answer_id, long question_id, String token) {
13 | this.answer_id = answer_id;
14 | this.question_id = question_id;
15 | this.token = token;
16 | }
17 |
18 | public VoteModel(long answer_id, long question_id, String token, String mark) {
19 | this.answer_id = answer_id;
20 | this.question_id = question_id;
21 | this.token = token;
22 | this.mark = mark;
23 | }
24 |
25 | public long getAnswer_id() {
26 | return answer_id;
27 | }
28 |
29 | public String getMark() {
30 | return mark;
31 | }
32 |
33 | public void setMark(String mark) {
34 | this.mark = mark;
35 | }
36 |
37 | public void setAnswer_id(long answer_id) {
38 | this.answer_id = answer_id;
39 | }
40 |
41 | public long getQuestion_id() {
42 | return question_id;
43 | }
44 |
45 | public void setQuestion_id(long question_id) {
46 | this.question_id = question_id;
47 | }
48 |
49 | public String getToken() {
50 | return token;
51 | }
52 |
53 | public void setToken(String token) {
54 | this.token = token;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/impl/UserServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service.impl;
2 |
3 | import com.mkyong.web.entity.User;
4 | import com.mkyong.web.repository.UserRepository;
5 | import com.mkyong.web.service.UserService;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.stereotype.Service;
8 |
9 | import java.util.List;
10 |
11 | @Service
12 | public class UserServiceImpl implements UserService {
13 | @Autowired
14 | private UserRepository userRepository;
15 |
16 | @Override
17 | public User addUser(User tag) {
18 | User savedBank = userRepository.saveAndFlush(tag);
19 |
20 | return savedBank;
21 | }
22 |
23 | @Override
24 | public void delete(long id) {
25 | userRepository.delete(id);
26 | }
27 |
28 | @Override
29 | public User getByUsername(String name) {
30 | return userRepository.findByUsername(name);
31 | }
32 |
33 | @Override
34 | public User getById(Long id) {
35 | return userRepository.findById(id);
36 | }
37 |
38 | @Override
39 | public User editUser(User user) {
40 | return userRepository.saveAndFlush(user);
41 | }
42 |
43 | @Override
44 | public List getAll() {
45 | return userRepository.findAll();
46 | }
47 |
48 | @Override
49 | public boolean isUserExist(User user) { return getByUsername(user.getUsername()) != null; };
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/LoginResponseBody.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 |
6 | public class LoginResponseBody {
7 |
8 | @JsonView(Views.Public.class)
9 | Boolean authenticated;
10 |
11 | @JsonView(Views.Public.class)
12 | String token;
13 |
14 | @JsonView(Views.Public.class)
15 | String message;
16 |
17 | public LoginResponseBody() {
18 | }
19 |
20 | public LoginResponseBody(Boolean authenticated, String token, String message) {
21 | this.authenticated = authenticated;
22 | this.token = token;
23 | this.message = message;
24 | }
25 |
26 | public LoginResponseBody(Boolean authenticated, String token) {
27 | this.authenticated = authenticated;
28 | this.token = token;
29 | }
30 |
31 | public String getMessage() {
32 | return message;
33 | }
34 |
35 | public void setMessage(String message) {
36 | this.message = message;
37 | }
38 |
39 | public Boolean getAuthenticated() {
40 | return authenticated;
41 | }
42 |
43 | public void setAuthenticated(Boolean authenticated) {
44 | this.authenticated = authenticated;
45 | }
46 |
47 | public String getToken() {
48 | return token;
49 | }
50 |
51 | public void setToken(String token) {
52 | this.token = token;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/WelcomeController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller;
2 |
3 | import com.mkyong.web.entity.User;
4 | import com.mkyong.web.service.UserService;
5 | import com.mkyong.web.service.impl.UserServiceImpl;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Controller;
10 | import org.springframework.ui.ModelMap;
11 | import org.springframework.web.bind.annotation.RequestMapping;
12 | import org.springframework.web.bind.annotation.RequestMethod;
13 |
14 | import javax.persistence.EntityManager;
15 | import javax.persistence.EntityManagerFactory;
16 |
17 | @Controller
18 | public class WelcomeController {
19 |
20 | // private static final Logger logger =
21 | // LoggerFactory.getLogger(WelcomeController.class);
22 |
23 | @Autowired
24 | private UserService userService;
25 |
26 | @RequestMapping(value = { "/*", "/*/*" }, method = RequestMethod.GET)
27 | public String printWelcome(ModelMap model) {
28 | System.out.println("go to welcome contoller");
29 |
30 |
31 | // User user = new User();
32 | // user.setUsername("sasha");
33 | // user.setPassword("12345");
34 | // userService.addUser(user);
35 |
36 | // org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("STDOUT");
37 | // log.debug("Hello world.");
38 |
39 | // logger.debug("welcome is executed");
40 |
41 | return "index";
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/webapp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "spring-mvc-react",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "watch": "webpack --watch",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "Alexander Morgunov ",
12 | "license": "ISC",
13 | "dependencies": {
14 | "history": "^2.1.2",
15 | "jquery": "^3.1.1",
16 | "json-loader": "^0.5.4",
17 | "localizify": "^1.1.2",
18 | "prismjs": "^1.6.0",
19 | "react": "^15.4.0",
20 | "react-document-title": "^2.0.2",
21 | "react-dom": "^15.4.0",
22 | "react-router": "^3.0.0",
23 | "wolfy87-eventemitter": "^5.1.0"
24 | },
25 | "devDependencies": {
26 | "babel-core": "^6.14.0",
27 | "babel-loader": "^6.2.5",
28 | "babel-preset-es2015": "^6.14.0",
29 | "babel-preset-react": "^6.16.0",
30 | "babelify": "^7.3.0",
31 | "browserify": "^13.1.0",
32 | "chai": "^3.5.0",
33 | "eslint": "^3.5.0",
34 | "eslint-config-airbnb": "^11.1.0",
35 | "eslint-config-airbnb-base": "^7.1.0",
36 | "eslint-plugin-import": "^1.15.0",
37 | "eslint-plugin-jsx-a11y": "^2.2.2",
38 | "eslint-plugin-react": "^6.3.0",
39 | "html-webpack-plugin": "^2.24.1",
40 | "jsx-loader": "^0.13.2",
41 | "mocha": "^3.0.2",
42 | "sinon": "^1.17.6",
43 | "static": "^2.0.0",
44 | "watchify": "^3.7.0",
45 | "webpack": "^1.13.2"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/tags.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loader from '../utils/loader';
3 | import TagService from '../../services/tag';
4 | import { t } from 'localizify';
5 |
6 | import { Link } from 'react-router';
7 |
8 | const TagsPage = React.createClass({
9 | getInitialState() {
10 | return {
11 | data: [],
12 | loading: true
13 | };
14 | },
15 | componentDidMount() {
16 | const tag = this.props.tag || false;
17 | const service = new TagService();
18 |
19 | service.get().then(data => {
20 | this.setState({ data, loading: false });
21 | });
22 |
23 | },
24 | render() {
25 | if (this.state.loading) {
26 | return ( );
27 | }
28 |
29 | const { data } = this.state;
30 | console.log(data);
31 |
32 | return (
33 |
34 |
Список тегов
35 |
36 |
37 | {data.map((item, index) =>
38 |
39 | {item.name}
40 | x {item.popular}
41 |
42 | )}
43 |
44 |
45 |
46 | );
47 | }
48 | });
49 |
50 | export default TagsPage;
51 |
--------------------------------------------------------------------------------
/src/main/webapp/app/routers/routers.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, IndexRoute } from 'react-router';
3 | import App from '../components/app.jsx';
4 | import auth from '../auth';
5 |
6 | import {
7 | HomePage,
8 | LoginPage,
9 | LogoutPage,
10 | AddQuestionPage,
11 | ContactPage,
12 | DashboardPage,
13 | QuestionPage,
14 | QuestionsByTagPage,
15 | SignupPage,
16 | UserPage,
17 | ChangePasswordPage,
18 | TagsPage
19 | } from '../components/index.jsx';
20 |
21 |
22 |
23 | function requireAuth(nextState, replace) {
24 | if (!auth.loggedIn()) {
25 | replace({
26 | pathname: '/login',
27 | state: { nextPathname: nextState.location.pathname }
28 | })
29 | }
30 | }
31 |
32 | export default (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/views/jsp/index.jsp:
--------------------------------------------------------------------------------
1 | <%@page session="false"%>
2 | <%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
3 | <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%>
4 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
5 |
6 |
7 |
8 |
9 |
10 |
11 | Export default
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/impl/AnswerServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service.impl;
2 |
3 | import com.mkyong.web.entity.Answer;
4 | import com.mkyong.web.entity.User;
5 | import com.mkyong.web.repository.AnswerRepository;
6 | import com.mkyong.web.service.AnswerService;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.data.domain.Sort;
9 | import org.springframework.stereotype.Service;
10 |
11 | import java.util.List;
12 |
13 | @Service
14 | public class AnswerServiceImpl implements AnswerService {
15 | @Autowired
16 | private AnswerRepository answerRepository;
17 |
18 | @Override
19 | public Answer addAnswer(Answer answer) {
20 | Answer savedBank = answerRepository.saveAndFlush(answer);
21 |
22 | return savedBank;
23 | }
24 |
25 | @Override
26 | public void delete(long id) {
27 | answerRepository.delete(id);
28 | }
29 |
30 | @Override
31 | public List getByUser(User user) {
32 | return answerRepository.findByUser(user.getId());
33 | }
34 |
35 | @Override
36 | public Answer getById(Long id) {
37 | return answerRepository.findById(id);
38 | }
39 |
40 | @Override
41 | public Answer editAnswer(Answer answer) {
42 | return answerRepository.saveAndFlush(answer);
43 | }
44 |
45 | @Override
46 | public List getAll() {
47 | return answerRepository.findAll(sortByIdAsc());
48 | }
49 |
50 | private Sort sortByIdAsc() {
51 | return new Sort(Sort.Direction.DESC, "id");
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/questions.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { t } from 'localizify';
4 |
5 | import Question from './question';
6 | import Loader from '../utils/loader';
7 |
8 | import QuestionService from '../../services/question';
9 |
10 | const Questions = React.createClass({
11 | getInitialState() {
12 | return {
13 | questions: [],
14 | loading: true
15 | };
16 | },
17 | componentDidMount() {
18 | const tag = this.props.tag || false;
19 | const service = new QuestionService();
20 |
21 | if (tag) {
22 | service.getByTag(tag).then(questions => {
23 | this.setState({ questions, loading: false });
24 | });
25 | } else {
26 | service.get().then(questions => {
27 | this.setState({ questions, loading: false });
28 | });
29 | }
30 | },
31 | render() {
32 | if (this.state.loading) {
33 | return ( );
34 | }
35 |
36 | const data = this.state.questions;
37 | const tag = this.props.tag;
38 |
39 | if (!data || !data.length) {
40 | return ({t('Questions hasn\'t exist yet')}
);
41 | }
42 |
43 | return (
44 |
45 | { tag && (
{t('Questions by tag «{tag}»', { tag })}
) }
46 |
47 | {data.map((item, index) =>
48 |
49 |
50 |
51 | )}
52 |
53 |
54 | );
55 | }
56 | });
57 |
58 | export default Questions;
59 |
--------------------------------------------------------------------------------
/src/main/webapp/app/utils/time-ago.js:
--------------------------------------------------------------------------------
1 | import declOfNum from './number-dec';
2 | import { t } from 'localizify';
3 |
4 | const DURATION_IN_SECONDS = {
5 | epochs: ['year', 'month', 'day', 'hour', 'minute', 'second'],
6 | year: 31536000,
7 | month: 2592000,
8 | day: 86400,
9 | hour: 3600,
10 | minute: 60,
11 | second: 1
12 | };
13 |
14 | const getTranslations = () => ({
15 | year: [t('year'), t('years'), t('years 2')],
16 | month: [t('month'), t('months'), t('months 2')],
17 | day: [t('day'), t('days'), t('days 2')],
18 | hour: [t('hour'), t('hours'), t('hours 2')],
19 | minute: [t('minute'), t('minutes'), t('minutes 2')],
20 | second: [t('second'), t('seconds'), t('seconds 2')]
21 | });
22 |
23 | function getDuration(seconds) {
24 | var epoch, interval;
25 |
26 | for (var i = 0; i < DURATION_IN_SECONDS.epochs.length; i++) {
27 | epoch = DURATION_IN_SECONDS.epochs[i];
28 | interval = Math.floor(seconds / DURATION_IN_SECONDS[epoch]);
29 | if (interval >= 1) {
30 | return { interval: interval, epoch: declOfNum(interval, getTranslations()[epoch]) };
31 | }
32 | }
33 | }
34 |
35 | function timeAgo(date) {
36 | var seconds = Math.floor((new Date() - new Date(date)) / 1000);
37 | var duration = getDuration(seconds);
38 | if (!duration || !duration.interval) {
39 | return t('right now');
40 | }
41 | var suffix = ' ' + t('ago'); //(duration.interval > 1 || duration.interval === 0) ? 's' : '';
42 | return duration.interval + ' ' + duration.epoch + suffix;
43 | }
44 |
45 |
46 | export default timeAgo;
47 |
48 | // alert(timeSince('2015-09-17T18:53:23'));
49 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/signup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import auth from '../../auth';
3 | import { withRouter } from 'react-router';
4 |
5 | import { t } from 'localizify';
6 |
7 | const SignupPage = withRouter(
8 | React.createClass({
9 |
10 | getInitialState() {
11 | return {
12 | error: false,
13 | message: ''
14 | }
15 | },
16 |
17 | handleSubmit(event) {
18 | event.preventDefault()
19 |
20 | const username = this.refs.username.value
21 | const pass = this.refs.pass.value
22 |
23 | auth.register(username, pass, (loggedIn, message = t('Type wrong data"')) => {
24 | if (!loggedIn)
25 | return this.setState({ error: true, message: t(message) })
26 |
27 | const { location } = this.props
28 |
29 | if (location.state && location.state.nextPathname) {
30 | this.props.router.replace(location.state.nextPathname)
31 | } else {
32 | this.props.router.replace('/')
33 | }
34 | })
35 | },
36 |
37 | render() {
38 | return (
39 |
47 | )
48 | }
49 | })
50 | )
51 |
52 | export default SignupPage;
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/login.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import auth from '../../auth';
3 | import { withRouter } from 'react-router';
4 |
5 | import { t } from 'localizify';
6 |
7 | const LoginPage = withRouter(
8 | React.createClass({
9 |
10 | getInitialState() {
11 | return {
12 | error: false,
13 | message: ''
14 | }
15 | },
16 |
17 | handleSubmit(event) {
18 | event.preventDefault()
19 |
20 | const username = this.refs.username.value
21 | const pass = this.refs.pass.value
22 |
23 | auth.login(username, pass, (loggedIn, message = t('Type wrong data')) => {
24 | if (!loggedIn)
25 | return this.setState({ error: true, message: t(message) })
26 |
27 | const { location } = this.props
28 |
29 | if (location.state && location.state.nextPathname) {
30 | this.props.router.replace(location.state.nextPathname)
31 | } else {
32 | this.props.router.replace('/')
33 | }
34 | })
35 | },
36 |
37 | render() {
38 | return (
39 |
47 | )
48 | }
49 | })
50 | )
51 |
52 | export default LoginPage;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/impl/TagServiecImpl.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service.impl;
2 |
3 | import com.mkyong.web.entity.Question;
4 | import com.mkyong.web.entity.Tag;
5 | import com.mkyong.web.repository.TagRepository;
6 | import com.mkyong.web.service.TagService;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.data.domain.Sort;
9 | import org.springframework.stereotype.Service;
10 |
11 | import java.util.List;
12 |
13 | @Service
14 | public class TagServiecImpl implements TagService {
15 | @Autowired
16 | private TagRepository tagRepository;
17 |
18 | @Override
19 | public Tag addTag(Tag tag) {
20 | Tag savedBank = tagRepository.saveAndFlush(tag);
21 |
22 | return savedBank;
23 | }
24 |
25 | @Override
26 | public void delete(long id) {
27 | tagRepository.delete(id);
28 | }
29 |
30 | @Override
31 | public Tag getByName(String name) {
32 | return tagRepository.findByName(name);
33 | }
34 |
35 | @Override
36 | public Tag editTag(Tag tag) {
37 | return tagRepository.saveAndFlush(tag);
38 | }
39 |
40 | @Override
41 | public List getAll() {
42 | return tagRepository.findAll(sortByPopuparDesc());
43 | }
44 |
45 | private Sort sortByPopuparDesc() {
46 | return new Sort(Sort.Direction.DESC, "popular");
47 | }
48 |
49 | @Override
50 | public Tag getById(Long id) {
51 | return tagRepository.findById(id);
52 | }
53 |
54 | @Override
55 | public List getByCharacters(String searchTerm) {
56 | return tagRepository.findByCharacters(searchTerm);
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/webapp/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | // "extends": "airbnb/base",
3 | "plugins": [
4 | "react"
5 | ],
6 | "env": {
7 | "browser": true,
8 | "node": true,
9 | "es6": true,
10 | "mocha": true
11 | },
12 | "globals": {
13 | "expect": true,
14 | "sinon": true
15 | },
16 |
17 | "rules": {
18 | // disable requiring trailing commas, and whatever rule we want to override
19 | "comma-dangle": 0
20 | },
21 | "parserOptions": {
22 | "ecmaFeatures": {
23 | "react/jsx-uses-react": "error",
24 | "react/jsx-uses-vars": "error",
25 |
26 | "arrowFunctions": true,
27 | "binaryLiterals": true,
28 | "blockBindings": true,
29 | "classes": true,
30 | "defaultParams": true,
31 | "destructuring": true,
32 | "forOf": true,
33 | "generators": true,
34 | "modules": true,
35 | "objectLiteralComputedProperties": true,
36 | "objectLiteralDuplicateProperties": true,
37 | "objectLiteralShorthandMethods": true,
38 | "objectLiteralShorthandProperties": true,
39 | "octalLiterals": true,
40 | "regexUFlag": true,
41 | "regexYFlag": true,
42 | "spread": true,
43 | "superInFunctions": true,
44 | "templateStrings": true,
45 | "unicodeCodePointEscapes": true,
46 | "globalReturn": true,
47 | "jsx": true
48 | },
49 | "sourceType": "module",
50 | },
51 | "settings": {
52 | "react": {
53 | "createClass": "createClass", // Regex for Component Factory to use, default to "createClass"
54 | "pragma": "React", // Pragma to use, default to "React"
55 | "version": "15.0" // React version, default to the latest React stable release
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/model/User.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.model;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 |
6 | public class User {
7 |
8 | @JsonView(Views.Public.class)
9 | String username;
10 | String password;
11 | @JsonView(Views.Public.class)
12 | String email;
13 | @JsonView(Views.Public.class)
14 | String phone;
15 | String address;
16 |
17 | public User() {
18 | }
19 |
20 | public User(String username, String password, String email, String phone, String address) {
21 | super();
22 | this.username = username;
23 | this.password = password;
24 | this.email = email;
25 | this.phone = phone;
26 | this.address = address;
27 | }
28 |
29 | public String getUsername() {
30 | return username;
31 | }
32 |
33 | public void setUsername(String username) {
34 | this.username = username;
35 | }
36 |
37 | public String getPassword() {
38 | return password;
39 | }
40 |
41 | public void setPassword(String password) {
42 | this.password = password;
43 | }
44 |
45 | public String getEmail() {
46 | return email;
47 | }
48 |
49 | public void setEmail(String email) {
50 | this.email = email;
51 | }
52 |
53 | public String getPhone() {
54 | return phone;
55 | }
56 |
57 | public void setPhone(String phone) {
58 | this.phone = phone;
59 | }
60 |
61 | public String getAddress() {
62 | return address;
63 | }
64 |
65 | public void setAddress(String address) {
66 | this.address = address;
67 | }
68 |
69 | @Override
70 | public String toString() {
71 | return "User [username=" + username + ", password=" + password + ", email=" + email + ", phone=" + phone
72 | + ", address=" + address + "]";
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/webapp/app/utils/format-str.js:
--------------------------------------------------------------------------------
1 | var tagsToReplace = {
2 | '&': '&',
3 | '<': '<',
4 | '>': '>'
5 | };
6 |
7 | function replaceTag(tag) {
8 | return tagsToReplace[tag] || tag;
9 | }
10 |
11 | function safe_tags_replace(str) {
12 | return str.replace(/[&<>]/g, replaceTag);
13 | }
14 |
15 |
16 | export default function formatText(comment) {
17 | // bold texts
18 | // comment.match(/\*(.*?)\*/gi).map(i => {
19 | //
20 | // });
21 |
22 |
23 | // let html = safe_tags_replace(comment);
24 | let html = comment;
25 |
26 | // console.log(html);
27 |
28 | // comment.match(/```(.*?)```/gi).map(text => {
29 | const codeFragments = comment.match(/```([\s\S]*?)```/gi) || [];
30 |
31 | codeFragments.map(text => {
32 | const code = Prism.highlight(text.slice(3, -3), Prism.languages.javascript);
33 | html = html.replace(text, `${code}
`);
34 | });
35 |
36 | // console.log(html);
37 |
38 | const codeInlineFragments = html.match(/`(.*?)`/gi) || [];
39 |
40 | // console.log(codeInlineFragments);
41 |
42 | codeInlineFragments.map(text => {
43 | const code = text.slice(1, -1);
44 | html = html.replace(text, `${code}`);
45 | });
46 |
47 | const boldFragments = html.match(/\*(.*?)\*/gi) || [];
48 |
49 | boldFragments.map(text => {
50 | const code = text.slice(1, -1);
51 | html = html.replace(text, `${code}`);
52 | });
53 |
54 |
55 | // var code = "var data = 1;";
56 | // var html = Prism.highlight(code, Prism.languages.javascript);
57 | return html.replace(/\n\n/g, "
");;
58 | };
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/question-list-small.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { t } from 'localizify';
4 |
5 | import declOfNum from '../../utils/number-dec';
6 | import timeAgo from '../../utils/time-ago';
7 | import formatText from '../../utils/format-str';
8 | import UserSign from '../utils/user-sign';
9 |
10 | import Vote from './vote';
11 |
12 | import { Link } from 'react-router';
13 |
14 | const QuestionListSmallItem = React.createClass({
15 | render() {
16 | const { id, title, answers, comment, created_at, user, updated_at, votes } = this.props.data;
17 |
18 | const popular = votes.filter(t => t.mark === 'UP').length - votes.filter(t => t.mark === 'DOWN').length;
19 |
20 | return (
21 |
22 |
23 | {popular}
24 | {title}
25 | {t('Asked')} {timeAgo(created_at)}
26 |
27 | )
28 | }
29 | });
30 |
31 | const QuestionListSmall = React.createClass({
32 | componentDidMount() {},
33 |
34 | render() {
35 | const data = this.props.data;
36 |
37 | if (!data || !data.length) {
38 | return ( {t('User haven\'t questions yet')}
);
39 | }
40 |
41 | return (
42 |
43 | {data.map((item, index) =>
44 |
45 |
46 |
47 | )}
48 |
49 | );
50 | }
51 | });
52 |
53 | export default QuestionListSmall;
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring 4 MVC + ReactJS
2 |
3 | 
4 |
5 | Very light version of [stackoverflow](http://stackoverflow.com/) build by [ReactJS](https://facebook.github.io/react/) (client-side) and [Spring4](https://spring.io/) (server-side).
6 |
7 | ## Features
8 |
9 | - Authorization system (by [json web token](https://jwt.io/))
10 | - Questions, answers, users, reputation, tags and votes!
11 | - Localization in react using [localizify](https://github.com/noveogroup-amorgunov/localizify)
12 |
13 | ## Intallation
14 |
15 | **0** Clone repository!
16 |
17 | ```shell
18 | $ git clone https://github.com/noveogroup-amorgunov/spring-mvc-react.git
19 | ```
20 |
21 | **1** Change database driver (by default set for MySQL) and connections parameters (url, user and password) in `src/main/resources/app.properties`
22 |
23 | **2** Change `jwt` secret key in `src/main/resources/app.properties` too (not nessasary)
24 |
25 | **3** Create schema. After run application table will be created in auto mode. Follow example for MySQL
26 |
27 | ```sql
28 | CREATE SCHEMA `spring-mvc-react` DEFAULT CHARACTER SET utf8 ;
29 | ```
30 |
31 | **4** Install and build frontend dependencies
32 |
33 | ```shell
34 | $ cd src/main/webapp
35 | $ npm install
36 | $ npm install webpack -g # intstall webpack globally
37 | $ npm run build # build bundle.js file
38 | ```
39 |
40 | Use `npm run watch` for work in watch-mode. When you change some javascript file, here will be build new bundle.js
41 |
42 | **5** Run server
43 |
44 | ```shell
45 | $ mvn jetty:run
46 | ```
47 | Access ```http://localhost:4017/spring4ajax```
48 |
49 | To import this project into Eclipse IDE:
50 |
51 | 1. ```$ mvn eclipse:eclipse```
52 | 2. Import into Eclipse via **existing projects into workspace** option.
53 | 3. Done.
54 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/answer-list-small.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { t } from 'localizify';
4 |
5 | import declOfNum from '../../utils/number-dec';
6 | import timeAgo from '../../utils/time-ago';
7 | import formatText from '../../utils/format-str';
8 | import UserSign from '../utils/user-sign';
9 |
10 | import Vote from './vote';
11 |
12 | import { Link } from 'react-router';
13 |
14 | const AnswerListSmallItem = React.createClass({
15 | render() {
16 | const { id, comment, created_at, user, updated_at, votes } = this.props.data;
17 |
18 | const popular = votes.filter(t => t.mark === 'UP').length - votes.filter(t => t.mark === 'DOWN').length;
19 | const popularText = declOfNum(popular, [t('vote'), t('votes'), t('votes-2')]);
20 | const html = formatText(comment);
21 |
22 | const data = { user, created_at, text: t('Answered') };
23 |
24 | return (
25 |
30 | )
31 | }
32 | });
33 |
34 | const AnswerListSmall = React.createClass({
35 | componentDidMount() {},
36 |
37 | render() {
38 | const data = this.props.data;
39 |
40 | if (!data || !data.length) {
41 | return ( {t('User haven\'t questions yet')}
);
42 | }
43 |
44 | return (
45 |
46 | {data.map((item, index) =>
47 |
50 | )}
51 |
52 | );
53 | }
54 | });
55 |
56 | export default AnswerListSmall;
57 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/service/impl/QuestionServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.service.impl;
2 |
3 | import com.mkyong.web.entity.Question;
4 | import com.mkyong.web.entity.Tag;
5 | import com.mkyong.web.entity.User;
6 | import com.mkyong.web.repository.QuestionRepository;
7 | import com.mkyong.web.service.QuestionService;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.data.domain.Sort;
10 | import org.springframework.stereotype.Service;
11 | import java.util.List;
12 |
13 | @Service
14 | public class QuestionServiceImpl implements QuestionService {
15 | @Autowired
16 | private QuestionRepository questionRepository;
17 |
18 | @Override
19 | public Question addQuestion(Question tag) {
20 | Question savedBank = questionRepository.saveAndFlush(tag);
21 |
22 | return savedBank;
23 | }
24 |
25 | @Override
26 | public void delete(long id) {
27 | questionRepository.delete(id);
28 | }
29 |
30 | @Override
31 | public List getByUser(User user) {
32 | return questionRepository.findByUser(user.getId());
33 | }
34 |
35 | @Override
36 | public List getByTag(Tag tag) {
37 | return questionRepository.findByTag(tag.getId());
38 | }
39 |
40 |
41 | @Override
42 | public Question getById(Long id) {
43 | return questionRepository.findById(id);
44 | }
45 |
46 | @Override
47 | public Question editQuestion(Question user) {
48 | return questionRepository.saveAndFlush(user);
49 | }
50 |
51 | // @Override
52 | // public List getAll() {
53 | // return questionRepository.findAll();
54 | // }
55 |
56 | @Override
57 | public List getAll() {
58 | return questionRepository.findAll(sortByIdAsc());
59 | }
60 |
61 | private Sort sortByIdAsc() {
62 | return new Sort(Sort.Direction.DESC, "id");
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/layout/header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router';
3 | import { t } from 'localizify';
4 |
5 | import LanguageSwitcher from '../utils/language-switcher';
6 |
7 | var Header = React.createClass({
8 | render() {
9 | return (
10 |
40 | );
41 | }
42 | });
43 |
44 | export default Header;
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/util/AuthService.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.util;
2 |
3 | import com.sun.org.apache.xpath.internal.operations.Bool;
4 | import io.jsonwebtoken.Jwts;
5 | import org.springframework.beans.factory.annotation.Value;
6 |
7 | import java.util.Objects;
8 |
9 | public class AuthService {
10 | private String userName;
11 | private Boolean isAuth;
12 | private Boolean isAdmin;
13 |
14 | private String message;
15 |
16 | private String key;
17 |
18 |
19 |
20 | public AuthService() {
21 | isAuth = false;
22 | isAdmin = false;
23 | userName = null;
24 | }
25 |
26 | public AuthService(String token, String key1) {
27 | isAuth = false;
28 | isAdmin = false;
29 | userName = null;
30 |
31 | key = key1;
32 | verifyToken(token);
33 | }
34 |
35 |
36 | public boolean verifyToken(String token) {
37 |
38 | try {
39 | userName = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody().getSubject();
40 | isAuth = true;
41 | isAdmin = Objects.equals(userName, "admin");
42 | message = "userName equals " + userName;
43 | return true;
44 |
45 | } catch (Exception e) {
46 | isAuth = false;
47 | isAdmin = false;
48 | message = e.getMessage();
49 | return false;
50 | }
51 | }
52 |
53 |
54 | public String getMessage() {
55 | return message;
56 | }
57 |
58 | public void setMessage(String message) {
59 | this.message = message;
60 | }
61 |
62 | public Boolean getAdmin() {
63 | return isAdmin;
64 | }
65 |
66 | public void setAdmin(Boolean admin) {
67 | isAdmin = admin;
68 | }
69 |
70 | public String getUserName() {
71 | return userName;
72 | }
73 |
74 | public void setUserName(String userName) {
75 | this.userName = userName;
76 | }
77 |
78 | public Boolean getAuth() {
79 | return isAuth;
80 | }
81 |
82 | public void setAuth(Boolean auth) {
83 | isAuth = auth;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/change-password.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DocumentTitle from 'react-document-title';
3 | import { withRouter } from 'react-router';
4 | import { t } from 'localizify';
5 |
6 | import UserService from '../../services/user';
7 | import auth from '../../auth';
8 |
9 | const ChangePasswordPage = withRouter(
10 | React.createClass({
11 |
12 | getInitialState() {
13 | return {
14 | error: false,
15 | message: ''
16 | }
17 | },
18 |
19 | handleSubmit(event) {
20 | event.preventDefault()
21 |
22 | const old_password = this.refs.old_pass.value;
23 | const password = this.refs.pass.value;
24 | const token = auth.getToken();
25 |
26 |
27 | const data = { token, old_password, password };
28 |
29 | const service = new UserService();
30 | service.changePassword(data).then(response => {
31 |
32 | console.log(response);
33 |
34 | if (response.code == '404') {
35 | console.log(404);
36 | this.setState({ error: true, message: t(response.msg) });
37 | return;
38 | } else {
39 |
40 | const { location } = this.props
41 |
42 | if (location.state && location.state.nextPathname) {
43 | this.props.router.replace(location.state.nextPathname)
44 | } else {
45 | this.props.router.replace('/dashboard')
46 | }
47 | }
48 | })
49 | },
50 |
51 | render() {
52 | const token = auth.getToken();
53 | const name = auth.getName();
54 |
55 | return (
56 |
57 |
67 |
68 | )
69 | }
70 | })
71 | );
72 |
73 | export default ChangePasswordPage;
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/question.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { t } from 'localizify';
3 |
4 | import Tags from './tags';
5 |
6 | import declOfNum from '../../utils/number-dec';
7 | import timeAgo from '../../utils/time-ago';
8 |
9 | import { Link } from 'react-router';
10 |
11 | const Question = React.createClass({
12 | render() {
13 | const { id, answers, comment, title, created_at, tags, user, updated_at, votes } = this.props.data;
14 |
15 | const popular = votes.filter(t => t.mark === 'UP').length - votes.filter(t => t.mark === 'DOWN').length;
16 | const popularText = declOfNum(Math.abs(popular), [t('vote'), t('votes'), t('votes-2')]);
17 | const answersCountText = declOfNum(answers.length, [t('answer'), t('answers'), t('answers-2')]);
18 | // console.log(votes);
19 | // console.log(popular);
20 |
21 | const watchedCount = localStorage.getItem(`q${id}`) || 0;
22 | const watchedCountText = declOfNum(watchedCount, [t('show'), t('shows'), t('shows-2')]);
23 |
24 | return (
25 |
26 |
27 |
28 |
{popular}
29 |
{`${popularText}`}
30 |
31 |
32 |
{answers.length}
33 |
{`${answersCountText}`}
34 |
35 |
36 |
{watchedCount}
37 |
{watchedCountText}
38 |
39 |
40 |
41 |
{title}
42 | {tags.length ? (
) : ''}
43 |
44 | {t('Asked')} {timeAgo(created_at)}
45 | {user.username} {user.popular || 0}
46 |
47 |
48 |
49 | )
50 | }
51 | });
52 |
53 | export default Question;
54 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/TagController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.entity.Tag;
5 | import com.mkyong.web.jsonview.Views;
6 | import com.mkyong.web.service.TagService;
7 | import com.mkyong.web.util.CustomErrorType;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.http.HttpStatus;
12 | import org.springframework.http.ResponseEntity;
13 | import org.springframework.web.bind.annotation.PathVariable;
14 | import org.springframework.web.bind.annotation.RequestMapping;
15 | import org.springframework.web.bind.annotation.RequestMethod;
16 | import org.springframework.web.bind.annotation.RestController;
17 |
18 | import java.util.List;
19 |
20 | @RestController
21 | @RequestMapping("/api")
22 | public class TagController {
23 | public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);
24 |
25 | @Autowired
26 | TagService tagService; //Service which will do all data retrieval/manipulation work
27 |
28 | @JsonView(Views.Public.class)
29 | @RequestMapping(value = "/tags", method = RequestMethod.GET)
30 | public ResponseEntity> listAllQuestions() {
31 | List tags = tagService.getAll();
32 | if (tags.isEmpty()) {
33 | return new ResponseEntity(HttpStatus.NO_CONTENT);
34 | // You many decide to return HttpStatus.NOT_FOUND
35 | }
36 | return new ResponseEntity>(tags, HttpStatus.OK);
37 | }
38 |
39 | @JsonView(Views.Public.class)
40 | @RequestMapping(value = "/tag/{id}", method = RequestMethod.GET)
41 | public ResponseEntity> getQuestion(@PathVariable("id") long id) {
42 | logger.info("Fetching Tag with id {}", id);
43 | Tag tag = tagService.getById(id);
44 | if (tag == null) {
45 | logger.error("Question with id {} not found.", id);
46 | return new ResponseEntity(new CustomErrorType("Tag with id " + id
47 | + " not found"), HttpStatus.NOT_FOUND);
48 | }
49 | return new ResponseEntity(tag, HttpStatus.OK);
50 | }
51 |
52 | @JsonView(Views.Public.class)
53 | @RequestMapping(value = "/tags/{q}", method = RequestMethod.POST)
54 | public ResponseEntity> getTagsByTerm(@PathVariable("q") String q) {
55 | logger.info("Fetching Tags with search term {}", q);
56 | List tags = tagService.getByCharacters(q);
57 | return new ResponseEntity>(tags, HttpStatus.OK);
58 | }
59 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Asked": "Asked",
3 | "Created using": "Created using",
4 | "Add": "Add",
5 | "Tags": "Tags",
6 | "Profile": "Profile",
7 | "Hi": "Hi",
8 | "Logout": "Logout",
9 | "Login": "Login",
10 | "Sign up": "Sign up",
11 | "You can't vote for own answer or question": "You can't vote for own answer or question",
12 | "Show question with tag «{name}»": "Show question with tag «{name}»",
13 | "Questions hasn't exist yet": "Questions hasn't exist yet",
14 | "Questions by tag «{tag}»": "Questions by tag «{tag}»",
15 | "show questions by tag «{tag}»": "show questions by tag «{tag}»",
16 | "vote": "vote",
17 | "votes": "votes",
18 | "votes-2": "votes",
19 | "answer": "answer",
20 | "answers": "answers",
21 | "answers-2": "answers",
22 | "question": "question",
23 | "questions": "questions",
24 | "questions-2": "questions",
25 | "reputation": "reputation",
26 | "show": "show",
27 | "shows": "shows",
28 | "shows-2": "shows",
29 | "level of reputation": "level of reputation",
30 | "User haven't questions yet": "User haven't questions yet",
31 | "Answered": "Answered",
32 | "User with that name has already existed": "User with that name has already existed",
33 | "User with that name isn't exist": "User with that name isn't exist",
34 | "wrong_password": "wrong_password",
35 | "wrong old password": "wrong old password",
36 | "Change password": "Change password",
37 | "Change password 2": "Change password",
38 | "Type old password": "Type old password",
39 | "Type new password": "Type new password",
40 | "Type wrong data": "Type wrong data",
41 | "Login to system": "Login to system",
42 | "Sign up 2": "Sign up",
43 | "year": "year",
44 | "years": "years",
45 | "years 2": "years",
46 | "month": "month",
47 | "months": "months",
48 | "months 2": "months",
49 | "day": "day",
50 | "days": "days",
51 | "days 2": "days",
52 | "hour": "hour",
53 | "hours": "hours",
54 | "hours 2": "hours",
55 | "minute": "minute",
56 | "minutes": "minutes",
57 | "minutes 2": "minutes",
58 | "second": "second",
59 | "seconds": "seconds",
60 | "seconds 2": "seconds",
61 | "right now": "right now",
62 | "ago": "ago",
63 | "Add question": "Add question",
64 | "Question name": "Question name",
65 | "Description": "Description",
66 | "Add tag": "Add tag",
67 | "Type tag": "Type tag",
68 | "Add new tag": "Add new tag",
69 | "Personal page": "Personal page",
70 | "Your token": "Your token",
71 | "Change password": "Change password",
72 | "You logout succesfully": "You logout succesfully",
73 | "Share": "Share",
74 | "Report": "Report",
75 | "Answers": "Answers",
76 | "Add answer": "Add answer",
77 | "Message": "Message",
78 | "You should auth": "You should auth",
79 | "User haven't exist": "User haven't exist",
80 | "Hello": "Hello",
81 | "User's page": "User's page",
82 | "Questions": "Questions"
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/webapp/app/messages/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "Asked": "Задан",
3 | "Created using": "Разработано с помощью",
4 | "Add": "Добавить",
5 | "Tags": "Теги",
6 | "Profile": "Профиль",
7 | "Hi": "Привет",
8 | "Logout": "Выйти",
9 | "Login": "Войти",
10 | "Sign up": "Регистрация",
11 | "You can't vote for own answer or question": "Вы не можете голосовать за свой ответ или вопрос",
12 | "Show question with tag «{name}»": "показать вопросы с меткой «{name}»",
13 | "Questions hasn't exist yet": "Вопросов пока нет",
14 | "Questions by tag «{tag}»": "Вопросы с тегом «{tag}»",
15 | "show questions by tag «{tag}»": "показать вопросы с меткой «{tag}»",
16 | "vote": "голос",
17 | "votes": "голоса",
18 | "votes-2": "голосов",
19 | "answer": "ответ",
20 | "answers": "ответа",
21 | "answers-2": "ответов",
22 | "question": "вопрос",
23 | "questions": "вопроса",
24 | "questions-2": "вопросов",
25 | "reputation": "репутация",
26 | "show": "показ",
27 | "shows": "показа",
28 | "shows-2": "показов",
29 | "level of reputation": "уровень репутации",
30 | "User haven't questions yet": "Пользователь не оставлял вопросов",
31 | "Answered": "Ответ дан",
32 | "User with that name has already existed": "Пользователь с таким именем уже существует",
33 | "User with that name isn't exist": "Такого пользователя не существует",
34 | "wrong_password": "Неверный пароль",
35 | "wrong old password": "Неверный текущий пароль",
36 | "Change password": "Изменить пароль",
37 | "Change password 2": "Сменить пароль",
38 | "Type old password": "Введите старый пароль",
39 | "Type new password": "Введите новый пароль",
40 | "Type wrong data": "Введены неверные данные",
41 | "Login to system": "Войти в систему",
42 | "Sign up 2": "Зарегистрироваться",
43 | "year": "год",
44 | "years": "года",
45 | "years 2": "лет",
46 | "month": "месяц",
47 | "months": "месяца",
48 | "months 2": "месяцев",
49 | "day": "день",
50 | "days": "дня",
51 | "days 2": "дней",
52 | "hour": "час",
53 | "hours": "часа",
54 | "hours 2": "часов",
55 | "minute": "минуту",
56 | "minutes": "минуты",
57 | "minutes 2": "минут",
58 | "second": "секунду",
59 | "seconds": "секунды",
60 | "seconds 2": "секунд",
61 | "right now": "только что",
62 | "ago": "назад",
63 | "Add question": "Добавить вопрос",
64 | "Question name": "Название вопроса",
65 | "Description": "Описание",
66 | "Add tag": "Добавить метку (тэг)",
67 | "Type tag": "Введите метку",
68 | "Add new tag": "Добавить новую метку",
69 | "Personal page": "Персональная страница",
70 | "Your token": "Ваш токен",
71 | "Change password": "Сменить пароль",
72 | "You logout succesfully": "Вы успешно вышли из системы",
73 | "Share": "Поделиться",
74 | "Report": "Пожаловаться",
75 | "Answers": "Ответы",
76 | "Add answer": "Добавить ответ",
77 | "Message": "Сообщение",
78 | "You should auth": "Необходима авторизация",
79 | "User haven't exist": "Пользователя не существует",
80 | "Hello": "Привет",
81 | "User's page": "Страница пользователя",
82 | "Questions": "Вопросы"
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/webapp/app/services/request.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | class Request {
4 | constructor() {
5 | this.apikey = '123';
6 | this.baseUrl = `${window.config.basename}/api/`;
7 | }
8 |
9 | /**
10 | * serialize Object into a list of parameters
11 | * ex. obj { token: 12345, action: login } => token=12345&action=login
12 | */
13 | static getParams(o) {
14 | return Object.keys(o).map(key => `${key}=${encodeURIComponent(o[key])}`).join('&');
15 | }
16 |
17 | /**
18 | * replace params from Object in passed string
19 | * ex. str: "match/{id}", params: {id:5} => "match/5"
20 | */
21 | static replaceParams(str, params = {}) {
22 | for (const prop of Object.keys(params)) {
23 | str = str.replace(`{${prop}}`, params[prop]);
24 | }
25 | return str;
26 | }
27 |
28 | /**
29 | * get request to API with params
30 | */
31 | static request(url, params = false, data, { type }) {
32 | const paramsString = params ? Request.getParams(params) : '';
33 | const urlWithParams = `${url}?${paramsString}`;
34 |
35 | console.log(`api request to ${urlWithParams}`);
36 |
37 | data = data ? JSON.stringify(data) : false;
38 |
39 | return new Promise((resolve, reject) => {
40 | $.ajax({
41 | url: urlWithParams,
42 | type,
43 | dataType: 'json',
44 | data,
45 | contentType: 'application/json',
46 | success: data => {
47 | resolve(data);
48 | },
49 | error: (xhr, status, err) => {
50 | console.error(urlWithParams, status, err.toString());
51 | }
52 | })
53 |
54 | // request(urlWithParams, (error, response, body) => {
55 | // const { statusCode } = response;
56 | // // handle error
57 | // if (error || statusCode < 200 || statusCode > 299) {
58 | // reject(new Error(`Failed to load page with status code: ${response.statusCode}`));
59 | // }
60 | // resolve(body);
61 | // });
62 | });
63 | }
64 |
65 | /**
66 | * public method for get data from api
67 | */
68 | get(resource, options = {}, data = false) {
69 | return new Promise((resolve, reject) => {
70 | // add api_token to request
71 | const params = { api_token: this.apikey };
72 | const url = this.baseUrl + Request.replaceParams(resource, options);
73 |
74 | Request.request(url, params, data, { type: 'GET' })
75 | .then(result => resolve(result))
76 | .catch(error => reject(error));
77 | });
78 | }
79 |
80 | /**
81 | * public method for get data from api
82 | */
83 | post(resource, options = {}, data = false) {
84 | return new Promise((resolve, reject) => {
85 | // add api_token to request
86 | const params = { api_token: this.apikey };
87 | const url = this.baseUrl + Request.replaceParams(resource, options);
88 |
89 | Request.request(url, params, data, { type: 'POST' })
90 | .then(result => resolve(result))
91 | .catch(error => reject(error));
92 | });
93 | }
94 | }
95 |
96 | export default Request;
97 | export { Request };
98 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/prism.css:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript+abap+actionscript+ada+apacheconf+apl+applescript+asciidoc+aspnet+autoit+autohotkey+bash+basic+batch+c+brainfuck+bro+bison+csharp+cpp+coffeescript+ruby+css-extras+d+dart+diff+docker+eiffel+elixir+erlang+fsharp+fortran+gherkin+git+glsl+go+graphql+groovy+haml+handlebars+haskell+haxe+http+icon+inform7+ini+j+jade+java+jolie+json+julia+keyman+kotlin+latex+less+livescript+lolcode+lua+makefile+markdown+matlab+mel+mizar+monkey+nasm+nginx+nim+nix+nsis+objectivec+ocaml+oz+parigp+parser+pascal+perl+php+php-extras+powershell+processing+prolog+properties+protobuf+puppet+pure+python+q+qore+r+jsx+reason+rest+rip+roboconf+crystal+rust+sas+sass+scss+scala+scheme+smalltalk+smarty+sql+stylus+swift+tcl+textile+twig+typescript+verilog+vhdl+vim+wiki+xojo+yaml */
2 | /**
3 | * okaidia theme for JavaScript, CSS and HTML
4 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/
5 | * @author ocodia
6 | */
7 |
8 | code[class*="language-"],
9 | pre[class*="language-"] {
10 | color: #f8f8f2;
11 | background: none;
12 | text-shadow: 0 1px rgba(0, 0, 0, 0.3);
13 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.1;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | /* Code blocks */
32 | pre[class*="language-"] {
33 | padding: 10px 15px;
34 | margin: .5em 0;
35 | overflow: auto;
36 | border-radius: 0.3em;
37 | font-size: 13px;
38 | }
39 |
40 | :not(pre) > code[class*="language-"],
41 | pre[class*="language-"] {
42 | background: #272822;
43 | }
44 |
45 | /* Inline code */
46 | :not(pre) > code[class*="language-"] {
47 | padding: .1em;
48 | border-radius: .3em;
49 | white-space: normal;
50 | }
51 |
52 | .token.comment,
53 | .token.prolog,
54 | .token.doctype,
55 | .token.cdata {
56 | color: slategray;
57 | }
58 |
59 | .token.punctuation {
60 | color: #f8f8f2;
61 | }
62 |
63 | .namespace {
64 | opacity: .7;
65 | }
66 |
67 | .token.property,
68 | .token.tag,
69 | .token.constant,
70 | .token.symbol,
71 | .token.deleted {
72 | color: #f92672;
73 | }
74 |
75 | .token.boolean,
76 | .token.number {
77 | color: #ae81ff;
78 | }
79 |
80 | .token.selector,
81 | .token.attr-name,
82 | .token.string,
83 | .token.char,
84 | .token.builtin,
85 | .token.inserted {
86 | color: #a6e22e;
87 | }
88 |
89 | .token.operator,
90 | .token.entity,
91 | .token.url,
92 | .language-css .token.string,
93 | .style .token.string,
94 | .token.variable {
95 | color: #f8f8f2;
96 | }
97 |
98 | .token.atrule,
99 | .token.attr-value,
100 | .token.function {
101 | color: #e6db74;
102 | }
103 |
104 | .token.keyword {
105 | color: #66d9ef;
106 | }
107 |
108 | .token.regex,
109 | .token.important {
110 | color: #fd971f;
111 | }
112 |
113 | .token.important,
114 | .token.bold {
115 | font-weight: bold;
116 | }
117 | .token.italic {
118 | font-style: italic;
119 | }
120 |
121 | .token.entity {
122 | cursor: help;
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/Vote.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity;
2 |
3 |
4 | import com.fasterxml.jackson.annotation.JsonView;
5 | import com.mkyong.web.entity.enums.VoteMark;
6 | import com.mkyong.web.entity.enums.VoteModule;
7 | import com.mkyong.web.jsonview.Views;
8 | import org.hibernate.annotations.GenericGenerator;
9 |
10 | import javax.persistence.*;
11 | import java.util.Date;
12 | import java.util.Set;
13 |
14 | @Entity
15 | @Table(name = "vote")
16 | public class Vote {
17 |
18 | @Id
19 | @GeneratedValue(generator = "increment")
20 | @GenericGenerator(name = "increment", strategy = "increment")
21 | @Column(name = "id", length = 6, nullable = false)
22 | @JsonView(Views.Public.class)
23 | private long id;
24 |
25 | @Column(name = "module_id")
26 | @Enumerated(EnumType.STRING)
27 | @JsonView(Views.Public.class)
28 | private VoteModule module;
29 |
30 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
31 | @JoinColumn(name = "question_id", nullable = true)
32 | private Question question;
33 |
34 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
35 | @JoinColumn(name = "answer_id", nullable = true)
36 | private Answer answer;
37 |
38 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
39 | @JoinColumn(name = "user_id", nullable = false)
40 | @JsonView(Views.Public.class)
41 | private User user;
42 |
43 | @Column(name = "mark")
44 | @Enumerated(EnumType.STRING)
45 | @JsonView(Views.Public.class)
46 | private VoteMark mark;
47 |
48 | public Vote(VoteModule module, Question question, Answer answer, User user, VoteMark mark) {
49 | this.module = module;
50 | this.question = question;
51 | this.answer = answer;
52 | this.user = user;
53 | this.mark = mark;
54 | }
55 |
56 | public VoteMark getMark() {
57 | return mark;
58 | }
59 |
60 | public void setMark(VoteMark mark) {
61 | this.mark = mark;
62 | }
63 |
64 | public Vote() {
65 | }
66 |
67 | public Vote(VoteModule module, Question question, Answer answer, User user) {
68 | this.module = module;
69 | this.question = question;
70 | this.answer = answer;
71 | this.user = user;
72 | }
73 |
74 | public long getId() {
75 | return id;
76 | }
77 |
78 | public void setId(long id) {
79 | this.id = id;
80 | }
81 |
82 | public VoteModule getModule() {
83 | return module;
84 | }
85 |
86 | public void setModule(VoteModule module) {
87 | this.module = module;
88 | }
89 |
90 | public Question getQuestion() {
91 | return question;
92 | }
93 |
94 | public void setQuestion(Question question) {
95 | this.question = question;
96 | }
97 |
98 | public Answer getAnswer() {
99 | return answer;
100 | }
101 |
102 | public void setAnswer(Answer answer) {
103 | this.answer = answer;
104 | }
105 |
106 | public User getUser() {
107 | return user;
108 | }
109 |
110 | public void setUser(User user) {
111 | this.user = user;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/Tag.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 | import org.hibernate.annotations.GenericGenerator;
6 | import javax.persistence.*;
7 | import java.util.Date;
8 | import java.util.Set;
9 |
10 | @Entity
11 | @Table(name = "tag")
12 | public class Tag {
13 |
14 | @Id
15 | @GeneratedValue(generator = "increment")
16 | @GenericGenerator(name= "increment", strategy= "increment")
17 | @Column(name = "id", length = 6, nullable = false)
18 | @JsonView(Views.Public.class)
19 | private long id;
20 |
21 | @Column(name = "name", unique=true)
22 | @JsonView(Views.Public.class)
23 | private String name;
24 |
25 | @Column(name = "description")
26 | @JsonView(Views.Public.class)
27 | private String description;
28 |
29 | @Column(name = "popular")
30 | @JsonView(Views.Public.class)
31 | private Integer popular;
32 |
33 |
34 | @Column(name = "created_at")
35 | @Temporal(TemporalType.TIMESTAMP)
36 | @JsonView(Views.Public.class)
37 | private Date created_at;
38 |
39 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
40 | @JoinColumn(name = "user_id", nullable = false)
41 | private User user;
42 |
43 | @ManyToMany(fetch = FetchType.EAGER, mappedBy = "tags")
44 | private Set questions;
45 |
46 | @PrePersist
47 | protected void onCreate() {
48 | created_at = new Date();
49 | }
50 |
51 | public Tag() {
52 | }
53 |
54 | public Tag(String name, String description, User user) {
55 | this.name = name;
56 | this.description = description;
57 | this.popular = 1;
58 | this.user = user;
59 | }
60 |
61 | public Tag(String name, String description, Integer popular, Date created_at, User user, Set questions) {
62 | this.name = name;
63 | this.description = description;
64 | this.popular = popular;
65 | this.created_at = created_at;
66 | this.user = user;
67 | this.questions = questions;
68 | }
69 |
70 | public long getId() {
71 | return id;
72 | }
73 |
74 | public void setId(long id) {
75 | this.id = id;
76 | }
77 |
78 | public String getName() {
79 | return name;
80 | }
81 |
82 | public void setName(String name) {
83 | this.name = name;
84 | }
85 |
86 | public String getDescription() {
87 | return description;
88 | }
89 |
90 | public void setDescription(String description) {
91 | this.description = description;
92 | }
93 |
94 | public Integer getPopular() {
95 | return popular;
96 | }
97 |
98 | public void setPopular(Integer popular) {
99 | this.popular = popular;
100 | }
101 |
102 | public Date getCreated_at() {
103 | return created_at;
104 | }
105 |
106 | public void setCreated_at(Date created_at) {
107 | this.created_at = created_at;
108 | }
109 |
110 | public User getUser() {
111 | return user;
112 | }
113 |
114 | public void setUser(User user) {
115 | this.user = user;
116 | }
117 |
118 | public Set getQuestions() {
119 | return questions;
120 | }
121 |
122 | public void setQuestions(Set questions) {
123 | this.questions = questions;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/user.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { t } from 'localizify';
3 |
4 | import UserService from '../../services/user';
5 | import QuestionService from '../../services/question';
6 | import AnswerService from '../../services/answer';
7 |
8 | import Loader from '../utils/loader';
9 | import declOfNum from '../../utils/number-dec';
10 |
11 | import QuestionListSmall from '../items/question-list-small';
12 | import AnswerListSmall from '../items/answer-list-small';
13 |
14 | var UserPage = React.createClass({
15 | getInitialState() {
16 | return {
17 | data: {},
18 | isExist: false,
19 | loading: true
20 | };
21 | },
22 | componentDidMount() {
23 |
24 | const userName = this.props.params.name;
25 | const service = new UserService();
26 | service.getByUsername(userName).then(data => {
27 | this.setState({ loading: false });
28 | if (!data) {
29 | return;
30 | }
31 |
32 | const questionService = new QuestionService();
33 | const answerService = new AnswerService();
34 |
35 | data.questions = [];
36 | data.answers = [];
37 | this.setState({ isExist: true });
38 | this.setState({ data });
39 |
40 |
41 | Promise.all([
42 | questionService.getByUsername(userName),
43 | answerService.getByUsername(userName),
44 | ]).then(data => {
45 | const newData = Object.assign({}, this.state.data);
46 | newData.questions = data[0];
47 | newData.answers = data[1];
48 | this.setState({ data: newData });
49 | });
50 | })
51 | },
52 | render() {
53 | if (this.state.loading) {
54 | return ( );
55 | }
56 |
57 | if (!this.state.isExist) {
58 | return ( {t('User haven\'t exist')}
);
59 | }
60 |
61 | // console.log(this.state.data);
62 |
63 | const answers = this.state.data.answers || [];
64 | const questions = this.state.data.questions || [];
65 |
66 |
67 | return (
68 |
69 |
{this.props.dashboard ? t('Hello') + ', ' : t('User\'s page')} {this.state.data.username}
70 |
71 |
72 |
73 | {questions.length}
74 | {declOfNum(questions.length, [t('question'), t('questions'), t('questions-2')])}
75 |
76 |
77 | {answers.length}
78 | {declOfNum(answers.length, [t('answer'), t('answers'), t('answers-2')])}
79 |
80 |
81 | {this.state.data.popular}
82 | {declOfNum(this.state.data.popular, [t('reputation'), t('reputation'), t('reputation')])}
83 |
84 |
85 |
86 |
87 |
88 |
{t('Questions')}
89 |
90 |
91 | {/*
92 |
{t('Answer')}
93 |
94 |
*/}
95 |
96 | );
97 | }
98 | });
99 |
100 | export default UserPage;
101 |
--------------------------------------------------------------------------------
/src/main/webapp/app/components/items/vote.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { t } from 'localizify';
3 |
4 | import declOfNum from '../../utils/number-dec';
5 | import timeAgo from '../../utils/time-ago';
6 | import formatText from '../../utils/format-str';
7 |
8 | import auth from '../../auth';
9 | import UserSign from '../utils/user-sign';
10 |
11 | import { Link } from 'react-router';
12 |
13 | const Vote = React.createClass({
14 | getInitialState() {
15 | return {
16 | rating: 0,
17 | isVoted: 0,
18 | };
19 | },
20 |
21 | up(e) {
22 | event.preventDefault();
23 | console.log('up');
24 | this.request('UP');
25 | },
26 | down(e) {
27 | event.preventDefault();
28 | this.request('DOWN');
29 | },
30 |
31 | request(mark = 'UP') {
32 | if (this.isOwnUser() || this.isVoted('UP') || this.isVoted('DOWN')) {
33 | console.log('this is own user or is voted');
34 | return false;
35 | }
36 |
37 | const data = {
38 | token: auth.getToken(),
39 | mark
40 | };
41 |
42 | if (this.props.data.answerId) {
43 | data.answer_id = this.props.data.answerId
44 | } else if (this.props.data.questionId) {
45 | data.question_id = this.props.data.questionId
46 | }
47 |
48 | $.ajax({
49 | type: 'POST',
50 | url: `${window.config.basename}/api/vote`,
51 | contentType: 'application/json',
52 | data: JSON.stringify(data),
53 | success: data => {
54 | console.log(data);
55 | const sign = mark === 'UP' ? 1 : -1;
56 | if (data.msg) {
57 | this.setState({ rating: this.state.rating + sign });
58 | this.setState({ isVoted: sign });
59 | }
60 | },
61 | error: (xhr, status, err) => {
62 | console.error(status, err.toString());
63 | }
64 | });
65 | },
66 |
67 | isOwnUser() {
68 | return auth.getName() === this.props.data.user.username;
69 | },
70 |
71 | isVoted(mark = 'UP') {
72 | if (this.state.isVoted === 1 && mark === 'UP') {
73 | return true;
74 | }
75 | if (this.state.isVoted === -1 && mark === 'DOWN') {
76 | return true;
77 | }
78 |
79 | let result = false;
80 | const userName = auth.getName();
81 | if (!this.props.data.votes) {
82 | return false;
83 | }
84 | this.props.data.votes.forEach(item => {
85 | if (item.user.username == userName && item.mark == mark) {
86 | result = true;
87 | }
88 | });
89 | return result;
90 | },
91 |
92 | componentDidMount() {
93 | this.setState({ rating: this.props.data.popular });
94 | },
95 |
96 | render() {
97 | // console.log(JSON.stringify(this.props.data));
98 | const { questionId, answerId, popular, votes } = this.props.data;
99 |
100 | const classNameUp = this.state.isVoted === 1 || this.isVoted('UP') ? 'vote-up-on' : 'vote-up-off';
101 | const classNameDown = this.state.isVoted === -1 || this.isVoted('DOWN') ? 'vote-down-on' : 'vote-down-off';
102 |
103 |
104 |
105 | return (
106 |
111 | )
112 | }
113 | });
114 |
115 | export default Vote;
116 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/Answer.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 | import org.hibernate.annotations.GenericGenerator;
6 | import org.hibernate.annotations.Type;
7 |
8 | import javax.persistence.*;
9 | import java.util.Date;
10 | import java.util.Set;
11 |
12 | @Entity
13 | @Table(name = "answer")
14 | public class Answer {
15 | @Id
16 | @GeneratedValue(generator = "increment")
17 | @GenericGenerator(name= "increment", strategy= "increment")
18 | @Column(name = "id", length = 6, nullable = false)
19 | @JsonView(Views.Public.class)
20 | private long id;
21 |
22 | @Column(name = "comment", columnDefinition="TEXT")
23 | @Type(type = "text")
24 | @JsonView(Views.Public.class)
25 | private String comment;
26 |
27 | @Column(name = "created_at")
28 | @Temporal(TemporalType.TIMESTAMP)
29 | @JsonView(Views.Public.class)
30 | private Date created_at;
31 |
32 | @Column(name = "updated_at")
33 | @Temporal(TemporalType.TIMESTAMP)
34 | @JsonView(Views.Public.class)
35 | private Date updated_at;
36 |
37 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
38 | @JoinColumn(name = "user_id", nullable = false)
39 | @JsonView(Views.Public.class)
40 | private User user;
41 |
42 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
43 | @JoinColumn(name = "question_id", nullable = false)
44 | private Question question;
45 |
46 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "answer")
47 | @JsonView(Views.Public.class)
48 | private Set votes;
49 |
50 | @PrePersist
51 | protected void onCreate() {
52 | created_at = new Date();
53 | updated_at = new Date();
54 | }
55 |
56 | @PreUpdate
57 | protected void onUpdate() {
58 | updated_at = new Date();
59 | }
60 |
61 | public Answer() {
62 | }
63 |
64 | public Answer(String comment, User user, Question question) {
65 | this.comment = comment;
66 | this.user = user;
67 | this.question = question;
68 | }
69 |
70 | public Answer(String comment, Date created_at, Date updated_at, User user, Question question) {
71 | this.comment = comment;
72 | this.created_at = created_at;
73 | this.updated_at = updated_at;
74 | this.user = user;
75 | this.question = question;
76 | }
77 |
78 |
79 |
80 |
81 | public long getId() {
82 | return id;
83 | }
84 |
85 | public void setId(long id) {
86 | this.id = id;
87 | }
88 |
89 | public String getComment() {
90 | return comment;
91 | }
92 |
93 | public void setComment(String comment) {
94 | this.comment = comment;
95 | }
96 |
97 | public Date getCreated_at() {
98 | return created_at;
99 | }
100 |
101 | public void setCreated_at(Date created_at) {
102 | this.created_at = created_at;
103 | }
104 |
105 | public Date getUpdated_at() {
106 | return updated_at;
107 | }
108 |
109 | public void setUpdated_at(Date updated_at) {
110 | this.updated_at = updated_at;
111 | }
112 |
113 | public User getUser() {
114 | return user;
115 | }
116 |
117 | public void setUser(User user) {
118 | this.user = user;
119 | }
120 |
121 | public Question getQuestion() {
122 | return question;
123 | }
124 |
125 | public void setQuestion(Question question) {
126 | this.question = question;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/AjaxController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import javax.annotation.PostConstruct;
7 |
8 | import org.springframework.util.StringUtils;
9 | import org.springframework.web.bind.annotation.RequestBody;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RestController;
12 |
13 | import com.fasterxml.jackson.annotation.JsonView;
14 | import com.mkyong.web.jsonview.Views;
15 | import com.mkyong.web.model.AjaxResponseBody;
16 | import com.mkyong.web.model.SearchCriteria;
17 | import com.mkyong.web.model.User;
18 |
19 | @RestController
20 | public class AjaxController {
21 |
22 | List users;
23 |
24 | // @ResponseBody, not necessary, since class is annotated with @RestController
25 | // @RequestBody - Convert the json data into object (SearchCriteria) mapped by field name.
26 | // @JsonView(Views.Public.class) - Optional, limited the json data display to client.
27 | @JsonView(Views.Public.class)
28 | @RequestMapping(value = "/search/api/getSearchResult")
29 | public AjaxResponseBody getSearchResultViaAjax(@RequestBody SearchCriteria search) {
30 |
31 | AjaxResponseBody result = new AjaxResponseBody();
32 |
33 | if (isValidSearchCriteria(search)) {
34 | List users = findByUserNameOrEmail(search.getUsername(), search.getEmail());
35 |
36 | if (users.size() > 0) {
37 | result.setCode("200");
38 | result.setMsg("");
39 | result.setResult(users);
40 | } else {
41 | result.setCode("204");
42 | result.setMsg("No user!");
43 | }
44 |
45 | } else {
46 | result.setCode("400");
47 | result.setMsg("Search criteria is empty!");
48 | }
49 |
50 | //AjaxResponseBody will be converted into json format and send back to client.
51 | return result;
52 |
53 | }
54 |
55 | private boolean isValidSearchCriteria(SearchCriteria search) {
56 |
57 | boolean valid = true;
58 |
59 | if (search == null) {
60 | valid = false;
61 | }
62 |
63 | if ((StringUtils.isEmpty(search.getUsername())) && (StringUtils.isEmpty(search.getEmail()))) {
64 | valid = false;
65 | }
66 |
67 | return valid;
68 | }
69 |
70 | // Init some users for testing
71 | @PostConstruct
72 | private void iniDataForTesting() {
73 | users = new ArrayList();
74 |
75 | User user1 = new User("mkyong", "pass123", "mkyong@yahoo.com", "012-1234567", "address 123");
76 | User user2 = new User("yflow", "pass456", "yflow@yahoo.com", "016-7654321", "address 456");
77 | User user3 = new User("laplap", "pass789", "mkyong@yahoo.com", "012-111111", "address 789");
78 | users.add(user1);
79 | users.add(user2);
80 | users.add(user3);
81 |
82 | }
83 |
84 | // Simulate the search function
85 | private List findByUserNameOrEmail(String username, String email) {
86 |
87 | List result = new ArrayList();
88 |
89 | for (User user : users) {
90 |
91 | if ((!StringUtils.isEmpty(username)) && (!StringUtils.isEmpty(email))) {
92 |
93 | if (username.equals(user.getUsername()) && email.equals(user.getEmail())) {
94 | result.add(user);
95 | continue;
96 | } else {
97 | continue;
98 | }
99 |
100 | }
101 | if (!StringUtils.isEmpty(username)) {
102 | if (username.equals(user.getUsername())) {
103 | result.add(user);
104 | continue;
105 | }
106 | }
107 |
108 | if (!StringUtils.isEmpty(email)) {
109 | if (email.equals(user.getEmail())) {
110 | result.add(user);
111 | continue;
112 | }
113 | }
114 |
115 | }
116 |
117 | return result;
118 |
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/webapp/app/auth.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | function pretendRegisterRequest(username, password, cb) {
4 | console.log(`pretendRegisterRequest: ${username}:${password}`);
5 | $.ajax({
6 | type: 'POST',
7 | url: `${window.config.basename}/api/register`,
8 | contentType: 'application/json',
9 | data: JSON.stringify({ username, password }),
10 | success: data => {
11 | console.log(data);
12 | if (data.token) {
13 | cb({
14 | authenticated: true,
15 | token: data.token,
16 | })
17 | } else {
18 | cb({ authenticated: false, message: data.message })
19 | }
20 | },
21 | error: (xhr, status, err) => {
22 | console.error(status, err.toString());
23 | cb({ authenticated: false })
24 | }
25 | });
26 | }
27 |
28 | function pretendRequest(username, password, cb) {
29 | console.log(`pretendREquest: ${username}:${password}`);
30 | $.ajax({
31 | type: 'POST',
32 | url: `${window.config.basename}/api/login`,
33 | // dataType: 'json',
34 | contentType: 'application/json',
35 | data: JSON.stringify({ username, password }),
36 | success: data => {
37 | console.log(data);
38 | if (data.token) {
39 | cb({
40 | authenticated: true,
41 | token: data.token,
42 | })
43 | } else {
44 | cb({ authenticated: false, message: data.message })
45 | }
46 | },
47 | error: (xhr, status, err) => {
48 | console.error(status, err.toString());
49 | cb({ authenticated: false })
50 | }
51 | });
52 |
53 |
54 | // setTimeout(() => {
55 | // if (username === 'joe' && pass === 'pass1') {
56 | // cb({
57 | // authenticated: true,
58 | // token: Math.random().toString(36).substring(7)
59 | // })
60 | // } else {
61 | // cb({ authenticated: false })
62 | // }
63 | // }, 0)
64 | }
65 |
66 | export default {
67 | login(username, pass, cb) {
68 | cb = arguments[arguments.length - 1]
69 | if (localStorage.token) {
70 | if (cb) cb(true)
71 | this.onChange(true)
72 | return
73 | }
74 |
75 | if (!username || !pass) {
76 | if (cb) cb(false)
77 | this.onChange(false)
78 | return
79 | }
80 |
81 | pretendRequest(username, pass, (res) => {
82 | if (res.authenticated) {
83 | localStorage.token = res.token
84 | localStorage.name = username;
85 | if (cb) cb(true, res.message)
86 | this.onChange(true)
87 | } else {
88 | if (cb) cb(false, res.message)
89 | this.onChange(false)
90 | }
91 | })
92 | },
93 |
94 | register(username, pass, cb) {
95 | cb = arguments[arguments.length - 1]
96 |
97 | if (!username || !pass) {
98 | if (cb) cb(false, `Введите логин и пароль`)
99 | this.onChange(false)
100 | return
101 | }
102 |
103 | pretendRegisterRequest(username, pass, (res) => {
104 | if (res.authenticated) {
105 | localStorage.token = res.token
106 | localStorage.name = username;
107 | if (cb) cb(true, res.message)
108 | this.onChange(true)
109 | } else {
110 | if (cb) cb(false, res.message)
111 | this.onChange(false)
112 | }
113 | })
114 | },
115 |
116 | getToken() {
117 | return localStorage.token
118 | },
119 |
120 | getName() {
121 | return localStorage.name
122 | },
123 |
124 | logout(cb) {
125 | delete localStorage.token
126 | delete localStorage.name
127 | if (cb) cb()
128 | this.onChange(false)
129 | },
130 |
131 | loggedIn() {
132 | return !!localStorage.token
133 | },
134 |
135 | onChange() {}
136 | };
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/User.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 | import org.hibernate.annotations.Fetch;
6 | import org.hibernate.annotations.FetchMode;
7 | import org.hibernate.annotations.GenericGenerator;
8 | import javax.persistence.*;
9 | import java.util.Date;
10 | import java.util.Set;
11 |
12 | @Entity
13 | @Table(name = "user")
14 | public class User {
15 | @Id
16 | @GeneratedValue(generator = "increment")
17 | @GenericGenerator(name= "increment", strategy= "increment")
18 | @Column(name = "id", length = 6, nullable = false)
19 | @JsonView(Views.Public.class)
20 | private long id;
21 |
22 | @Column(name = "username", length = 64, unique=true)
23 | @JsonView(Views.Public.class)
24 | private String username;
25 |
26 | @Column(name = "password")
27 | private String password;
28 |
29 | @Column(name = "created_at")
30 | @Temporal(TemporalType.TIMESTAMP)
31 | @JsonView(Views.Public.class)
32 | private Date created_at;
33 |
34 | @Column(name = "status")
35 | @JsonView(Views.Public.class)
36 | private String status;
37 |
38 | @Column(name = "popular")
39 | @JsonView(Views.Public.class)
40 | private Integer popular;
41 |
42 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
43 | private Set questions;
44 |
45 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "user")
46 | private Set answers;
47 |
48 | @PrePersist
49 | protected void onCreate() {
50 | created_at = new Date();
51 | }
52 |
53 | public User() {
54 | }
55 |
56 | public User(String username, String password, Date created_at, String status, Integer popular) {
57 | this.username = username;
58 | this.password = password;
59 | this.created_at = created_at;
60 | this.status = status;
61 | this.popular = popular;
62 | }
63 |
64 | public User(String username, String password, Date created_at, String status, Integer popular, Set questions, Set answers) {
65 | this.username = username;
66 | this.password = password;
67 | this.created_at = created_at;
68 | this.status = status;
69 | this.popular = popular;
70 | this.questions = questions;
71 | this.answers = answers;
72 | }
73 |
74 | public Set getAnswers() {
75 | return answers;
76 | }
77 |
78 | public void setAnswers(Set answers) {
79 | this.answers = answers;
80 | }
81 |
82 | public long getId() {
83 | return id;
84 | }
85 |
86 | public void setId(long id) {
87 | this.id = id;
88 | }
89 |
90 | public String getUsername() {
91 | return username;
92 | }
93 |
94 | public void setUsername(String username) {
95 | this.username = username;
96 | }
97 |
98 | public String getPassword() {
99 | return password;
100 | }
101 |
102 | public void setPassword(String password) {
103 | this.password = password;
104 | }
105 |
106 | public Date getCreated_at() {
107 | return created_at;
108 | }
109 |
110 | public void setCreated_at(Date created_at) {
111 | this.created_at = created_at;
112 | }
113 |
114 | public String getStatus() {
115 | return status;
116 | }
117 |
118 | public void setStatus(String status) {
119 | this.status = status;
120 | }
121 |
122 | public Integer getPopular() {
123 | return popular;
124 | }
125 |
126 | public void setPopular(Integer popular) {
127 | this.popular = popular;
128 | }
129 |
130 | public Set getQuestions() {
131 | return questions;
132 | }
133 |
134 | public void setQuestions(Set questions) {
135 | this.questions = questions;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/AuthorizationController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.entity.User;
5 | import com.mkyong.web.jsonview.Views;
6 | import com.mkyong.web.model.LoginModel;
7 | import com.mkyong.web.model.LoginResponseBody;
8 | import com.mkyong.web.model.SearchCriteria;
9 | import com.mkyong.web.service.UserService;
10 | import com.mkyong.web.util.CustomErrorType;
11 | import com.mkyong.web.util.MD5;
12 | import io.jsonwebtoken.Jwts;
13 | import io.jsonwebtoken.SignatureAlgorithm;
14 | import io.jsonwebtoken.impl.crypto.MacProvider;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 | import org.springframework.beans.factory.annotation.Autowired;
18 | import org.springframework.beans.factory.annotation.Value;
19 | import org.springframework.http.HttpStatus;
20 | import org.springframework.http.ResponseEntity;
21 | import org.springframework.web.bind.annotation.*;
22 |
23 | import java.security.Key;
24 | import java.util.Date;
25 | import java.util.List;
26 | import java.util.Objects;
27 |
28 | @RestController
29 | @RequestMapping("/api")
30 | public class AuthorizationController {
31 | public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);
32 |
33 | @Value("${jwt.secret}")
34 | private String key;
35 |
36 | @Autowired
37 | UserService userService; //Service which will do all data retrieval/manipulation work
38 |
39 | @JsonView(Views.Public.class)
40 | @RequestMapping(value = "/login", method = RequestMethod.POST)
41 | public ResponseEntity> login(@RequestBody LoginModel data) {
42 | User user = userService.getByUsername(data.getUsername());
43 |
44 | if (user == null) {
45 | return new ResponseEntity(new LoginResponseBody(false, null, "User with that name isn't exist"),
46 | HttpStatus.OK);
47 | }
48 |
49 | if (!Objects.equals(user.getPassword(), MD5.getHash(data.getPassword()))) {
50 | return new ResponseEntity(new LoginResponseBody(false, null, "wrong_password"),
51 | HttpStatus.OK);
52 | }
53 |
54 | String token = Jwts.builder()
55 | .setSubject(data.getUsername())
56 | .signWith(SignatureAlgorithm.HS512, key)
57 | .compact();
58 |
59 | return new ResponseEntity(new LoginResponseBody(true, token), HttpStatus.OK);
60 | }
61 |
62 | @JsonView(Views.Public.class)
63 | @RequestMapping(value = "/register", method = RequestMethod.POST)
64 | public ResponseEntity> register(@RequestBody LoginModel data) {
65 |
66 | User user = userService.getByUsername(data.getUsername());
67 |
68 | if (user != null) {
69 | return new ResponseEntity(new LoginResponseBody(false, null, "User with that name has already existed"),
70 | HttpStatus.OK);
71 | }
72 |
73 | User newUser = new User(data.getUsername(), MD5.getHash(data.getPassword()), new Date(), "active", 0);
74 | userService.addUser(newUser);
75 |
76 | String token = Jwts.builder()
77 | .setSubject(newUser.getUsername())
78 | .signWith(SignatureAlgorithm.HS512, key)
79 | .compact();
80 |
81 | return new ResponseEntity(new LoginResponseBody(true, token), HttpStatus.OK);
82 | }
83 |
84 | /* @JsonView(Views.Public.class)
85 | @RequestMapping(value = "/question/{id}", method = RequestMethod.GET)
86 | public ResponseEntity> getQuestion(@PathVariable("id") long id) {
87 | logger.info("Fetching Question with id {}", id);
88 | Question question = questionService.getById(id);
89 | if (question == null) {
90 | logger.error("Question with id {} not found.", id);
91 | return new ResponseEntity(new CustomErrorType("Question with id " + id
92 | + " not found"), HttpStatus.NOT_FOUND);
93 | }
94 | return new ResponseEntity(question, HttpStatus.OK);
95 | }*/
96 | }
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/util/TimeAgo.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.util;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.Date;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import java.util.concurrent.TimeUnit;
8 |
9 | public class TimeAgo {
10 |
11 | public static final List times = Arrays.asList(
12 | TimeUnit.DAYS.toMillis(365),
13 | TimeUnit.DAYS.toMillis(30),
14 | TimeUnit.DAYS.toMillis(1),
15 | TimeUnit.HOURS.toMillis(1),
16 | TimeUnit.MINUTES.toMillis(1),
17 | TimeUnit.SECONDS.toMillis(1)
18 | );
19 |
20 | public static final List timesString = Arrays.asList("год","месяц","день","час","минута","секунда");
21 |
22 | public static String get(String date1) {
23 |
24 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
25 |
26 | Date d1 = new Date();
27 | Date d2 = new Date();
28 |
29 | try {
30 | d2 = format.parse(date1);
31 | } catch(Exception e){}
32 |
33 | long mseconds = (d1.getTime()-d2.getTime());
34 | return toDuration(mseconds);
35 | }
36 |
37 | public static String toDuration(long duration) {
38 |
39 | StringBuffer res = new StringBuffer();
40 | for(int i=0;i< TimeAgo.times.size(); i++) {
41 | Long current = TimeAgo.times.get(i);
42 | long temp = duration/current;
43 | if(temp>0) {
44 | res.append(temp).append(" ").append(decline(temp, TimeAgo.timesString.get(i)) ).append(temp > 1000000 ? "s" : "").append(" назад");
45 | break;
46 | }
47 | }
48 | if("".equals(res.toString()))
49 | return "только что";
50 | else
51 | return res.toString();
52 | }
53 |
54 | public static String decline(long numLong, String nominative) {
55 |
56 | // String nominative = null;
57 | String singular = null;
58 | String plural = null;
59 |
60 | //"год","месяц","день","час","минита","секунда"
61 |
62 | switch (nominative) {
63 | case "год":
64 | singular = "года";
65 | plural = "лет";
66 | break;
67 | case "месяц":
68 | singular = "месяца";
69 | plural = "месяцев";
70 | break;
71 | case "день":
72 | singular = "дня";
73 | plural = "дней";
74 | break;
75 | case "час":
76 | singular = "часа";
77 | plural = "часов";
78 | break;
79 | case "минута":
80 | singular = "минуты";
81 | plural = "минут";
82 | break;
83 | case "секунда":
84 | singular = "секунды";
85 | plural = "секунд";
86 | break;
87 | }
88 |
89 | int num = (int)numLong;
90 | if (num > 10 && ((num % 100) / 10) == 1) {
91 | return plural;
92 | }
93 |
94 | switch (num % 10) {
95 | case 1:
96 | return nominative;
97 | case 2:
98 | case 3:
99 | case 4:
100 | return singular;
101 | default: // case 0, 5-9
102 | return plural;
103 | }
104 | }
105 |
106 |
107 |
108 |
109 | /*public static void main(String args[]) {
110 | System.out.println(toDuration(123));
111 | System.out.println(toDuration(1230));
112 | System.out.println(toDuration(12300));
113 | System.out.println(toDuration(123000));
114 | System.out.println(toDuration(1230000));
115 | System.out.println(toDuration(12300000));
116 | System.out.println(toDuration(123000000));
117 | System.out.println(toDuration(1230000000));
118 | System.out.println(toDuration(12300000000L));
119 | System.out.println(toDuration(123000000000L));
120 | }*/
121 | }
122 |
123 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/VoteController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.entity.*;
5 | import com.mkyong.web.entity.enums.VoteMark;
6 | import com.mkyong.web.jsonview.Views;
7 | import com.mkyong.web.model.AjaxResponseBody;
8 | import com.mkyong.web.model.AnswerModel;
9 | import com.mkyong.web.model.QuestionModel;
10 | import com.mkyong.web.model.VoteModel;
11 | import com.mkyong.web.service.*;
12 | import com.mkyong.web.service.UserService;
13 | import com.mkyong.web.service.impl.VoteServiceImpl;
14 | import com.mkyong.web.util.*;
15 | import io.jsonwebtoken.Jwts;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 | import org.springframework.beans.factory.annotation.Autowired;
19 | import org.springframework.beans.factory.annotation.Value;
20 | import org.springframework.http.HttpStatus;
21 | import org.springframework.http.ResponseEntity;
22 | import org.springframework.web.bind.annotation.*;
23 | import org.springframework.web.util.UriComponentsBuilder;
24 |
25 | import java.util.HashSet;
26 | import java.util.List;
27 | import java.util.Objects;
28 | import java.util.Set;
29 |
30 | @RestController
31 | @RequestMapping("/api")
32 | public class VoteController {
33 | public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);
34 |
35 | @Value("${jwt.secret}")
36 | private String key;
37 |
38 | @Autowired
39 | QuestionService questionService;
40 |
41 | @Autowired
42 | AnswerService answerService;
43 |
44 | @Autowired
45 | UserService userService;
46 |
47 | @Autowired
48 | VoteService voteService;
49 |
50 | @JsonView(Views.Public.class)
51 | @RequestMapping(value = "/vote", method = RequestMethod.POST)
52 | public AjaxResponseBody createQuestion(@RequestBody VoteModel data, UriComponentsBuilder ucBuilder) {
53 | logger.info("Creating Vote : {}", data);
54 | AjaxResponseBody result = new AjaxResponseBody();
55 |
56 | AuthService authService = new AuthService(data.getToken(), key);
57 |
58 | if (authService.getUserName() == null) {
59 | result.setCode("404");
60 | result.setMsg(authService.getMessage());
61 | return result;
62 | }
63 |
64 | //OK, we can trust this JWT
65 | String userName = authService.getUserName();
66 |
67 | User user = userService.getByUsername(userName);
68 | Question question = questionService.getById(data.getQuestion_id());
69 | Answer answer = answerService.getById(data.getAnswer_id());
70 |
71 | VoteMark mark = VoteMark.DOWN;
72 | if (Objects.equals(data.getMark(), "UP")) {
73 | mark = VoteMark.UP;
74 | }
75 |
76 | User author = user;
77 |
78 | if (question != null) {
79 | if (Objects.equals(user.getUsername(), question.getUser().getUsername())) {
80 | user = question.getUser();
81 | }
82 |
83 | author = question.getUser();
84 | if (mark == VoteMark.DOWN) {
85 | author.setPopular(author.getPopular() - 2);
86 | } else {
87 | author.setPopular(author.getPopular() + 5);
88 | }
89 |
90 | } else if (answer != null) {
91 | if (Objects.equals(user.getUsername(), answer.getQuestion().getUser().getUsername())) {
92 | user = answer.getQuestion().getUser();
93 | }
94 |
95 | author = answer.getUser();
96 | if (mark == VoteMark.DOWN) {
97 | author.setPopular(author.getPopular() - 2);
98 | } else {
99 | author.setPopular(author.getPopular() + 10);
100 | }
101 | }
102 |
103 | if (author != user) {
104 | userService.editUser(author);
105 | }
106 |
107 | Vote vote = new Vote(null, question, answer, user, mark);
108 | vote = voteService.addVote(vote);
109 |
110 | result.setCode("201");
111 | result.setMsg(Long.toString(vote.getId()));
112 |
113 | return result;
114 | }
115 |
116 |
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/my.css:
--------------------------------------------------------------------------------
1 | /* Rosario. I've added and changed few things. Feel free to change it as you like */
2 |
3 | /* Font size of 2.0 is too big, I'll reduce it
4 | body {font-size: 1.9rem; }*/
5 |
6 | /* Adding some text shadow*/
7 | .site-head { padding: 40px 0; text-shadow: 2px 2px #234;}
8 | .site-head a {color:#eee;}
9 |
10 | /* The font is too large in the post view. Revert to small size*/
11 | .post-header .blog-title {font-size:inherit;}
12 |
13 | /* Reduce font size and add line-height*/
14 | pre{
15 | font-size: 0.75em;
16 | line-height: 1.4em;
17 | }
18 |
19 | nav {
20 | font-size: 13px;
21 | text-align: center;
22 | padding: 0.5em;
23 | }
24 | /*nav a {
25 | font-family: 'Open Sans', sans-serif;
26 | display: inline-block;
27 | margin: 0.5em 1em;
28 | background: #f2f2f2;
29 | color: #57A3E8;
30 | text-decoration: none;
31 | border-radius: 6px;
32 | padding: 0 1em;
33 | }*/
34 |
35 | nav a:hover {
36 | background: #57A3E8;
37 | color: #f2f2f2;
38 | }
39 |
40 | /* Author styling */
41 | .author {margin-bottom:1em;}
42 | .author header{
43 | float:left;
44 | width: 80px;
45 | }
46 | .author article{
47 | margin-left:80px;
48 | min-height: 80px;
49 | padding-left:20px;
50 | }
51 | img.profile {
52 | width:80px;
53 | height:auto;
54 | border-radius: 50px;
55 | }
56 |
57 | .PageNavigation {
58 | font-size: 14px;
59 | display: block;
60 | width: auto;
61 | overflow: hidden;
62 | }
63 |
64 | .PageNavigation a {
65 | display: block;
66 | width: 50%;
67 | float: left;
68 | margin: 1em 0;
69 | }
70 |
71 | .PageNavigation .next {
72 | text-align: right;
73 | }
74 |
75 |
76 |
77 | @media only screen and (max-width: 900px) {
78 |
79 | blockquote {
80 | margin-left: 0;
81 | }
82 |
83 | .NewsletterSignup {
84 | position: relative !important;
85 | }
86 |
87 | .site-head {
88 | -webkit-box-sizing: border-box;
89 | -moz-box-sizing: border-box;
90 | box-sizing: border-box;
91 | height: auto;
92 | min-height: 240px;
93 | padding: 15% 0;
94 | }
95 |
96 | .blog-title {
97 | font-size: 4rem;
98 | letter-spacing: -1px;
99 | }
100 |
101 | .blog-description {
102 | font-size: 1.7rem;
103 | line-height: 1.5em;
104 | }
105 |
106 | .post {
107 |
108 | }
109 |
110 | .post-template .post {
111 | padding-bottom: 1rem;
112 | }
113 |
114 | .post-template .post-header {
115 | padding: 40px 0;
116 | }
117 |
118 |
119 | }
120 |
121 | @media only screen and (max-width: 500px) {
122 |
123 | .blog-logo img {
124 | max-height: 80px;
125 | }
126 |
127 | .NewsletterSignup {
128 | position: relative !important;
129 | }
130 |
131 | .inner,
132 | .pagination {
133 | width: auto;
134 | margin-left: 16px;
135 | margin-right: 16px;
136 | }
137 |
138 | .post {
139 |
140 | }
141 |
142 | .site-head {
143 | padding: 10% 0;
144 | }
145 |
146 | .blog-title {
147 | font-size: 3rem;
148 | }
149 |
150 | .blog-description {
151 | font-size: 1.5rem;
152 | }
153 |
154 |
155 |
156 |
157 |
158 | .post-template .post {
159 | padding-bottom: 0;
160 | }
161 |
162 | .post-template .post-header {
163 | padding: 30px 0;
164 | }
165 |
166 | .post-meta {
167 | font-size: 1.3rem;
168 | }
169 |
170 | .post-footer {
171 | padding: 4rem 0;
172 | text-align: center;
173 | }
174 |
175 | .post-footer .author {
176 | margin: 0 0 2rem 0;
177 | padding: 0 0 1.6rem 0;
178 | border-bottom: #EBF2F6 1px dashed;
179 | }
180 |
181 | .post-footer .share {
182 | position: static;
183 | width: auto;
184 | }
185 |
186 | .post-footer .share a {
187 | margin: 1.4rem 0.8rem 0 0.8rem;
188 | }
189 |
190 | .older-posts,
191 | .newer-posts {
192 | position: static;
193 | margin: 10px 0;
194 | }
195 |
196 | .page-number {
197 | display: block;
198 | }
199 |
200 | .site-footer {
201 | margin-top: 6rem;
202 | font-size: 1.1rem;
203 | }
204 |
205 | }
--------------------------------------------------------------------------------
/src/main/webapp/resources/fonts/notoserif.css:
--------------------------------------------------------------------------------
1 | /* cyrillic-ext */
2 | @font-face {
3 | font-family: 'Noto Serif';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: local('Noto Serif'), local('NotoSerif'), url(./Q47Ro23nlKqZrOLipd3-SyEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
8 | }
9 | /* cyrillic */
10 | @font-face {
11 | font-family: 'Noto Serif';
12 | font-style: normal;
13 | font-weight: 400;
14 | src: local('Noto Serif'), local('NotoSerif'), url(./qkE6YsKPRiYUugBb1_QwHCEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
16 | }
17 | /* greek-ext */
18 | @font-face {
19 | font-family: 'Noto Serif';
20 | font-style: normal;
21 | font-weight: 400;
22 | src: local('Noto Serif'), local('NotoSerif'), url(./N2U74xxQEyaTBF6QLZRr1CEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
23 | unicode-range: U+1F00-1FFF;
24 | }
25 | /* greek */
26 | @font-face {
27 | font-family: 'Noto Serif';
28 | font-style: normal;
29 | font-weight: 400;
30 | src: local('Noto Serif'), local('NotoSerif'), url(./1_daFS3X6gkNOcmGmHl7UiEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
31 | unicode-range: U+0370-03FF;
32 | }
33 | /* vietnamese */
34 | @font-face {
35 | font-family: 'Noto Serif';
36 | font-style: normal;
37 | font-weight: 400;
38 | src: local('Noto Serif'), local('NotoSerif'), url(./G-mm5mDezDSs-RvEL7XAECEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
39 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
40 | }
41 | /* latin-ext */
42 | @font-face {
43 | font-family: 'Noto Serif';
44 | font-style: normal;
45 | font-weight: 400;
46 | src: local('Noto Serif'), local('NotoSerif'), url(./fVu1p3782bqS2z-CaJvp9iEAvth_LlrfE80CYdSH47w.woff2) format('woff2');
47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Noto Serif';
52 | font-style: normal;
53 | font-weight: 400;
54 | src: local('Noto Serif'), local('NotoSerif'), url(./eCpfeMZI7q4jLksXVRWPQ_k_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
56 | }
57 | /* cyrillic-ext */
58 | @font-face {
59 | font-family: 'Noto Serif';
60 | font-style: normal;
61 | font-weight: 700;
62 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPede9INZm0R8ZMJUtfOsxrw.woff2) format('woff2');
63 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
64 | }
65 | /* cyrillic */
66 | @font-face {
67 | font-family: 'Noto Serif';
68 | font-style: normal;
69 | font-weight: 700;
70 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPbpHcMS0zZe4mIYvDKG2oeM.woff2) format('woff2');
71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
72 | }
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Noto Serif';
76 | font-style: normal;
77 | font-weight: 700;
78 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPRquHyap-BLkxbFhcQRhghg.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 | /* greek */
82 | @font-face {
83 | font-family: 'Noto Serif';
84 | font-style: normal;
85 | font-weight: 700;
86 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPTyJJ3dJfU6-XWVNf-DPRbs.woff2) format('woff2');
87 | unicode-range: U+0370-03FF;
88 | }
89 | /* vietnamese */
90 | @font-face {
91 | font-family: 'Noto Serif';
92 | font-style: normal;
93 | font-weight: 700;
94 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPTh33M2A-6X0bdu871ruAGs.woff2) format('woff2');
95 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
96 | }
97 | /* latin-ext */
98 | @font-face {
99 | font-family: 'Noto Serif';
100 | font-style: normal;
101 | font-weight: 700;
102 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPRHJTnCUrjaAm2S9z52xC3Y.woff2) format('woff2');
103 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
104 | }
105 | /* latin */
106 | @font-face {
107 | font-family: 'Noto Serif';
108 | font-style: normal;
109 | font-weight: 700;
110 | src: local('Noto Serif Bold'), local('NotoSerif-Bold'), url(./lJAvZoKA5NttpPc9yc6lPYWiMMZ7xLd792ULpGE4W_Y.woff2) format('woff2');
111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
112 | }
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/AnswerController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.entity.Answer;
5 | import com.mkyong.web.entity.Question;
6 | import com.mkyong.web.entity.User;
7 | import com.mkyong.web.jsonview.Views;
8 | import com.mkyong.web.model.AjaxResponseBody;
9 | import com.mkyong.web.model.AnswerModel;
10 | import com.mkyong.web.service.AnswerService;
11 | import com.mkyong.web.service.QuestionService;
12 | import com.mkyong.web.service.UserService;
13 | import com.mkyong.web.util.AuthService;
14 | import com.mkyong.web.util.CustomErrorType;
15 | import org.slf4j.Logger;
16 | import org.slf4j.LoggerFactory;
17 | import org.springframework.beans.factory.annotation.Autowired;
18 | import org.springframework.beans.factory.annotation.Value;
19 | import org.springframework.http.HttpStatus;
20 | import org.springframework.http.ResponseEntity;
21 | import org.springframework.web.bind.annotation.*;
22 | import org.springframework.web.util.UriComponentsBuilder;
23 |
24 | import java.util.List;
25 | import java.util.Objects;
26 |
27 | @RestController
28 | @RequestMapping("/api")
29 | public class AnswerController {
30 | public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);
31 |
32 | @Value("${jwt.secret}")
33 | private String key;
34 |
35 | @Autowired
36 | QuestionService questionService;
37 |
38 | @Autowired
39 | AnswerService answerService;
40 |
41 | @Autowired
42 | UserService userService;
43 |
44 | @JsonView(Views.Public.class)
45 | @RequestMapping(value = "/answers", method = RequestMethod.GET)
46 | public ResponseEntity> listAllAnswers() {
47 | List answers = answerService.getAll();
48 | if (answers.isEmpty()) {
49 | return new ResponseEntity(HttpStatus.NO_CONTENT);
50 | // You many decide to return HttpStatus.NOT_FOUND
51 | }
52 | return new ResponseEntity>(answers, HttpStatus.OK);
53 | }
54 |
55 | @JsonView(Views.Public.class)
56 | @RequestMapping(value = "/answer/{id}", method = RequestMethod.GET)
57 | public ResponseEntity> getAnswer(@PathVariable("id") long id) {
58 | logger.info("Fetching Answer with id {}", id);
59 | Answer answer = answerService.getById(id);
60 | if (answer == null) {
61 | logger.error("Answer with id {} not found.", id);
62 | return new ResponseEntity(new CustomErrorType("Answer with id " + id
63 | + " not found"), HttpStatus.NOT_FOUND);
64 | }
65 | return new ResponseEntity(answer, HttpStatus.OK);
66 | }
67 |
68 | @JsonView(Views.Public.class)
69 | @RequestMapping(value = "/answer/user/{name}", method = RequestMethod.GET)
70 | public ResponseEntity> getAnswersByUser(@PathVariable("name") String name) {
71 |
72 | User user = userService.getByUsername(name);
73 |
74 | if (user == null) {
75 | return new ResponseEntity(HttpStatus.NO_CONTENT);
76 | }
77 |
78 | List answers = answerService.getByUser(user);
79 | if (answers.isEmpty()) {
80 | return new ResponseEntity(HttpStatus.NO_CONTENT);
81 | }
82 | return new ResponseEntity>(answers, HttpStatus.OK);
83 | }
84 |
85 | @JsonView(Views.Public.class)
86 | @RequestMapping(value = "/answer", method = RequestMethod.POST)
87 | public AjaxResponseBody createQuestion(@RequestBody AnswerModel data, UriComponentsBuilder ucBuilder) {
88 | logger.info("Creating Answer : {}", data);
89 | AjaxResponseBody result = new AjaxResponseBody();
90 |
91 | AuthService authService = new AuthService(data.getToken(), key);
92 | if (authService.getUserName() == null) {
93 | result.setCode("404");
94 | result.setMsg(authService.getMessage());
95 | return result;
96 | }
97 | //OK, we can trust this JWT
98 | String userName = authService.getUserName();
99 |
100 | User user = userService.getByUsername(userName);
101 |
102 | Question question = questionService.getById(data.getQuestion_id());
103 |
104 | // prevent error of two instance of one object
105 | if (Objects.equals(question.getUser().getUsername(), user.getUsername())) {
106 | user = question.getUser();
107 | }
108 |
109 | Answer answer = new Answer(data.getMessage(), user, question);
110 | answer = answerService.addAnswer(answer);
111 |
112 | result.setCode("201");
113 | result.setMsg(Long.toString(answer.getId()));
114 |
115 | return result;
116 | }
117 |
118 |
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/bundle.css:
--------------------------------------------------------------------------------
1 | #main{font-family:"Noto Serif", serif;color:#3a4145}html,body{margin:0;padding:0;height:100%}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}h2{font-size:16px}a.black:hover u,a.black:hover,a:hover{color:#a00}a{text-decoration:none;color:#03709a;transition:all 0.2s linear}a.black,a.black u{color:#21211e}a.black u{border-color:rgba(33,33,30,0.2)}.hidden{display:none}.cleared:after{content:"";clear:both;display:block}.wrapper{min-height:100%;position:relative}.break{padding:15px;border-bottom:1px solid #ddd}.help-block{color:#e60435;font-size:13px}body{position:relative;min-height:100%;height:100%;font-family:"Helvetica Neue", Arial, sans-serif}body::after{content:"";opacity:0.25;top:0;left:0;bottom:0;right:0;position:absolute;z-index:-1}.footer{position:absolute;right:0;bottom:0;left:0;padding:1rem;background-color:#f5f5f5;text-align:center;margin:0 auto;padding:32px 16px;font-size:88%}.footer .copyright{color:#aaa}.footer .copyright a:hover{color:#a00}.footer .social{font-size:150%;padding:0;margin:0 0 1em}.content{width:790px;height:100%;padding:30px;margin-left:auto;margin-right:auto;background:rgba(255,255,255,0.9);padding-bottom:100px}.header-wrap{position:relative;padding:0 16px;background:#f5f5f5}.header{padding:8px 0 6px;max-width:780px;margin:0 auto}.menu{text-transform:uppercase;letter-spacing:1px;font-size:11px;line-height:28px;list-style:none;margin:0 0 0 -10px;padding:0;display:inline-block;width:100%}.menu .right{float:right}.menu .li{display:inline-block;position:relative;white-space:nowrap}.menu .li a{padding:8px 10px}.menu .li a.black,.menu .li a.black u{color:#21211e;border-color:rgba(33,33,30,0.2)}.menu .li a:hover u,.menu .li a.selected u{color:#a00;border-color:#a00}.menu .li a u{text-decoration:none;border-bottom:.1em solid;color:#03709a;border-color:rgba(3,112,154,0.2)}.input{position:relative;z-index:1;display:inline-block;margin:0.5em;max-width:280px;width:calc(100% - 2em);vertical-align:top}.input__field{position:relative;display:block;float:right;padding:0.8em;width:60%;border:none;border-radius:0;background:#f0f0f0;color:#aaa;font-weight:bold;font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;-webkit-appearance:none}.input__field:focus{outline:none}.input__label{display:inline-block;float:right;padding:0 1em;width:40%;color:#6a7989;font-weight:bold;font-size:70.25%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.input__label-content{position:relative;display:block;padding:1.6em 0;width:100%}.graphic{position:absolute;top:0;left:0;fill:none}.icon{color:#ddd;font-size:150%}.input__field--isao{z-index:10;padding:0.75em 0.1em 0.25em;width:100%;background:transparent;color:#afb3b8;font-size:18px}.input__label--isao{position:relative;overflow:hidden;padding:0;width:100%;color:#dadada;text-align:left}.input__label--isao::before{content:'';position:absolute;top:0;width:100%;height:3px;background:#dadada;-webkit-transform:scale3d(1, 0.4, 1);transform:scale3d(1, 0.4, 1);-webkit-transform-origin:50% 100%;transform-origin:50% 100%;-webkit-transition:-webkit-transform 0.3s, background-color 0.3s;transition:transform 0.3s, background-color 0.3s;-webkit-transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1)}.input__label--isao::after{content:attr(data-content);position:absolute;top:0;left:0;padding:0.75em 0.15em;color:#da7071;opacity:0;-webkit-transform:translate3d(0, 50%, 0);transform:translate3d(0, 50%, 0);-webkit-transition:opacity 0.3s, -webkit-transform 0.3s;transition:opacity 0.3s, transform 0.3s;-webkit-transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);pointer-events:none}.input__field--isao:focus+.input__label--isao::before{background-color:#da7071;-webkit-transform:scale3d(1, 1, 1);transform:scale3d(1, 1, 1)}.input__field--isao:focus+.input__label--isao{pointer-events:none}.input__field--isao:focus+.input__label--isao::after{opacity:1;-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0)}.input__label-content--isao{padding:0.75em 0.15em;-webkit-transition:opacity 0.3s, -webkit-transform 0.3s;transition:opacity 0.3s, transform 0.3s;-webkit-transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1)}.input__field--isao:focus+.input__label--isao .input__label-content--isao{opacity:0;-webkit-transform:translate3d(0, -50%, 0);transform:translate3d(0, -50%, 0)}.stickers{margin-left:auto;margin-right:auto;width:600px;background-color:#f5f5f5;padding:15px;font-size:12px}.stickers:after{content:"";display:block;clear:both}.stickers__item{width:120px;height:120px;border:1px solid #222;background-color:#d5e8fb;padding:10px;margin:10px;position:relative;float:left}.stickers__item a{color:#222}.stickers__item a:hover{color:#555;text-decoration:none}.stickers__item__title{height:20px;font-size:14px;overflow:hidden}.stickers__item__close{position:absolute;right:5px;top:5px}.stickers__item__description{background-color:#fff;font-size:12px;padding:10px;margin-bottom:6px;height:60px;overflow:hidden}.stickers__item__like{float:right}.liked{font-weight:bold}.stickers__item__title.label{cursor:pointer}.editing .stickers__item{background-color:#f16b84}
2 |
3 | /*# sourceMappingURL=bundle.css.map*/
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/question.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Prism from 'prismjs';
3 | import Loader from '../utils/loader';
4 | import $ from 'jquery';
5 | import { t } from 'localizify';
6 |
7 | import { withRouter } from 'react-router';
8 | import Answers from '../items/answers';
9 | import Vote from '../items/vote';
10 |
11 | import auth from '../../auth';
12 |
13 | import UserSign from '../utils/user-sign';
14 |
15 | import formatText from '../../utils/format-str';
16 | import declOfNum from '../../utils/number-dec';
17 | import timeAgo from '../../utils/time-ago';
18 |
19 | import Tags from '../items/tags';
20 |
21 |
22 |
23 | var QuestionPage = withRouter(React.createClass({
24 | getInitialState() {
25 | return {
26 | data: {},
27 | loading: true
28 | };
29 | },
30 | componentDidMount() {
31 |
32 | if (this.state.loading) {
33 | const id = this.props.params.id;
34 | const watchedCount = localStorage.getItem(`q${id}`) || 0;
35 | localStorage.setItem(`q${id}`, +watchedCount + 1);
36 | }
37 |
38 | setTimeout(() =>
39 | $.ajax({
40 | url: `${window.config.basename}/api/question/${this.props.params.id}`,
41 | dataType: 'json',
42 | success: data => {
43 | console.log(data);
44 | this.setState({ data, loading: false });
45 | },
46 | error: (xhr, status, err) => {
47 | console.error(this.props.url, status, err.toString());
48 | }
49 | })
50 | , 0);
51 | },
52 |
53 | handleSubmit(event) {
54 | event.preventDefault();
55 |
56 | const message = this.refs.message.value.trim();
57 |
58 | $.ajax({
59 | type: 'POST',
60 | url: `${window.config.basename}/api/answer`,
61 | contentType: 'application/json',
62 | data: JSON.stringify({
63 | message,
64 | question_id: this.props.params.id,
65 | token: auth.getToken()
66 | }),
67 | success: data => {
68 | console.log(data);
69 | if (data.msg) {
70 |
71 | if (data.msg == 'Wrong token' || data.code == '404') {
72 | auth.logout();
73 | return this.props.router.replace('/login');
74 | }
75 |
76 | $(this.refs.message).val('');
77 |
78 | const { location } = this.props
79 |
80 | this.componentDidMount();
81 | // window.location.reload();
82 | // if (location.state && location.state.nextPathname) {
83 | // this.props.router.replace(location.state.nextPathname)
84 | // } else {
85 | // this.props.router.replace(`/questions/${this.props.params.id}`)
86 | // }
87 | } else {
88 | console.error(data);
89 | }
90 | },
91 | error: (xhr, status, err) => {
92 | console.error(status, err.toString());
93 | }
94 | });
95 |
96 | return false;
97 | },
98 |
99 | onChangeAnswer() {
100 | const message = this.refs.message.value.trim();
101 | // console.log((message));
102 | $('.preview').html(formatText(message));
103 | },
104 |
105 | render() {
106 | if (this.state.loading) {
107 | return ( );
108 | }
109 |
110 | const { title, ago, created_at, tags, comment, answers, user, votes, id } = this.state.data;
111 | const html = formatText(comment);
112 |
113 | const popular = votes.filter(t => t.mark === 'UP').length - votes.filter(t => t.mark === 'DOWN').length;
114 | const userChooses = null
115 |
116 | const data = { user, created_at };
117 |
118 | return (
119 |
120 |
{title}
121 |
122 |
123 |
127 |
128 |
132 |
133 |
134 |
{t('Answers')}
135 |
136 |
137 |
{t('Add answer')}:
138 |
139 |
147 |
{t('You should auth')}
148 |
149 | );
150 | }
151 | }));
152 |
153 | export default QuestionPage;
154 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/config/SpringWebConfig.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.config;
2 |
3 | import org.hibernate.ejb.HibernatePersistence;
4 | import org.springframework.boot.web.servlet.FilterRegistrationBean;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.ComponentScan;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.context.annotation.PropertySource;
9 | import org.springframework.core.env.Environment;
10 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
11 | import org.springframework.jdbc.datasource.DriverManagerDataSource;
12 | import org.springframework.orm.jpa.JpaTransactionManager;
13 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
14 | import org.springframework.transaction.annotation.EnableTransactionManagement;
15 | import org.springframework.web.filter.CharacterEncodingFilter;
16 | import org.springframework.web.servlet.config.annotation.EnableWebMvc;
17 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
18 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
19 | import org.springframework.web.servlet.view.InternalResourceViewResolver;
20 | import org.springframework.web.servlet.view.JstlView;
21 |
22 | import javax.annotation.Resource;
23 | import javax.sql.DataSource;
24 | import java.util.Properties;
25 |
26 | @EnableWebMvc
27 | @Configuration
28 | @EnableTransactionManagement
29 | @ComponentScan({ "com.mkyong.web" })
30 | @PropertySource("classpath:app.properties")
31 | @EnableJpaRepositories("com.mkyong.web.repository")
32 | //@EnableAutoConfiguration
33 | public class SpringWebConfig extends WebMvcConfigurerAdapter {
34 |
35 | private static final String PROP_DATABASE_DRIVER = "db.driver";
36 | private static final String PROP_DATABASE_PASSWORD = "db.password";
37 | private static final String PROP_DATABASE_URL = "db.url";
38 | private static final String PROP_DATABASE_USERNAME = "db.username";
39 | private static final String PROP_HIBERNATE_DIALECT = "hibernate.dialect";
40 | private static final String PROP_HIBERNATE_SHOW_SQL = "hibernate.show_sql";
41 | private static final String PROP_ENTITYMANAGER_PACKAGES_TO_SCAN = "db.entitymanager.packages.to.scan";
42 | private static final String PROP_HIBERNATE_HBM2DDL_AUTO = "hibernate.hbm2ddl.auto";
43 |
44 | @Resource
45 | private Environment env;
46 |
47 | @Bean
48 | public FilterRegistrationBean filterRegistrationBean() {
49 | CharacterEncodingFilter filter = new CharacterEncodingFilter();
50 | filter.setEncoding("UTF-8");
51 |
52 | FilterRegistrationBean registrationBean = new FilterRegistrationBean();
53 | registrationBean.setFilter(filter);
54 | registrationBean.addUrlPatterns("/*");
55 | return registrationBean;
56 | }
57 |
58 | @Bean
59 | public DataSource dataSource() {
60 | DriverManagerDataSource dataSource = new DriverManagerDataSource();
61 |
62 | dataSource.setDriverClassName(env.getRequiredProperty(PROP_DATABASE_DRIVER));
63 | dataSource.setUrl(env.getRequiredProperty(PROP_DATABASE_URL));
64 | dataSource.setUsername(env.getRequiredProperty(PROP_DATABASE_USERNAME));
65 | dataSource.setPassword(env.getRequiredProperty(PROP_DATABASE_PASSWORD));
66 |
67 | return dataSource;
68 | }
69 |
70 | @Bean
71 | public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
72 | LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
73 | entityManagerFactoryBean.setDataSource(dataSource());
74 | entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class);
75 | entityManagerFactoryBean.setPackagesToScan(env.getRequiredProperty(PROP_ENTITYMANAGER_PACKAGES_TO_SCAN));
76 |
77 | entityManagerFactoryBean.setJpaProperties(getHibernateProperties());
78 |
79 | return entityManagerFactoryBean;
80 | }
81 |
82 | @Bean
83 | public JpaTransactionManager transactionManager() {
84 | JpaTransactionManager transactionManager = new JpaTransactionManager();
85 | transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
86 |
87 | return transactionManager;
88 | }
89 |
90 | private Properties getHibernateProperties() {
91 | Properties properties = new Properties();
92 | properties.put(PROP_HIBERNATE_DIALECT, env.getRequiredProperty(PROP_HIBERNATE_DIALECT));
93 | properties.put(PROP_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROP_HIBERNATE_SHOW_SQL));
94 | properties.put(PROP_HIBERNATE_HBM2DDL_AUTO, env.getRequiredProperty(PROP_HIBERNATE_HBM2DDL_AUTO));
95 |
96 | properties.put("hibernate.connection.CharSet", "utf-8");
97 | properties.put("hibernate.connection.useUnicode", true);
98 | properties.put("hibernate.connection.characterEncoding", "utf-8");
99 |
100 | return properties;
101 | }
102 |
103 | @Override
104 | public void addResourceHandlers(ResourceHandlerRegistry registry) {
105 | registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
106 | }
107 |
108 | @Bean
109 | public InternalResourceViewResolver viewResolver() {
110 | InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
111 | viewResolver.setViewClass(JstlView.class);
112 | viewResolver.setPrefix("/WEB-INF/views/jsp/");
113 | viewResolver.setSuffix(".jsp");
114 | return viewResolver;
115 | }
116 |
117 | }
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/entity/Question.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.entity;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.jsonview.Views;
5 | import com.mkyong.web.util.TimeAgo;
6 | import org.hibernate.annotations.Fetch;
7 | import org.hibernate.annotations.FetchMode;
8 | import org.hibernate.annotations.GenericGenerator;
9 | import org.hibernate.annotations.Type;
10 |
11 | import javax.persistence.*;
12 | import java.util.Date;
13 | import java.util.Set;
14 |
15 | @Entity
16 | @Table(name = "question")
17 | public class Question {
18 | @Id
19 | @GeneratedValue(generator = "increment")
20 | @GenericGenerator(name= "increment", strategy= "increment")
21 | @Column(name = "id", length = 6, nullable = false)
22 | @JsonView(Views.Public.class)
23 | private long id;
24 |
25 | @Column(name = "title", length = 255)
26 | @JsonView(Views.Public.class)
27 | private String title;
28 |
29 | //@JsonView(Views.Public.class)
30 | private String ago;
31 |
32 | public String getAgo() {
33 | return TimeAgo.get(created_at.toString());
34 | }
35 |
36 | public void setAgo(String ago) {
37 | this.ago = ago;
38 | }
39 |
40 | public Set getVotes() {
41 | return votes;
42 | }
43 |
44 | public void setVotes(Set votes) {
45 | this.votes = votes;
46 | }
47 |
48 | @Column(name = "comment", columnDefinition="TEXT")
49 | @Type(type = "text")
50 | @JsonView(Views.Public.class)
51 | private String comment;
52 |
53 | @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
54 | @JoinColumn(name = "user_id", nullable = false)
55 | @JsonView(Views.Public.class)
56 | private User user;
57 |
58 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "question")
59 | @JsonView(Views.Public.class)
60 | @OrderBy("id DESC")
61 | private Set answers;
62 |
63 | @Column(name = "created_at")
64 | @Temporal(TemporalType.TIMESTAMP)
65 | @JsonView(Views.Public.class)
66 | private Date created_at;
67 |
68 | @Column(name = "updated_at")
69 | @Temporal(TemporalType.TIMESTAMP)
70 | @JsonView(Views.Public.class)
71 | private Date updated_at;
72 |
73 | @ManyToMany(fetch = FetchType.EAGER)
74 | @JoinTable(name="question_tag",
75 | joinColumns = @JoinColumn(name="question_id", referencedColumnName="id"),
76 | inverseJoinColumns = @JoinColumn(name="tag_id", referencedColumnName="id")
77 | )
78 | @JsonView(Views.Public.class)
79 | private Set tags;
80 |
81 |
82 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "question")
83 | @JsonView(Views.Public.class)
84 | private Set votes;
85 |
86 |
87 | @PrePersist
88 | protected void onCreate() {
89 | created_at = new Date();
90 | updated_at = new Date();
91 | }
92 |
93 | @PreUpdate
94 | protected void onUpdate() {
95 | updated_at = new Date();
96 | }
97 |
98 | public Question() {
99 | }
100 |
101 | public Question(String title, String comment, User user, Set tags) {
102 | this.title = title;
103 | this.comment = comment;
104 | this.user = user;
105 |
106 | this.tags = tags;
107 | }
108 |
109 | public Question(String title, String comment, User user, Set answers, Date created_at, Date updated_at, Set tags) {
110 | this.title = title;
111 | this.comment = comment;
112 | this.user = user;
113 | this.answers = answers;
114 | this.created_at = created_at;
115 | this.updated_at = updated_at;
116 | this.tags = tags;
117 | }
118 |
119 | public Set getAnswers() {
120 | return answers;
121 | }
122 |
123 | public void setAnswers(Set answers) {
124 | this.answers = answers;
125 | }
126 |
127 | public Set getTags() {
128 | return tags;
129 | }
130 |
131 | public void setTags(Set tags) {
132 | this.tags = tags;
133 | }
134 |
135 | public Question(String title, String comment, User user, Date created_at, Date updated_at) {
136 | this.title = title;
137 | this.comment = comment;
138 | this.user = user;
139 | this.created_at = created_at;
140 | this.updated_at = updated_at;
141 | }
142 |
143 | public Question(String title, String comment, User user) {
144 | this.title = title;
145 | this.comment = comment;
146 | this.user = user;
147 | }
148 |
149 | public long getId() {
150 | return id;
151 | }
152 |
153 | public void setId(long id) {
154 | this.id = id;
155 | }
156 |
157 | public String getTitle() {
158 | return title;
159 | }
160 |
161 | public void setTitle(String title) {
162 | this.title = title;
163 | }
164 |
165 | public User getUser() {
166 | return user;
167 | }
168 |
169 | public void setUser(User user) {
170 | this.user = user;
171 | }
172 |
173 | public String getComment() {
174 | return comment;
175 | }
176 |
177 | public void setComment(String comment) {
178 | this.comment = comment;
179 | }
180 |
181 | public Date getCreated_at() {
182 | return created_at;
183 | }
184 |
185 | public void setCreated_at(Date created_at) {
186 | this.created_at = created_at;
187 | }
188 |
189 | public Date getUpdated_at() {
190 | return updated_at;
191 | }
192 |
193 | public void setUpdated_at(Date updated_at) {
194 | this.updated_at = updated_at;
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | com.mkyong
5 | spring4-mvc-maven-ajax-example
6 | war
7 | 1.0-SNAPSHOT
8 | spring4 mvc maven ajax example
9 |
10 |
11 | 1.8
12 | 4.2.2.RELEASE
13 | 2.6.3
14 | 1.1.3
15 | 1.7.12
16 | 1.2
17 | 3.1.0
18 |
19 | 1.3.4.RELEASE
20 | 3.0.1
21 | 5.1.29
22 | 4.2.5.Final
23 |
24 |
25 |
26 |
27 |
28 | org.springframework
29 | spring-webmvc
30 | ${spring.version}
31 |
32 |
33 | commons-logging
34 | commons-logging
35 |
36 |
37 |
38 |
39 |
40 |
41 | com.fasterxml.jackson.core
42 | jackson-core
43 | ${jackson.version}
44 |
45 |
46 |
47 | com.fasterxml.jackson.core
48 | jackson-databind
49 | ${jackson.version}
50 |
51 |
52 |
53 |
54 | javax.servlet
55 | jstl
56 | ${jstl.version}
57 |
58 |
59 |
60 |
61 | org.slf4j
62 | jcl-over-slf4j
63 | ${jcl.slf4j.version}
64 |
65 |
66 |
67 | ch.qos.logback
68 | logback-classic
69 | ${logback.version}
70 |
71 |
72 |
73 |
74 |
75 | javax.servlet
76 | javax.servlet-api
77 | ${servletapi.version}
78 | provided
79 |
80 |
81 |
82 | mysql
83 | mysql-connector-java
84 | ${mysql.version}
85 |
86 |
87 |
88 | org.springframework.data
89 | spring-data-jpa
90 | ${spring.data}
91 |
92 |
93 |
94 | org.hibernate
95 | hibernate-entitymanager
96 | ${hb.manager}
97 |
98 |
99 |
100 | org.springframework
101 | spring-aop
102 | ${spring.version}
103 |
104 |
105 |
106 |
107 | org.springframework.boot
108 | spring-boot
109 | 1.4.3.RELEASE
110 |
111 |
112 |
113 | io.jsonwebtoken
114 | jjwt
115 | 0.7.0
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | org.apache.maven.plugins
126 | maven-compiler-plugin
127 | 3.3
128 |
129 | ${jdk.version}
130 | ${jdk.version}
131 |
132 |
133 |
134 |
135 | org.eclipse.jetty
136 | jetty-maven-plugin
137 | 9.2.11.v20150529
138 |
139 | 10
140 |
141 | /spring4ajax
142 |
143 |
144 |
145 | 4017
146 |
147 |
148 |
149 |
150 |
151 | org.apache.maven.plugins
152 | maven-eclipse-plugin
153 | 2.10
154 |
155 | true
156 | true
157 | 2.0
158 | spring4ajax
159 |
160 |
161 |
162 |
163 | org.apache.maven.plugins
164 | maven-war-plugin
165 | 2.6
166 |
167 | false
168 |
169 |
170 |
171 |
172 |
173 | org.wildfly.plugins
174 | wildfly-maven-plugin
175 | 1.1.0.Alpha5
176 |
177 | 127.0.0.1
178 | 9990
179 | admin
180 | admin
181 | spring4ajax.war
182 |
183 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/QuestionController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.entity.Tag;
5 | import com.mkyong.web.entity.User;
6 | import com.mkyong.web.model.AjaxResponseBody;
7 | import com.mkyong.web.model.LoginModel;
8 | import com.mkyong.web.model.LoginResponseBody;
9 | import com.mkyong.web.model.QuestionModel;
10 | import com.mkyong.web.service.TagService;
11 | import com.mkyong.web.service.UserService;
12 | import com.mkyong.web.util.AuthService;
13 | import com.mkyong.web.util.CustomErrorType;
14 | import com.mkyong.web.entity.Question;
15 | import com.mkyong.web.jsonview.Views;
16 | import com.mkyong.web.service.QuestionService;
17 | import com.mkyong.web.util.MD5;
18 | import io.jsonwebtoken.Jwts;
19 | import io.jsonwebtoken.SignatureAlgorithm;
20 | import org.slf4j.Logger;
21 | import org.slf4j.LoggerFactory;
22 | import org.springframework.beans.factory.annotation.Autowired;
23 | import org.springframework.beans.factory.annotation.Value;
24 | import org.springframework.http.HttpHeaders;
25 | import org.springframework.http.HttpStatus;
26 | import org.springframework.http.ResponseEntity;
27 | import org.springframework.web.bind.annotation.*;
28 | import org.springframework.web.util.UriComponentsBuilder;
29 |
30 | import java.util.*;
31 |
32 | @RestController
33 | @RequestMapping("/api")
34 | public class QuestionController {
35 | public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);
36 |
37 | @Value("${jwt.secret}")
38 | private String key;
39 |
40 | @Autowired
41 | QuestionService questionService; //Service which will do all data retrieval/manipulation work
42 |
43 | @Autowired
44 | UserService userService;
45 |
46 | @Autowired
47 | TagService tagService;
48 |
49 | @JsonView(Views.Public.class)
50 | @RequestMapping(value = "/questions", method = RequestMethod.GET)
51 | public ResponseEntity> listAllQuestions() {
52 | List questions = questionService.getAll();
53 | if (questions.isEmpty()) {
54 | return new ResponseEntity(HttpStatus.NO_CONTENT);
55 | // You many decide to return HttpStatus.NOT_FOUND
56 | }
57 | return new ResponseEntity>(questions, HttpStatus.OK);
58 | }
59 |
60 | @JsonView(Views.Public.class)
61 | @RequestMapping(value = "/question/{id}", method = RequestMethod.GET)
62 | public ResponseEntity> getQuestion(@PathVariable("id") long id) {
63 | logger.info("Fetching Question with id {}", id);
64 | Question question = questionService.getById(id);
65 | if (question == null) {
66 | logger.error("Question with id {} not found.", id);
67 | return new ResponseEntity(new CustomErrorType("Question with id " + id
68 | + " not found"), HttpStatus.NOT_FOUND);
69 | }
70 | return new ResponseEntity(question, HttpStatus.OK);
71 | }
72 |
73 | @JsonView(Views.Public.class)
74 | @RequestMapping(value = "/questions/user/{name}", method = RequestMethod.GET)
75 | public ResponseEntity> getQuestionsByUser(@PathVariable("name") String name) {
76 |
77 | User user = userService.getByUsername(name);
78 |
79 | if (user == null) {
80 | return new ResponseEntity(HttpStatus.NO_CONTENT);
81 | }
82 |
83 | List questions = questionService.getByUser(user);
84 | if (questions.isEmpty()) {
85 | return new ResponseEntity(HttpStatus.NO_CONTENT);
86 | }
87 | return new ResponseEntity>(questions, HttpStatus.OK);
88 | }
89 |
90 | @JsonView(Views.Public.class)
91 | @RequestMapping(value = "/questions/tag/{name}", method = RequestMethod.GET)
92 | public ResponseEntity> getQuestionsByTag(@PathVariable("name") String name) {
93 |
94 | Tag tag = tagService.getByName(name);
95 |
96 | if (tag == null) {
97 | return new ResponseEntity(HttpStatus.NO_CONTENT);
98 | }
99 |
100 | List questions = questionService.getByTag(tag);
101 | if (questions.isEmpty()) {
102 | return new ResponseEntity(HttpStatus.NO_CONTENT);
103 | }
104 | return new ResponseEntity>(questions, HttpStatus.OK);
105 | }
106 |
107 | @JsonView(Views.Public.class)
108 | @RequestMapping(value = "/question", method = RequestMethod.POST)
109 | public AjaxResponseBody createQuestion(@RequestBody QuestionModel data, UriComponentsBuilder ucBuilder) {
110 | logger.info("Creating Question : {}", data);
111 | AjaxResponseBody result = new AjaxResponseBody();
112 |
113 | AuthService authService = new AuthService(data.getToken(), key);
114 | if (authService.getUserName() == null) {
115 | result.setCode("404");
116 | result.setMsg(authService.getMessage());
117 | return result;
118 | }
119 | //OK, we can trust this JWT
120 | String userName = authService.getUserName();
121 |
122 |
123 | User user = userService.getByUsername(userName);
124 |
125 | String[] tagNames = data.getTags().split(",");
126 | Set tags = new HashSet<>();
127 |
128 | for (String name : tagNames) {
129 | Tag tag = tagService.getByName(name);
130 |
131 | if (tag != null) {
132 | tag.setPopular(tag.getPopular() + 1);
133 | tag = tagService.editTag(tag);
134 | } else {
135 | tag = new Tag(name, null, user);
136 | tag = tagService.addTag(tag);
137 | }
138 | tags.add(tag);
139 | }
140 |
141 | Question question = new Question(data.getTitle(), data.getComment(), user, tags);
142 | question = questionService.addQuestion(question);
143 |
144 | //
145 |
146 | result.setCode("201");
147 | result.setMsg(Long.toString(question.getId()));
148 |
149 | return result;
150 | // HttpHeaders headers = new HttpHeaders();
151 | // headers.setLocation(ucBuilder.path("/api/question/{id}").buildAndExpand(question.getId()).toUri());
152 | // return new ResponseEntity(headers, HttpStatus.CREATED);
153 | }
154 |
155 |
156 |
157 | }
--------------------------------------------------------------------------------
/src/main/webapp/app/components/pages/add-question.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router';
3 | import ReactDOM from 'react-dom';
4 | import { t } from 'localizify';
5 |
6 | import auth from '../../auth';
7 | import formatText from '../../utils/format-str';
8 |
9 |
10 | const TagsVariants = React.createClass({
11 | render() {
12 | const data = this.props.data;
13 |
14 | return (
15 |
23 | );
24 | }
25 | });
26 |
27 | const Tag = React.createClass({
28 | render() {
29 | return (
30 | {this.props.name}
31 | );
32 | }
33 | });
34 |
35 | const SelectedTags = React.createClass({
36 | render() {
37 | const data = this.props.data;
38 |
39 | return (
40 |
41 |
Теги:
42 |
43 | {data.map((item, index) =>
44 |
45 |
46 |
47 | )}
48 |
49 |
50 | );
51 | }
52 | });
53 |
54 | const AddQuestionPage = withRouter(
55 | React.createClass({
56 | getInitialState() {
57 | return {
58 | error: false,
59 | message: '',
60 | tags: [],
61 | findedTags: [],
62 | inputValue: ''
63 | }
64 | },
65 |
66 | handleSubmit(event) {
67 | event.preventDefault();
68 |
69 | const title = this.refs.title.value.trim();
70 | const comment = this.refs.comment.value.trim();
71 | const tags = this.state.tags.join(',');
72 |
73 | $.ajax({
74 | type: 'POST',
75 | url: `${window.config.basename}/api/question`,
76 | contentType: 'application/json',
77 | data: JSON.stringify({ title, comment, tags, token: auth.getToken() }),
78 | success: data => {
79 | console.log(data);
80 | if (data.msg) {
81 | const { location } = this.props
82 |
83 | if (location.state && location.state.nextPathname) {
84 | this.props.router.replace(location.state.nextPathname)
85 | } else {
86 | this.props.router.replace(`/questions/${data.msg}`)
87 | }
88 | } else {
89 | console.error(data);
90 | }
91 | },
92 | error: (xhr, status, err) => {
93 | console.error(status, err.toString());
94 | }
95 | });
96 |
97 | return false;
98 | },
99 |
100 | onChange(e) {
101 | // e.preventDefault();
102 |
103 | var searchedTerm = ReactDOM.findDOMNode(this.refs.searched).value.trim();
104 | this.setState({ inputValue: searchedTerm });
105 | if (!searchedTerm) {
106 | this.setState({ findedTags: [] });
107 | return;
108 | }
109 |
110 | $.ajax({
111 | type: 'POST',
112 | // data: { q: text }
113 | url: `${window.config.basename}/api/tags/${searchedTerm}`,
114 | contentType: 'application/json',
115 | })
116 | .done(response => {
117 | if (response) {
118 | console.log(JSON.stringify(response));
119 | const findedTags = response;
120 | this.setState({ findedTags });
121 | }
122 | });
123 |
124 |
125 | },
126 |
127 | addNewTag(e) {
128 | event.preventDefault();
129 | var tag = ReactDOM.findDOMNode(this.refs.searched).value.trim();
130 | this.addTag(tag);
131 | return false;
132 | },
133 |
134 | addTag(tag) {
135 | console.log(`new tag: ${tag}`);
136 | if (tag) {
137 | if (this.state.tags.indexOf(tag) === -1) {
138 | this.setState({ tags: [tag].concat(this.state.tags) });
139 | }
140 | this.setState({ inputValue: '', findedTags: [] });
141 | }
142 | },
143 |
144 | onAddNewTag(event) {
145 | event.preventDefault()
146 | const that = event.currentTarget;
147 |
148 | var tag = $(that).find('b').text(); // .replace(/[^a-zA-ZА-Яа-яЁё0-9-_]+/g,'');
149 | this.addTag(tag);
150 | },
151 |
152 | onTagClick(event) {
153 | console.log('delete tag!');
154 |
155 | event.preventDefault()
156 | const that = event.currentTarget;
157 |
158 | var deletedTag = $(that).text();
159 | if (this.state.tags.indexOf(deletedTag) !== -1) {
160 |
161 | const newTags = this.state.tags.filter(name => name !== deletedTag);
162 | this.setState({ tags: newTags })
163 |
164 | // tagsArray.splice(tagsArray.indexOf(deletedTag), 1);
165 | console.log(JSON.stringify(this.state.tags));
166 | }
167 | },
168 |
169 | onChangeAnswer() {
170 | const comment = this.refs.comment.value.trim();
171 | $('.preview').html(formatText(comment));
172 | },
173 |
174 | render() {
175 |
176 | const tags = this.state.tags;
177 |
178 | return (
179 |
205 | )
206 | }
207 | })
208 | );
209 |
210 | export default AddQuestionPage;
211 |
--------------------------------------------------------------------------------
/src/main/java/com/mkyong/web/controller/api/UserController.java:
--------------------------------------------------------------------------------
1 | package com.mkyong.web.controller.api;
2 |
3 | import com.fasterxml.jackson.annotation.JsonView;
4 | import com.mkyong.web.model.AjaxResponseBody;
5 | import com.mkyong.web.model.AnswerModel;
6 | import com.mkyong.web.model.ChangePasswordModel;
7 | import com.mkyong.web.util.AuthService;
8 | import com.mkyong.web.util.CustomErrorType;
9 | import com.mkyong.web.entity.User;
10 | import com.mkyong.web.jsonview.Views;
11 | import com.mkyong.web.service.UserService;
12 | import com.mkyong.web.util.MD5;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 | import org.springframework.beans.factory.annotation.Autowired;
16 | import org.springframework.beans.factory.annotation.Value;
17 | import org.springframework.http.HttpHeaders;
18 | import org.springframework.http.HttpStatus;
19 | import org.springframework.http.ResponseEntity;
20 | import org.springframework.web.bind.annotation.*;
21 | import org.springframework.web.util.UriComponentsBuilder;
22 |
23 | import java.util.List;
24 | import java.util.Objects;
25 |
26 | @RestController
27 | @RequestMapping("/api")
28 | public class UserController {
29 | public static final Logger logger = LoggerFactory.getLogger(UserController.class);
30 |
31 | @Value("${jwt.secret}")
32 | private String key;
33 |
34 | @Autowired
35 | UserService userService; //Service which will do all data retrieval/manipulation work
36 |
37 | @JsonView(Views.Public.class)
38 | @RequestMapping(value = "/users", method = RequestMethod.GET)
39 | public ResponseEntity> listAllUsers() {
40 | List users = userService.getAll();
41 | if (users.isEmpty()) {
42 | return new ResponseEntity(HttpStatus.NO_CONTENT);
43 | // You many decide to return HttpStatus.NOT_FOUND
44 | }
45 | return new ResponseEntity>(users, HttpStatus.OK);
46 | }
47 |
48 | @RequestMapping(value = "/users/name/{name}", method = RequestMethod.GET)
49 | public ResponseEntity> getUserByName(@PathVariable("name") String name) {
50 | logger.info("Fetching User with name {}", name);
51 | User user = userService.getByUsername(name);
52 | if (user == null) {
53 | logger.error("User with name {} not found.", name);
54 | return new ResponseEntity(new CustomErrorType("User with name " + name
55 | + " not found"), HttpStatus.NOT_FOUND);
56 | }
57 | user.setAnswers(null);
58 | user.setQuestions(null);
59 | user.setPassword(null);
60 | return new ResponseEntity(user, HttpStatus.OK);
61 | }
62 |
63 | @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
64 | public ResponseEntity> getUser(@PathVariable("id") long id) {
65 | logger.info("Fetching User with id {}", id);
66 | User user = userService.getById(id);
67 | if (user == null) {
68 | logger.error("User with id {} not found.", id);
69 | return new ResponseEntity(new CustomErrorType("User with id " + id
70 | + " not found"), HttpStatus.NOT_FOUND);
71 | }
72 | user.setAnswers(null);
73 | user.setQuestions(null);
74 | user.setPassword(null);
75 | return new ResponseEntity(user, HttpStatus.OK);
76 | }
77 |
78 | @RequestMapping(value = "/users", method = RequestMethod.POST)
79 | public ResponseEntity> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {
80 | logger.info("Creating User : {}", user);
81 |
82 | if (userService.isUserExist(user)) {
83 | logger.error("Unable to create. A User with name {} already exist", user.getUsername());
84 | return new ResponseEntity(new CustomErrorType("Unable to create. A User with name " +
85 | user.getUsername() + " already exist."),HttpStatus.CONFLICT);
86 | }
87 | userService.addUser(user);
88 |
89 | HttpHeaders headers = new HttpHeaders();
90 | headers.setLocation(ucBuilder.path("/api/user/{id}").buildAndExpand(user.getId()).toUri());
91 | return new ResponseEntity(headers, HttpStatus.CREATED);
92 | }
93 |
94 |
95 |
96 |
97 |
98 | @RequestMapping(value = "/user/changepassword", method = RequestMethod.POST)
99 | public AjaxResponseBody createQuestion(@RequestBody ChangePasswordModel data, UriComponentsBuilder ucBuilder) {
100 | AjaxResponseBody result = new AjaxResponseBody();
101 |
102 | AuthService authService = new AuthService(data.getToken(), key);
103 | if (authService.getUserName() == null) {
104 | result.setCode("404");
105 | result.setMsg(authService.getMessage());
106 | return result;
107 | }
108 | //OK, we can trust this JWT
109 | String userName = authService.getUserName();
110 |
111 | User user = userService.getByUsername(userName);
112 |
113 | if (!Objects.equals(user.getPassword(), MD5.getHash(data.getOld_password()))) {
114 | result.setCode("404");
115 | result.setMsg("wrong old password");
116 | return result;
117 | }
118 |
119 | user.setPassword(MD5.getHash(data.getPassword()));
120 |
121 | userService.editUser(user);
122 |
123 | result.setCode("201");
124 | result.setMsg(Long.toString(user.getId()));
125 |
126 | return result;
127 |
128 | }
129 |
130 |
131 | @RequestMapping(value = "/users/{id}", method = RequestMethod.PUT)
132 | public ResponseEntity> updateUser(@PathVariable("id") long id, @RequestBody User user) {
133 | logger.info("Updating User with id {}", id);
134 |
135 | User currentUser = userService.getById(id);
136 |
137 | if (currentUser == null) {
138 | logger.error("Unable to update. User with id {} not found.", id);
139 | return new ResponseEntity(new CustomErrorType("Unable to upate. User with id " + id + " not found."),
140 | HttpStatus.NOT_FOUND);
141 | }
142 |
143 | currentUser.setUsername(user.getUsername());
144 | currentUser.setPassword(user.getPassword());
145 |
146 | userService.editUser(currentUser);
147 | return new ResponseEntity(currentUser, HttpStatus.OK);
148 | }
149 |
150 | @RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
151 | public ResponseEntity> deleteUser(@PathVariable("id") long id) {
152 | logger.info("Fetching & Deleting User with id {}", id);
153 |
154 | User user = userService.getById(id);
155 | if (user == null) {
156 | logger.error("Unable to delete. User with id {} not found.", id);
157 | return new ResponseEntity(new CustomErrorType("Unable to delete. User with id " + id + " not found."),
158 | HttpStatus.NOT_FOUND);
159 | }
160 | userService.delete(id);
161 | return new ResponseEntity(HttpStatus.NO_CONTENT);
162 | }
163 |
164 | // @RequestMapping(value = "/user/", method = RequestMethod.DELETE)
165 | // public ResponseEntity deleteAllUsers() {
166 | // logger.info("Deleting All Users");
167 | //
168 | // userService.deleteAllUsers();
169 | // return new ResponseEntity(HttpStatus.NO_CONTENT);
170 | // }
171 | }
172 |
--------------------------------------------------------------------------------
/src/main/webapp/resources/css/question.css:
--------------------------------------------------------------------------------
1 | .token-text {
2 | font-size: 0.6em;
3 | }
4 |
5 | .post-container {
6 | padding: 14px 0 13px 0;
7 | border-bottom: 1px solid #e4e6e8;
8 | font-size: 13px;
9 | margin-left: 70px;
10 | height: 50px;
11 | }
12 |
13 | .post-container>a {
14 | float: left;
15 |
16 | }
17 |
18 | /*.post-container .vote {
19 | border-color: #5fba7d;
20 | background: #5fba7d;
21 | color: #FFF;
22 | }*/
23 |
24 | .post-container .vote {
25 | font-size: 12px;
26 | font-weight: normal;
27 | line-height: 1.3;
28 | text-align: center;
29 | color: #6a737c;
30 | min-width: 36px;
31 | height: auto;
32 | padding: 3px 6px;
33 | margin-right: 10px;
34 | display: inline-block;
35 | border: 1px solid #b9b9b9;
36 | border-radius: 2px;
37 | }
38 |
39 | .post-container a, .post-container .post-date {
40 | padding-top: 4px;
41 | }
42 |
43 | .post-date {
44 | display: block;
45 | float: right;
46 | }
47 |
48 | .post-container a {
49 | font-size: 13px;
50 | font-weight: 400;
51 | /*width: 79%;*/
52 | margin-bottom: 0;
53 | float: left;
54 | }
55 |
56 | .user-stats {
57 | color: #666;
58 | padding: 10px;
59 | font-size: 0.8em;
60 | margin-bottom: 30px;
61 | }
62 |
63 | .user-stats .number {
64 | display: block;
65 | color: #0C0D0E;
66 | font-weight: 700;
67 | font-size: 1.8em;
68 | }
69 |
70 | .col-3 {
71 | width: 100px;
72 | float: left;
73 | }
74 |
75 | /* ........................ */
76 |
77 |
78 |
79 |
80 | textarea {
81 | padding: 10px;
82 | height: 200px;
83 | line-height: 1.3;
84 | width: 100%;
85 | resize: vertical;
86 | }
87 |
88 | input {
89 | padding: 5px 10px;
90 | line-height: 1.3;
91 | width: 100%;
92 | height: 30px;
93 | margin: 5px;
94 | /* border: 1px solid rgba(0,0,0,0);*/
95 | }
96 |
97 | textarea {
98 | font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;
99 | }
100 |
101 | input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, textarea:focus {
102 | outline: 0;
103 | border: 1px solid #07C;
104 | }
105 |
106 | input:focus {
107 | outline-offset: 0;
108 | outline: 0;
109 | border: 1px solid #07C;
110 | }
111 |
112 | .btn-block + .btn-block {
113 | margin-top: 5px;
114 | }
115 | .btn-github {
116 | color: #fff;
117 | background-color: #444;
118 | border-color: rgba(0,0,0,0.2);
119 | }
120 |
121 | .btn-linkedin {
122 | color: #fff;
123 | background-color: #007bb6;
124 | border-color: rgba(0,0,0,0.2);
125 | }
126 |
127 | .btn-social {
128 | position: relative;
129 | padding-left: 44px;
130 | text-align: left;
131 | white-space: nowrap;
132 | overflow: hidden;
133 | text-overflow: ellipsis;
134 | }
135 | .btn-block {
136 | display: block;
137 | width: 100%;
138 | }
139 | .btn {
140 | display: inline-block;
141 | padding: 6px 12px;
142 | margin-bottom: 0;
143 | font-size: 14px;
144 | font-weight: normal;
145 | line-height: 1.42857143;
146 | text-align: center;
147 | white-space: nowrap;
148 | vertical-align: middle;
149 | -ms-touch-action: manipulation;
150 | touch-action: manipulation;
151 | cursor: pointer;
152 | -webkit-user-select: none;
153 | -moz-user-select: none;
154 | -ms-user-select: none;
155 | user-select: none;
156 | background-image: none;
157 | border: 1px solid transparent;
158 | border-radius: 4px;
159 | }
160 |
161 | /* ------------------------------------------- */
162 |
163 |
164 |
165 | .answer-summary {
166 | display: block;
167 | overflow: hidden;
168 | padding: 15px 0;
169 | /*float: left;*/
170 | /*width: 628px;*/
171 | border-bottom: 1px solid #eff0f1;
172 | }
173 |
174 |
175 | .hide {
176 | display: none;
177 | }
178 |
179 |
180 |
181 | .question-content, .answer-list {
182 | padding-left: 65px;
183 | }
184 |
185 | .envelope-on, .envelope-off, .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .comment-up-off, .comment-up-on, .comment-flag, .comment-flag-off, .comment-flag-on, .edited-yes, .feed-icon, .vote-accepted-off, .vote-accepted-on, .vote-accepted-bounty, .badge-earned-check, .delete-tag, .grippie, .expander-arrow-hide, .expander-arrow-show, .expander-arrow-small-hide, .expander-arrow-small-show, .anonymous-gravatar, .badge1, .badge2, .badge3 {
186 | background-image: url("sprites.png");
187 | background-size: initial;
188 | background-repeat: no-repeat;
189 | overflow: hidden;
190 | }
191 | .vote {
192 | min-width: 46px;
193 | text-align: center;
194 |
195 |
196 | position: absolute;
197 | margin-left: -70px;
198 |
199 | }
200 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .vote-accepted-off, .vote-accepted-on {
201 | display: block;
202 | margin: 0 auto;
203 | text-indent: -999em;
204 | width: 40px;
205 | height: 40px;
206 | margin-bottom: 2px;
207 | }
208 |
209 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off {
210 | height: 30px;
211 | }
212 | .vote-up-off {
213 | background-position: 0 -170px;
214 | }
215 |
216 | .vote-down-on {
217 | background-position: -40px -220px;
218 | margin-bottom: 8px;
219 | }
220 |
221 | .vote-up-on {
222 | background-position: -40px -170px;
223 | }
224 |
225 | .vote-down-off {
226 | background-position: 0 -220px;
227 | margin-bottom: 8px;
228 | }
229 |
230 | .vote-up-off, .vote-up-on, .vote-down-off, .vote-down-on, .star-on, .star-off, .comment-up-off, .comment-up-on, .comment-flag, .comment-flag-off, .comment-flag-on, .flag-off, .vote-accepted-off, .vote-accepted-on {
231 | text-indent: -9999em;
232 | font-size: 1px;
233 | }
234 | .vote-up-off, .vote-down-off, .vote-accepted-off, .star-off, .comment-up-off, .flag-off {
235 | cursor: pointer;
236 | }
237 |
238 |
239 | .votecell .vote-count-post {
240 | margin: 8px 0;
241 | }
242 | .vote span {
243 | display: block;
244 | color: #6a737c;
245 | }
246 |
247 | .vote-count-post {
248 | display: block;
249 | font-size: 20px;
250 | margin: 0 0 3px 0;
251 | }
252 |
253 |
254 |
255 | .share-block {
256 | font-size: 0.78em;
257 | padding-top: 15px;
258 | }
259 |
260 |
261 | .margin-top-100 {
262 | margin-top: 100px;
263 | }
264 |
265 | .margin-top-20 {
266 | margin-top: 20px;
267 | }
268 |
269 | .user-info {
270 | margin-top: 15px;
271 | box-sizing: border-box;
272 | padding: 5px 6px 7px 7px;
273 | width: 190px;
274 | color: #6a737c;
275 | float: right;
276 | background: #efefef;
277 | font-size: 12px;
278 | }
279 |
280 | .light {
281 | border: 0;
282 | height: 0;
283 | border-bottom: 1px solid #e5e5e5;
284 | }
285 |
286 | .code-inline {
287 | background: #f5f5f5;
288 | /* background: #3a4145;
289 | color: white;*/
290 | display: inline-block;
291 | padding: 0 2px;
292 | font-family: 'Courier new';
293 | font-size: 0.9em;
294 | }
295 |
296 |
297 | .tags-label {
298 | height:40px;padding-top: 10px;
299 | }
300 |
301 | .tags-label b {
302 | float:left;
303 | }
304 |
305 | .menu .active u {
306 | color: #ffffff !important;
307 | background: #484646;
308 | border-radius: 1px;
309 | }
310 |
311 | .menu .right a.active {
312 | color: #ffffff !important;
313 | background: #484646;
314 | border-radius: 1px;
315 | }
316 |
317 | .menu .li a {
318 | margin: 8px 10px !important;
319 | padding: 0 !important;
320 | }
321 |
322 | .loader {
323 | width: 100%;
324 | height: 100%;
325 | position: relative;
326 | text-align: center;
327 | padding-top: 30%;
328 | /*color: black;*/
329 | /*background-color: black;*/
330 | }
331 |
332 | .line-scale-party>div, .line-scale-pulse-out-rapid>div, .line-scale-pulse-out>div, .line-scale>div, .line-spin-fade-loader>div {
333 | border-radius: 2px;
334 | margin: 2px;
335 | background-color: rgba(0, 0, 0, 0.7);
336 | }
337 |
338 | .cp__item {
339 | width: 53px;
340 | text-align: center;
341 | }
342 |
343 | .clear {
344 | clear: both;
345 | }
346 |
347 | .clear:after {
348 | content: "";
349 | display: block;
350 | clear: both;
351 | }
352 |
353 | .padding-bottom-10 {
354 | padding-bottom: 10px;
355 | }
356 |
357 | .padding-top-10 {
358 | padding-top: 10px;
359 | }
360 |
361 | .color-red {
362 | color: red
363 | }
364 |
365 | .question-list {
366 | clear: both;
367 | overflow: hidden;
368 | margin-bottom: 30px;
369 | }
370 |
371 | .question-summary {
372 | padding: 12px 0 10px 0;
373 | border-bottom: 1px solid #e4e6e8;
374 | display: block;
375 | }
376 | .question-summary {
377 | overflow: hidden;
378 | padding: 15px 0;
379 | float: left;
380 | width: 728px;
381 | border-bottom: 1px solid #eff0f1;
382 | }
383 |
384 | .narrow .cp {
385 | width: 180px;
386 | }
387 | .narrow .cp {
388 | vertical-align: top;
389 | float: left;
390 | margin-right: 10px;
391 | margin-top: 10px;
392 | }
393 | .cp {
394 | cursor: pointer;
395 | }
396 |
397 | .narrow .status {
398 | display: inline-block;
399 | margin: 0 3px 0 0;
400 | min-width: 44px;
401 | height: auto;
402 | font-size: 11px;
403 | padding: 6px;
404 | }
405 | .narrow .votes, .narrow .status, .narrow .views {
406 | padding: 8px 5px;
407 | line-height: 1;
408 | }
409 | .narrow .status {
410 | color: #848d95;
411 | }
412 |
413 | .views {
414 | padding-top: 4px;
415 | text-align: center;
416 | }
417 | .narrow .votes, .narrow .status, .narrow .views {
418 | padding: 8px 5px;
419 | line-height: 1;
420 | }
421 | .narrow .views {
422 | display: inline-block;
423 | height: 38px;
424 | min-width: 40px;
425 | margin: 0 7px 0 0;
426 | font-size: 11px;
427 | color: #848d95;
428 | padding: 5px 5px 6px 5px;
429 | }
430 | .narrow .views {
431 | max-width: 40px;
432 | }
433 |
434 | .summary {
435 | float: left;
436 | width: 630px;
437 | }
438 | .narrow .summary {
439 | width: 430px;
440 | padding: 0;
441 | float: left;
442 | }
443 |
444 | .summary h3 {
445 | font-size: 15px;
446 | line-height: 1.4;
447 | margin-bottom: .5em;
448 | }
449 | .narrow .summary h3 {
450 | margin-bottom: .35em;
451 | line-height: 1.3;
452 | }
453 |
454 | .tags {
455 | line-height: 18px;
456 | /* float: left;*/
457 | }
458 |
459 | .post-tag, .moderator-tag, .required-tag, .disliked-tag, .favorite-tag, .company-tag, .geo-tag, .geo-tag, .container .chosen-choices .search-choice, .container .chosen-container-multi .chosen-choices li.search-choice {
460 | position: relative;
461 | display: inline-block;
462 | padding: .4em .5em;
463 | margin: 2px 2px 2px 0;
464 | font-size: 11px;
465 | line-height: 1;
466 | white-space: nowrap;
467 | text-decoration: none;
468 | text-align: center;
469 | border-width: 1px;
470 | border-style: solid;
471 | border-radius: 0;
472 | transition: all .15s ease-in-out;
473 | }
474 |
475 | .post-tag, .geo-tag, .container .chosen-choices .search-choice, .container .chosen-container-multi .chosen-choices li.search-choice {
476 | font-size: 12px;
477 | }
478 | .post-tag, .geo-tag, .container .chosen-choices .search-choice, .container .chosen-container-multi .chosen-choices li.search-choice {
479 | color: #39739d;
480 | background-color: #E1ECF4;
481 | border-color: transparent;
482 | }
483 | .started {
484 | width: 200px;
485 | float: right;
486 | line-height: 18px;
487 | font-family: 'Arial';
488 | }
489 | .narrow .started {
490 | width: auto;
491 | line-height: inherit;
492 | padding-top: 4px;
493 | white-space: nowrap;
494 | font-size: 12px;
495 | }
496 |
497 | .narrow .started a.name {
498 | font-weight: bold;
499 | }
500 |
501 | .started-link {
502 | font-size: 12px;
503 | color: #9199a1;
504 | }
505 |
506 | .started .user-info, .started .reputation-score {
507 | color: #848d95;
508 | }
509 | .reputation-score {
510 | font-weight: bold;
511 | font-size: 12px;
512 | margin-right: 2px;
513 | }
514 |
515 | .relativetime {
516 | text-decoration: none;
517 | }
518 | .question-hyperlink:visited, .answer-hyperlink:visited, #hot-network-questions ul a:visited {
519 | color: #005999;
520 | }
521 | .question-hyperlink, .answer-hyperlink {
522 | color: #07C;
523 | line-height: 1.3;
524 | margin-bottom: 1.2em;
525 | }
526 |
527 | .question-hyperlink {
528 | font-size: 16px;
529 | font-weight: 400;
530 | }
531 |
532 | .votes {
533 | padding: 0;
534 | margin-bottom: 8px;
535 | text-align: center;
536 | font-family: 'Arial';
537 | }
538 | .narrow .votes {
539 | display: inline-block;
540 | height: 38px;
541 | min-width: 38px;
542 | margin: 0 3px 0 0;
543 | font-size: 11px;
544 | color: #848d95;
545 | padding: 5px 5px 6px 5px;
546 | }
547 | .narrow .votes, .narrow .status, .narrow .views {
548 | padding: 8px 5px;
549 | line-height: 1;
550 | }
551 | .narrow .mini-counts {
552 | font-size: 22px;
553 | font-weight: 300;
554 | color: #6a737c;
555 | margin-bottom: 4px;
556 | text-align: center;
557 | }
558 | .narrow .mini-counts {
559 | margin-bottom: 2px;
560 | }
561 |
562 | .language-switcher span {
563 | padding: 3px;
564 | cursor: pointer;
565 | }
566 |
567 | .language-switcher .active {
568 | font-weight: bold;
569 | color: white;
570 | background: #222;
571 | padding: 0 1px 0 2px;
572 | }
573 |
574 |
575 |
576 | .language-switcher {
577 | font-size: 0.9em;
578 | padding-left: 15px;
579 | }
--------------------------------------------------------------------------------