├── 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 |
  1. Nulla pulvinar diam
  2. 12 |
  3. Facilisis bibendum
  4. 13 |
  5. Vestibulum vulputate
  6. 14 |
  7. Eget erat
  8. 15 |
  9. Id porttitor
  10. 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 |
27 | 28 |
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 |
25 | 26 |
27 | 28 |
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 |
40 |
41 |
42 | 43 | {this.state.error && ( 44 |

{this.state.message}

45 | )} 46 |
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 |
40 |
41 |
42 | 43 | {this.state.error && ( 44 |

{this.state.message}

45 | )} 46 |
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 | ![alt tag](src/main/webapp/resources/preview.png) 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 |
26 | 27 |
28 | 29 |
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 |
48 | 49 |
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 |
58 |
59 |
60 |
61 | 62 | {this.state.error && ( 63 |

{this.state.message}

64 | )} 65 |
66 |
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 |
107 | up vote 108 | {this.state.rating} 109 | down vote 110 |
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 |
124 | 125 |
126 |
127 | 128 |
129 | 130 | 131 |
132 | 133 |
134 |

{t('Answers')}

135 | 136 | 137 | {t('Add answer')}: 138 | 139 |
140 |
141 | {t('Message')}
142 |
143 |
144 |
145 | 146 |
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 |
16 | {data.map((item, index) => 17 | 18 | {item.name} ({item.popular})  19 | 20 | )} 21 | 22 |
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 |
180 |

{t('Add question')}

181 |
182 | {t('Question name')}:
183 | {t('Description')}: