Compare commits
54 Commits
gitActionT
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da13e49d62 | ||
|
|
b47b947e9d | ||
|
|
5091f21a16 | ||
|
|
8e25130109 | ||
|
|
22a42d0920 | ||
|
|
e1f0ccfd9c | ||
|
|
c897d26762 | ||
|
|
e6b11d6d0f | ||
|
|
427eae6efd | ||
|
|
74451508e9 | ||
|
|
20487f5a46 | ||
|
|
828c744285 | ||
|
|
b4cf76689c | ||
|
|
624c509a41 | ||
|
|
5a40832408 | ||
|
|
b8caa0140b | ||
|
|
ebf851d1c5 | ||
|
|
a55f0c28f8 | ||
|
|
aeb52fa2f4 | ||
|
|
ce1e1ca573 | ||
|
|
5f4bdf13ea | ||
|
|
eaa50e298b | ||
|
|
8e8a0b9c48 | ||
|
|
c51f469124 | ||
|
|
2248112454 | ||
|
|
4fc89b0e5b | ||
|
|
311ab00234 | ||
|
|
e8247f7521 | ||
|
|
021c8d7b74 | ||
|
|
534f62c4e8 | ||
|
|
ea3ab8bc3a | ||
|
|
3d212c0302 | ||
|
|
020c59d380 | ||
|
|
a1c45d5a82 | ||
|
|
8f30ee0a19 | ||
|
|
59d0f6c97d | ||
|
|
a709acac4b | ||
|
|
cf9edf2cfe | ||
|
|
76b3121b67 | ||
|
|
df3c2e19af | ||
|
|
918b6c8cb2 | ||
|
|
99dd1d292f | ||
|
|
9471351943 | ||
|
|
ebd42df504 | ||
|
|
2855b56749 | ||
|
|
f25e02777f | ||
|
|
81655e7509 | ||
|
|
ff9b481a82 | ||
|
|
cfe4e8b0dd | ||
|
|
8c78ad0bdf | ||
|
|
176e588ad0 | ||
|
|
9815200315 | ||
|
|
0909eab9c1 | ||
|
|
918a6a2b9e |
3
.github/workflows/autoTest.yml
vendored
3
.github/workflows/autoTest.yml
vendored
@@ -7,6 +7,9 @@ on:
|
||||
jobs:
|
||||
real-world:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
working-directory: .
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
||||
39
README.md
39
README.md
@@ -11,12 +11,15 @@ We've gone to great lengths to adhere to the **[Spring Boot]** community stylegu
|
||||
|
||||
For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo.
|
||||
|
||||
-----
|
||||
|
||||
## Enter
|
||||
|
||||
you can See Vue.js + SpringBoot FullStack Web Site Demo
|
||||
|
||||
<http://3.35.44.58:8080/>
|
||||
|
||||
------
|
||||
|
||||
## BackEnd - Spring
|
||||
|
||||
@@ -24,3 +27,39 @@ you can See Vue.js + SpringBoot FullStack Web Site Demo
|
||||
|
||||
<img width="464" alt="image" src="https://user-images.githubusercontent.com/30401054/201084053-60be024d-0615-40e1-9234-ceb926f402e5.png">
|
||||
|
||||
-----
|
||||
|
||||
### `Jacoco` Code Coverage (2022.11.17)
|
||||
|
||||

|
||||
|
||||
#### **Total 82% Code Coverage**
|
||||
|
||||
-----
|
||||
|
||||
# How it works
|
||||
|
||||
- Spring Boot(Java)
|
||||
- JPA
|
||||
- Security
|
||||
- H2
|
||||
- Vue3
|
||||
- Vite
|
||||
- vuerouter
|
||||
- vuex
|
||||
- localStorage
|
||||
|
||||
# Getting started
|
||||
|
||||
## Run Local
|
||||
```shell
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
# FrontEnd
|
||||
|
||||
**I don't know much about the front end. I wanted to create a visible application, so I adopted and implemented that framework.**
|
||||
|
||||
But my codes are simple codes that even beginners can easily see.
|
||||
|
||||
|
||||
|
||||
22
build.gradle
22
build.gradle
@@ -2,6 +2,7 @@ plugins {
|
||||
id 'org.springframework.boot' version '2.7.3'
|
||||
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
|
||||
id 'java'
|
||||
id 'jacoco'
|
||||
id 'com.github.node-gradle.node' version '3.5.0'
|
||||
}
|
||||
|
||||
@@ -86,6 +87,27 @@ dependencies {
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.8"
|
||||
reportsDir = file("$buildDir/customJacocoReportDir")
|
||||
}
|
||||
|
||||
jacocoTestReport {
|
||||
reports {
|
||||
xml.enabled false
|
||||
csv.enabled false
|
||||
html.destination file("${buildDir}/jacocoHtml")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
test {
|
||||
finalizedBy jacocoTestReport // report is always generated after tests run
|
||||
}
|
||||
jacocoTestReport {
|
||||
dependsOn test // tests are required to run before generating the report
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ REPOSITORY=/home/linux/app
|
||||
|
||||
echo "> 현재 구동 중인 애플리케이션 pid 확인"
|
||||
|
||||
CURRENT_PID=$(pgrep -fl action | grep java | awk '{print $1}')
|
||||
CURRENT_PID=$(pgrep -fl java | awk '{print $1}')
|
||||
|
||||
echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"
|
||||
|
||||
|
||||
@@ -2,16 +2,14 @@ package com.io.realworld.domain.aggregate.article.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.*;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@JsonTypeName("article")
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
|
||||
@@ -20,5 +18,4 @@ public class Articledto {
|
||||
private String description;
|
||||
private String body;
|
||||
private List<String> tagList;
|
||||
|
||||
}
|
||||
|
||||
@@ -22,4 +22,8 @@ public interface ArticleRepository extends JpaRepository<Article,Long> {
|
||||
@EntityGraph(attributePaths = "tagList")
|
||||
@Query("SELECT a FROM Article a LEFT JOIN Favorite f ON f.article.id = a.id WHERE f.user.username =:username ORDER BY a.createdDate DESC")
|
||||
List<Article> findByFavoritedUser(String username, Pageable pageable);
|
||||
|
||||
@EntityGraph(attributePaths = "tagList" )
|
||||
@Query("SELECT a FROM Article a ORDER BY a.createdDate DESC")
|
||||
List<Article> findByAll(Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ public class ArticleServiceImpl implements ArticleService {
|
||||
articles = articleRepository.findByAuthorName(articleParam.getAuthor(), pageable);
|
||||
}else if(articleParam.getFavorited() != null){
|
||||
articles = articleRepository.findByFavoritedUser(articleParam.getFavorited(), pageable);
|
||||
}else{
|
||||
articles = articleRepository.findByAll(pageable);
|
||||
}
|
||||
|
||||
return articles.stream().map(article -> {
|
||||
@@ -71,7 +73,7 @@ public class ArticleServiceImpl implements ArticleService {
|
||||
|
||||
Pageable pageable = PageRequest.of(offset,limit);
|
||||
|
||||
List<Follow> follows = profileRepository.findByFollowerId(userAuth.getId());
|
||||
List<Follow> follows = profileRepository.findByFolloweeId(userAuth.getId());
|
||||
follows.stream().forEach(follow -> {
|
||||
String followerName = follow.getFollower().getUsername();
|
||||
articles.addAll(articleRepository.findByAuthorName(followerName,pageable));
|
||||
|
||||
@@ -61,9 +61,10 @@ public class CommentServiceImpl implements CommentService {
|
||||
if (article.isEmpty()) {
|
||||
throw new CustomException(Error.ARTICLE_NOT_FOUND);
|
||||
}
|
||||
System.out.println(user.get().getUsername()+"!!");
|
||||
Comment comment = commentRepository.save(Comment.builder().body(commentdto.getBody()).article(article.get()).author(user.get()).build());
|
||||
|
||||
return convertComment(userAuth, article.get(), comment);
|
||||
return convertComment(userAuth, comment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,9 +81,9 @@ public class CommentServiceImpl implements CommentService {
|
||||
commentRepository.delete(comment.get());
|
||||
}
|
||||
|
||||
private CommentResponse convertComment(UserAuth userAuth, Article article, Comment comment) {
|
||||
private CommentResponse convertComment(UserAuth userAuth, Comment comment) {
|
||||
|
||||
ProfileResponse profile = profileService.getProfile(userAuth, article.getAuthor().getUsername());
|
||||
ProfileResponse profile = profileService.getProfile(userAuth, userAuth.getUsername());
|
||||
|
||||
return CommentResponse.builder()
|
||||
.id(comment.getId())
|
||||
|
||||
@@ -13,4 +13,6 @@ public interface ProfileRepository extends JpaRepository<Follow, Long> {
|
||||
|
||||
List<Follow> findByFollowerId(Long followeeId);
|
||||
|
||||
List<Follow> findByFolloweeId(Long followerId);
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ProfileServiceImpl implements ProfileService {
|
||||
|
||||
@Override
|
||||
public ProfileResponse getProfile(UserAuth userAuth, String username) {
|
||||
Optional<User> wantFindUser = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> new CustomException(Error.USER_NOT_FOUND)));
|
||||
Optional<User> wantFindUser = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> {throw new CustomException(Error.USER_NOT_FOUND);}));
|
||||
Boolean followStatus = null;
|
||||
if(userAuth == null){
|
||||
followStatus = false;
|
||||
@@ -34,7 +34,7 @@ public class ProfileServiceImpl implements ProfileService {
|
||||
|
||||
@Override
|
||||
public ProfileResponse followUser(UserAuth userAuth, String username) {
|
||||
Optional<User> follower = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> new CustomException(Error.USER_NOT_FOUND)));
|
||||
Optional<User> follower = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> {throw new CustomException(Error.USER_NOT_FOUND);}));
|
||||
Optional<User> followee = userRepository.findById(userAuth.getId());
|
||||
profileRepository.findByFolloweeIdAndFollowerId(followee.get().getId(), follower.get().getId()).ifPresent(follow -> {
|
||||
throw new CustomException(Error.ALREADY_FOLLOW);
|
||||
@@ -46,7 +46,7 @@ public class ProfileServiceImpl implements ProfileService {
|
||||
|
||||
@Override
|
||||
public ProfileResponse unfollowUser(UserAuth userAuth, String username) {
|
||||
Optional<User> follower = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> new CustomException(Error.USER_NOT_FOUND)));
|
||||
Optional<User> follower = Optional.ofNullable(userRepository.findByUsername(username).orElseThrow(() -> {throw new CustomException(Error.USER_NOT_FOUND);}));
|
||||
Optional<User> followee = userRepository.findById(userAuth.getId());
|
||||
Follow follow = profileRepository.findByFolloweeIdAndFollowerId(followee.get().getId(), follower.get().getId()).orElseThrow(() -> {
|
||||
throw new CustomException(Error.ALREADY_UNFOLLOW);
|
||||
|
||||
@@ -3,10 +3,7 @@ package com.io.realworld.domain.aggregate.user.controller;
|
||||
import com.io.realworld.domain.aggregate.user.dto.UserSigninRequest;
|
||||
import com.io.realworld.domain.aggregate.user.dto.UserSignupRequest;
|
||||
import com.io.realworld.domain.aggregate.user.dto.UserResponse;
|
||||
import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
import com.io.realworld.domain.service.JwtService;
|
||||
import com.io.realworld.domain.aggregate.user.service.UserServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
@@ -14,17 +11,13 @@ import javax.validation.Valid;
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UsersController {
|
||||
|
||||
|
||||
private final UserServiceImpl userService;
|
||||
|
||||
|
||||
public UsersController(UserServiceImpl userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
|
||||
@PostMapping(value = "")
|
||||
@PostMapping
|
||||
public UserResponse signup(@Valid @RequestBody UserSignupRequest userSignupRequest) {
|
||||
return userService.signup(userSignupRequest);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.io.realworld.domain.aggregate.user.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -10,7 +9,6 @@ import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@JsonTypeName("user")
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
|
||||
|
||||
@@ -22,4 +22,6 @@ public class UserUpdate {
|
||||
private String email;
|
||||
private String bio;
|
||||
private String image;
|
||||
private String password;
|
||||
private String username;
|
||||
}
|
||||
|
||||
@@ -83,4 +83,8 @@ public class User implements UserDetails {
|
||||
this.bio = userUpdate.getBio();
|
||||
this.image = userUpdate.getImage();
|
||||
}
|
||||
|
||||
public void changePassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.io.realworld.domain.aggregate.user.repository;
|
||||
|
||||
import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -16,4 +14,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
Optional<User> findByUsername(String username);
|
||||
List<User> findAll();
|
||||
|
||||
List<User> findAllByUsername(String username);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.io.realworld.domain.aggregate.user.service;
|
||||
|
||||
import com.io.realworld.domain.aggregate.user.dto.*;
|
||||
import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
|
||||
public interface UserService {
|
||||
UserResponse signup(UserSignupRequest userSignupRequest);
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
import com.io.realworld.domain.aggregate.user.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -17,7 +16,6 @@ import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Log4j2
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
@@ -41,7 +39,8 @@ public class UserServiceImpl implements UserService {
|
||||
return convertUser(userRepository.save(User.builder().
|
||||
username(userSignupRequest.getUsername()).
|
||||
email(userSignupRequest.getEmail()).
|
||||
password(madeHash(userSignupRequest.getPassword())).build()));
|
||||
password(madeHash(userSignupRequest.getPassword())).
|
||||
image("https://api.realworld.io/images/smiley-cyrus.jpeg").build()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +54,12 @@ public class UserServiceImpl implements UserService {
|
||||
User findUser = userRepository.findByEmail(userSigninRequest.getEmail());
|
||||
if (findUser == null) {
|
||||
throw new CustomException(Error.EMAIL_NULL_OR_INVALID);
|
||||
}else{
|
||||
if(!passwordEncoder.matches(userSigninRequest.getPassword(), findUser.getPassword())){
|
||||
throw new CustomException(Error.PASSWORD_WRONG);
|
||||
}
|
||||
}
|
||||
|
||||
return convertUser(findUser);
|
||||
}
|
||||
|
||||
@@ -72,14 +76,23 @@ public class UserServiceImpl implements UserService {
|
||||
@Override
|
||||
@Transactional
|
||||
public UserResponse updateUser(UserUpdate userUpdate, UserAuth userAuth){
|
||||
User user = userRepository.findById(userAuth.getId()).orElseThrow(() -> new CustomException(Error.USER_NOT_FOUND));
|
||||
|
||||
User user = userRepository.findById(userAuth.getId()).orElseThrow(() -> {throw new CustomException(Error.USER_NOT_FOUND);});
|
||||
if(userUpdate.getEmail() != null){
|
||||
userRepository.findAllByEmail(userUpdate.getEmail())
|
||||
.stream().filter(found -> !found.getId().equals(userRepository.findById(user.getId())))
|
||||
.findAny().ifPresent(found -> new CustomException(Error.DUPLICATE_EMAIL));
|
||||
.stream().filter(found -> !found.getId().equals(user.getId()))
|
||||
.findFirst().ifPresent(found ->{throw new CustomException(Error.DUPLICATE_EMAIL);} );
|
||||
user.changeEmail(userUpdate.getEmail());
|
||||
}
|
||||
|
||||
if(userUpdate.getUsername() != null){
|
||||
userRepository.findAllByUsername(userUpdate.getUsername())
|
||||
.stream().filter(found -> !found.getId().equals(user.getId()))
|
||||
.findFirst().ifPresent(found -> {throw new CustomException(Error.DUPLICATE_USERNAME);});
|
||||
user.changeUsername(userUpdate.getUsername());
|
||||
}
|
||||
if(userUpdate.getPassword() != null){
|
||||
user.changePassword(madeHash(userUpdate.getPassword()));
|
||||
}
|
||||
userUpdate.setId(user.getId());
|
||||
user.update(userUpdate);
|
||||
return convertUser(userRepository.save(user));
|
||||
|
||||
@@ -11,6 +11,7 @@ public enum Error {
|
||||
DUPLICATE_USERNAME("duplicate user username", HttpStatus.CONFLICT),
|
||||
SIGNUP_NULL_DATA("request body include null",HttpStatus.BAD_REQUEST),
|
||||
EMAIL_NULL_OR_INVALID("email is blank or invalid check plz",HttpStatus.BAD_REQUEST),
|
||||
PASSWORD_WRONG("password is wrong.", HttpStatus.BAD_REQUEST),
|
||||
USER_NOT_FOUND("user not found check your info",HttpStatus.NOT_FOUND),
|
||||
ALREADY_FOLLOW("already follow",HttpStatus.UNPROCESSABLE_ENTITY),
|
||||
ALREADY_UNFOLLOW("already unfollow",HttpStatus.UNPROCESSABLE_ENTITY),
|
||||
|
||||
@@ -38,27 +38,19 @@ public class WebConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(0)
|
||||
public SecurityFilterChain resources(HttpSecurity http) throws Exception {
|
||||
http.csrf().disable()
|
||||
.requestMatchers((matchers) -> {matchers.antMatchers("/h2-console/**");
|
||||
matchers.antMatchers(HttpMethod.GET,"/api/articles/**","/**");})
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf()
|
||||
.disable()
|
||||
.requestMatchers((matchers) -> {
|
||||
matchers.antMatchers("/h2-console/**");
|
||||
matchers.antMatchers(HttpMethod.GET,"/api/articles/:slug","/**");
|
||||
matchers.mvcMatchers("/api/users/**");
|
||||
})
|
||||
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
|
||||
.requestCache().disable()
|
||||
.securityContext().disable()
|
||||
.sessionManagement().disable()
|
||||
.headers().frameOptions().disable();
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf()
|
||||
.disable()
|
||||
.authorizeRequests()
|
||||
.mvcMatchers("/api/users/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.formLogin()
|
||||
.disable()
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.io.realworld.domain.aggregate.article.controller;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.io.realworld.config.WithAuthUser;
|
||||
import com.io.realworld.domain.aggregate.article.dto.*;
|
||||
import com.io.realworld.domain.aggregate.article.entity.Article;
|
||||
import com.io.realworld.domain.aggregate.article.repository.ArticleRepository;
|
||||
import com.io.realworld.domain.aggregate.article.service.ArticleService;
|
||||
import com.io.realworld.domain.aggregate.article.service.CommentService;
|
||||
import com.io.realworld.domain.aggregate.user.dto.UserAuth;
|
||||
@@ -56,9 +58,10 @@ class ArticleControllerTest {
|
||||
|
||||
private String slug;
|
||||
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup(){
|
||||
|
||||
String title = "create title";
|
||||
slug = title.toLowerCase().replace(' ','-');
|
||||
articleResponse = ArticleResponse.builder()
|
||||
|
||||
@@ -120,7 +120,7 @@ class ArticleServiceImplTest {
|
||||
}};
|
||||
|
||||
when(profileService.getProfile(eq(userAuth), any(String.class))).thenReturn(ProfileResponse.builder().username(articles.get(0).getAuthor().getUsername()).build());
|
||||
when(profileRepository.findByFollowerId(any(Long.class))).thenReturn(follows);
|
||||
when(profileRepository.findByFolloweeId(any(Long.class))).thenReturn(follows);
|
||||
when(articleRepository.findByAuthorName(any(String.class),any(Pageable.class))).thenReturn(articles);
|
||||
List<ArticleResponse> articleResponses = articleService.getFeed(userAuth, feedParam);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.io.realworld.domain.aggregate.profile.repository;
|
||||
|
||||
import com.io.realworld.domain.aggregate.profile.entity.Follow;
|
||||
import com.io.realworld.domain.aggregate.user.dto.UserAuth;
|
||||
import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
import com.io.realworld.domain.aggregate.user.repository.UserRepository;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -8,6 +9,8 @@ import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -28,7 +31,7 @@ class ProfileRepositoryTest {
|
||||
|
||||
userRepository.save(followee);
|
||||
userRepository.save(follower);
|
||||
Follow follow = Follow.builder().followee(followee).follower(follower).build();
|
||||
Follow follow = Follow.builder().id(1L).followee(followee).follower(follower).build();
|
||||
//when
|
||||
Follow getFollow = profileRepository.save(follow);
|
||||
|
||||
@@ -55,7 +58,7 @@ class ProfileRepositoryTest {
|
||||
//given
|
||||
userRepository.save(followee);
|
||||
userRepository.save(follower);
|
||||
Follow follow = Follow.builder().followee(followee).follower(follower).build();
|
||||
Follow follow = Follow.builder().id(1L).followee(followee).follower(follower).build();
|
||||
//when
|
||||
profileRepository.save(follow);
|
||||
Optional<Follow> getFollow = profileRepository.findByFolloweeIdAndFollowerId(followee.getId(),follower.getId());
|
||||
@@ -67,7 +70,7 @@ class ProfileRepositoryTest {
|
||||
}
|
||||
|
||||
@MethodSource("getFolloweeAndFollower")
|
||||
@ParameterizedTest(name = "repo:팔로잉 제거 테스거")
|
||||
@ParameterizedTest(name = "repo:팔로잉 제거 테스터")
|
||||
void deleteFollow(User followee, User follower){
|
||||
//given
|
||||
userRepository.save(followee);
|
||||
@@ -80,6 +83,37 @@ class ProfileRepositoryTest {
|
||||
//then
|
||||
}
|
||||
|
||||
@MethodSource("getFolloweeAndFollower")
|
||||
@ParameterizedTest(name = "repo:피드 테스트")
|
||||
void feedArticle(User followee, User follower){
|
||||
User follower2 = User.builder()
|
||||
.bio("follower bio")
|
||||
.email("follower2@email.com")
|
||||
.password("password")
|
||||
.image("follower image")
|
||||
.username("follower2")
|
||||
.build();
|
||||
userRepository.save(followee);
|
||||
userRepository.save(follower);
|
||||
userRepository.save(follower2);
|
||||
|
||||
UserAuth userAuth = UserAuth.builder().id(followee.getId()).username("username").bio("bio").email("email").build();
|
||||
|
||||
Follow follow = Follow.builder().followee(followee).follower(follower).build();
|
||||
Follow follow2 = Follow.builder().followee(followee).follower(follower2).build();
|
||||
|
||||
profileRepository.save(follow);
|
||||
profileRepository.save(follow2);
|
||||
|
||||
List<Follow> follows = profileRepository.findByFolloweeId(userAuth.getId());
|
||||
|
||||
assertThat(follows.get(0).getFollowee().getUsername()).isEqualTo(followee.getUsername());
|
||||
assertThat(follows.get(0).getFollower().getUsername()).isEqualTo(follower.getUsername());
|
||||
|
||||
assertThat(follows.get(1).getFollowee().getUsername()).isEqualTo(followee.getUsername());
|
||||
assertThat(follows.get(1).getFollower().getUsername()).isEqualTo(follower2.getUsername());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Stream<Arguments> getFolloweeAndFollower(){
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.io.realworld.domain.aggregate.user.repository;
|
||||
|
||||
import com.io.realworld.domain.aggregate.article.entity.Article;
|
||||
import com.io.realworld.domain.aggregate.article.repository.ArticleRepository;
|
||||
import com.io.realworld.domain.aggregate.user.entity.User;
|
||||
import com.io.realworld.domain.aggregate.user.repository.UserRepository;
|
||||
import org.hibernate.annotations.Filter;
|
||||
|
||||
@@ -99,17 +99,17 @@ class UserServiceImplTest {
|
||||
.bio("")
|
||||
.email(userSigninRequest.getEmail())
|
||||
.image("")
|
||||
.password(userSigninRequest.getPassword())
|
||||
.password(passwordEncoder.encode(userSigninRequest.getPassword()))
|
||||
.build();
|
||||
|
||||
when(userRepository.findByEmail(any(String.class))).thenReturn(user);
|
||||
|
||||
userService.signin(userSigninRequest);
|
||||
}
|
||||
|
||||
@MethodSource("invalidLoginUsers")
|
||||
@ParameterizedTest(name = "sv:로그인 실패 테스트")
|
||||
void loginFail(UserSigninRequest userSigninRequest){
|
||||
|
||||
when(userRepository.findByEmail(any(String.class))).thenReturn(null);
|
||||
try {
|
||||
userService.signin(userSigninRequest);
|
||||
@@ -119,6 +119,25 @@ class UserServiceImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@MethodSource("invalidLoginUsers")
|
||||
@ParameterizedTest(name="로그인 실패 테스트 : 패스워드 틀림")
|
||||
void loginFailPasswordWrong(UserSigninRequest userSigninRequest){
|
||||
User user = User.builder()
|
||||
.bio("")
|
||||
.email(userSigninRequest.getEmail())
|
||||
.image("")
|
||||
.password(passwordEncoder.encode("wrong password"))
|
||||
.build();
|
||||
|
||||
when(userRepository.findByEmail(any(String.class))).thenReturn(user);
|
||||
try{
|
||||
userService.signin(userSigninRequest);
|
||||
}catch (CustomException e){
|
||||
assertThat(e.getError().equals(Error.PASSWORD_WRONG));
|
||||
assertThat(e.getError().getMessage().equals(Error.PASSWORD_WRONG.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 유저 가져오기 성공 테스트")
|
||||
void currentUserSuccess(){
|
||||
|
||||
@@ -3,7 +3,7 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
spring.jpq.show-sql=true
|
||||
|
||||
logging.level.org.hibernate.type.descriptor.spl=trace
|
||||
#secret
|
||||
|
||||
real-world.token.expiry=3000000
|
||||
|
||||
309
src/vite-frontend/package-lock.json
generated
309
src/vite-frontend/package-lock.json
generated
@@ -16,12 +16,14 @@
|
||||
"path": "^0.12.7",
|
||||
"request": "^2.88.2",
|
||||
"vue": "^3.2.41",
|
||||
"vue-router": "^4.0.13"
|
||||
"vue-router": "^4.0.13",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.2.3",
|
||||
"vue-tsc": "^1.0.9"
|
||||
@@ -510,6 +512,19 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -608,6 +623,15 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -623,6 +647,18 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
@@ -665,6 +701,33 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1506,6 +1569,18 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||
@@ -1648,7 +1723,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
@@ -1818,6 +1893,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -1873,6 +1954,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-callable": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
@@ -1899,7 +1992,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1931,7 +2024,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
@@ -1954,6 +2047,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typed-array": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
|
||||
@@ -2158,6 +2260,15 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -2294,6 +2405,18 @@
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
|
||||
@@ -2382,6 +2505,18 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regexpp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
|
||||
@@ -2525,6 +2660,23 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz",
|
||||
"integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
@@ -2734,6 +2886,18 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
@@ -3037,6 +3201,17 @@
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vuex": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz",
|
||||
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.0.0-beta.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -3488,6 +3663,16 @@
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
@@ -3571,6 +3756,12 @@
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
@@ -3586,6 +3777,15 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
@@ -3616,6 +3816,22 @@
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -4151,6 +4367,15 @@
|
||||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"flat-cache": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
|
||||
@@ -4275,7 +4500,7 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
@@ -4373,6 +4598,12 @@
|
||||
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
|
||||
"dev": true
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@@ -4413,6 +4644,15 @@
|
||||
"has-tostringtag": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
@@ -4430,7 +4670,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
@@ -4450,7 +4690,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
@@ -4464,6 +4704,12 @@
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"devOptional": true
|
||||
},
|
||||
"is-typed-array": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
|
||||
@@ -4635,6 +4881,12 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
@@ -4746,6 +4998,12 @@
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.19",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz",
|
||||
@@ -4803,6 +5061,15 @@
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"regexpp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
|
||||
@@ -4897,6 +5164,17 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.56.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.1.tgz",
|
||||
"integrity": "sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
@@ -5048,6 +5326,15 @@
|
||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
@@ -5246,6 +5533,14 @@
|
||||
"@volar/vue-typescript": "1.0.9"
|
||||
}
|
||||
},
|
||||
"vuex": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz",
|
||||
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.0.0-beta.11"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
"path": "^0.12.7",
|
||||
"request": "^2.88.2",
|
||||
"vue": "^3.2.41",
|
||||
"vue-router": "^4.0.13"
|
||||
"vue-router": "^4.0.13",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.2.3",
|
||||
"vue-tsc": "^1.0.9"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TheHeader></TheHeader>
|
||||
<TheHeader :key="$route.fullPath"></TheHeader>
|
||||
<router-view></router-view>
|
||||
<TheFooter></TheFooter>
|
||||
</template>
|
||||
@@ -7,9 +7,14 @@
|
||||
<script lang="ts">
|
||||
import TheHeader from '@/components/TheHeader.vue'
|
||||
import TheFooter from "@/components/TheFooter.vue";
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
setup(){
|
||||
const route = useRoute();
|
||||
return route
|
||||
},
|
||||
components: {
|
||||
TheHeader,
|
||||
TheFooter
|
||||
@@ -19,12 +24,5 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
232
src/vite-frontend/src/api/index.ts
Normal file
232
src/vite-frontend/src/api/index.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
|
||||
const axiosService = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
})
|
||||
|
||||
|
||||
const signUp = async (user: object): Promise<AxiosResponse> => {
|
||||
return await axiosService.post('/api/users',{user});
|
||||
}
|
||||
|
||||
const signIn = async (user: object): Promise<AxiosResponse> => {
|
||||
return await axiosService.post('/api/users/login',{user})
|
||||
}
|
||||
|
||||
const getCurrentUser = async (): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.get('/api/user', {
|
||||
headers: {
|
||||
Authorization: "TOKEN " + currentToken
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateUser = async (user: object): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.put('/api/user', {user}, {
|
||||
headers: {
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getProfile = async (username: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return axiosService.get('/api/profiles/' + username);
|
||||
}else {
|
||||
return axiosService.get('/api/profiles/' + username,{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const followUser = async (username: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.post('/api/profiles/' + username + "/follow",{},{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unfollowUser = async (username: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.delete('/api/profiles/' + username + "/follow",{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const createArticle = async (article: object | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.post('/api/articles', { article },{
|
||||
headers :{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateArticle = async (article: object | undefined, slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.put('/api/articles/' + slug, { article },{
|
||||
headers :{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteArticle = async (slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.delete('/api/articles/' + slug,{
|
||||
headers :{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const listArticles = async (): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return await axiosService.get('/api/articles?limit=1000&offset=0');
|
||||
}else{
|
||||
return await axiosService.get('/api/articles?limit=1000&offset=0',{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const listArticlesByUsername = async (author: string): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return await axiosService.get('/api/articles?author=' + author);
|
||||
}else{
|
||||
return await axiosService.get('/api/articles?author=' + author,{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const listArticlesByFavorite = async (author: string): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return await axiosService.get('/api/articles?favorited=' + author);
|
||||
}else{
|
||||
return await axiosService.get('/api/articles?favorited=' + author,{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const feedArticle = async (): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.get('/api/articles/feed?limit=1000&offset=0',{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getArticle = async (slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return await axiosService.get('/api/articles/' + slug);
|
||||
}else{
|
||||
return await axiosService.get('/api/articles/' + slug,{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const addCommentToArticle = async (slug: string | undefined, comment: object): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.post('/api/articles/' + slug + '/comments',{
|
||||
comment
|
||||
},{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getCommentsFromArticle = async (slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
if(currentToken == null){
|
||||
return await axiosService.get('/api/articles/' + slug + "/comments");
|
||||
}else {
|
||||
return await axiosService.get('/api/articles/' + slug + "/comments",{
|
||||
headers:{
|
||||
Authorization: "TOKEN " + currentToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteCommentsFromArticle = async (slug: string | undefined, id: number): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.delete('/api/articles/' + slug + '/comments/' + id,{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const favoriteArticle = async (slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.post('/api/articles/' + slug + '/favorite',{},
|
||||
{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const unFavoriteArticle = async (slug: string | undefined): Promise<AxiosResponse> => {
|
||||
let currentToken = localStorage.getItem("token");
|
||||
return await axiosService.delete('/api/articles/' + slug + '/favorite',{
|
||||
headers:{
|
||||
Authorization : "TOKEN " + currentToken,
|
||||
"Content-Type": `application/json`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getTags = async (): Promise<AxiosResponse> => {
|
||||
return await axiosService.get('/api/tags');
|
||||
}
|
||||
|
||||
|
||||
export { signUp, signIn,
|
||||
getCurrentUser, updateUser,
|
||||
getProfile, followUser,
|
||||
createArticle, feedArticle,
|
||||
listArticles, listArticlesByUsername,
|
||||
unfollowUser, getArticle,
|
||||
addCommentToArticle, getCommentsFromArticle,
|
||||
favoriteArticle, unFavoriteArticle,
|
||||
listArticlesByFavorite, updateArticle,
|
||||
deleteArticle, deleteCommentsFromArticle,
|
||||
getTags
|
||||
}
|
||||
109
src/vite-frontend/src/api/usePaginationAPI.ts
Normal file
109
src/vite-frontend/src/api/usePaginationAPI.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ref, Ref } from "@vue/reactivity";
|
||||
import {listArticles, feedArticle, listArticlesByUsername, listArticlesByFavorite} from "@/api/index";
|
||||
|
||||
import { usePagination } from "@/ts/usePagination";
|
||||
|
||||
|
||||
export interface Article {
|
||||
slug: string,
|
||||
title: string,
|
||||
description: string,
|
||||
favorited: boolean,
|
||||
favoritesCount: number,
|
||||
createdAt: string,
|
||||
author: {
|
||||
username: string,
|
||||
image: string
|
||||
}
|
||||
}
|
||||
|
||||
export function usePaginationApi(
|
||||
currentPage: Ref<number>,
|
||||
rowsPerPage?: Ref<number>
|
||||
) {
|
||||
const articleLists: Ref<Article[]> = ref([]);
|
||||
const listsAreLoading = ref(false);
|
||||
const isEmpty = ref(false);
|
||||
|
||||
const { paginatedArray, numberOfPages } = usePagination<Article>({
|
||||
rowsPerPage,
|
||||
arrayToPaginate: articleLists,
|
||||
currentPage
|
||||
});
|
||||
|
||||
const feedLists = async () => {
|
||||
listsAreLoading.value = true;
|
||||
isEmpty.value = false;
|
||||
try{
|
||||
const { data } = await feedArticle();
|
||||
articleLists.value = data.articles;
|
||||
if(data.articlesCount == 0){
|
||||
isEmpty.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
listsAreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const loadLists = async () => {
|
||||
listsAreLoading.value = true;
|
||||
isEmpty.value = false;
|
||||
try {
|
||||
const { data } = await listArticles();
|
||||
articleLists.value = data.articles;
|
||||
if(data.articlesCount == 0){
|
||||
isEmpty.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
listsAreLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMyArticles = async (author: string) => {
|
||||
listsAreLoading.value = true;
|
||||
isEmpty.value = false;
|
||||
try{
|
||||
const { data } = await listArticlesByUsername(author);
|
||||
articleLists.value = data.articles;
|
||||
if(data.articlesCount == 0){
|
||||
isEmpty.value = true;
|
||||
}
|
||||
}catch (err){
|
||||
console.log(err);
|
||||
}finally {
|
||||
listsAreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const loadFavoriteArticles = async (author: string) => {
|
||||
listsAreLoading.value = true;
|
||||
isEmpty.value = false;
|
||||
try{
|
||||
const { data } = await listArticlesByFavorite(author);
|
||||
articleLists.value = data.articles;
|
||||
if(data.articlesCount == 0){
|
||||
isEmpty.value = true;
|
||||
}
|
||||
}catch (err){
|
||||
console.log(err);
|
||||
}finally {
|
||||
listsAreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
articleLists: paginatedArray,
|
||||
loadLists,
|
||||
feedLists,
|
||||
loadMyArticles,
|
||||
loadFavoriteArticles,
|
||||
listsAreLoading,
|
||||
isEmpty,
|
||||
numberOfPages
|
||||
};
|
||||
}
|
||||
624
src/vite-frontend/src/assets/css/font.css
Normal file
624
src/vite-frontend/src/assets/css/font.css
Normal file
@@ -0,0 +1,624 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX4QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX6QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX7QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX1QjU.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX4QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX6QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX7QjX78w.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/merriweathersans/v22/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX1QjU.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidh18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkido18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidg18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidv18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidj18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidi18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18Q.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJT9g.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDI.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdh18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdo18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdg18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdv18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdj18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdi18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18Q.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdh18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdo18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdg18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdv18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdj18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdi18Smxg.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSds18Q.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lqDY.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lqDY.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lqDY.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lqDY.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lqDY.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lqDY.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmhduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwkxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmRduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxduz8A.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRduz8A.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourcesanspro/v21/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXk-oBOL.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXA-oBOL.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXc-oBOL.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXs-oBOL.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXo-oBOL.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIQzD-0qpwxpaWvjeD0X88SAOeauXQ-oA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSGqxLUv.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSiqxLUv.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btS-qxLUv.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSOqxLUv.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSKqxLUv.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/sourceserifpro/v15/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSyqxA.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Titillium Web';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/titilliumweb/v15/NaPDcZTIAOhVxoMyOr9n_E7ffHjDGIVzY4SY.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Titillium Web';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(https://fonts.gstatic.com/s/titilliumweb/v15/NaPDcZTIAOhVxoMyOr9n_E7ffHjDGItzYw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
3
src/vite-frontend/src/assets/css/main.css
Normal file
3
src/vite-frontend/src/assets/css/main.css
Normal file
File diff suppressed because one or more lines are too long
97
src/vite-frontend/src/components/ArticleListFeed.vue
Normal file
97
src/vite-frontend/src/components/ArticleListFeed.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="article-preview">
|
||||
<div class="article-meta">
|
||||
<a href="javascript:(0)" @click="showProfile(article.author.username)"><img :src="article.author.image"/></a>
|
||||
<div class="info">
|
||||
<a class="author"
|
||||
href="javascript:void(0)"
|
||||
@click="showProfile(article.author.username)">{{article.author.username}}</a>
|
||||
<span class="date">{{convertDate(article.createdAt)}}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm pull-xs-right">
|
||||
<i class="ion-heart" @click="changeFavorite(article.slug, article.favorited)"></i> {{article.favoritesCount}}
|
||||
</button>
|
||||
</div>
|
||||
<a href="javascript:(0)"
|
||||
class="preview-link"
|
||||
@click="showArticle(article.slug)">
|
||||
<h1>{{article.title}}</h1>
|
||||
<p>{{article.description}}</p>
|
||||
<span>Read more...</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import convertDate from "@/ts/common";
|
||||
import { favoriteArticle, unFavoriteArticle } from "@/api";
|
||||
import router from "@/router";
|
||||
import {useStore} from "vuex";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ArticleListFeed",
|
||||
props: {
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
slug: "",
|
||||
title: "",
|
||||
description: "",
|
||||
favorited: false,
|
||||
favoritesCount: 0,
|
||||
createdAt: "",
|
||||
author: {
|
||||
username: "",
|
||||
image: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setup(props){
|
||||
const store = useStore();
|
||||
const token = store.state.token;
|
||||
|
||||
const showProfile = (username: string) => {
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {username: username}
|
||||
})
|
||||
}
|
||||
|
||||
const showArticle = (slug: string) =>{
|
||||
router.push({
|
||||
name: 'ArticleDetail',
|
||||
params: {slug: slug}
|
||||
})
|
||||
}
|
||||
|
||||
const changeFavorite = async (slug: string, favorite : boolean) => {
|
||||
if(token == ''){
|
||||
await router.push({name:"Login"});
|
||||
return;
|
||||
}else{
|
||||
if(favorite){
|
||||
const { data } = await unFavoriteArticle(slug);
|
||||
props.article.favoritesCount = data.article.favoritesCount;
|
||||
props.article.favorited = data.article.favorited;
|
||||
}else{
|
||||
const { data } = await favoriteArticle(slug);
|
||||
props.article.favoritesCount = data.article.favoritesCount;
|
||||
props.article.favorited = data.article.favorited;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { convertDate, changeFavorite, showProfile, showArticle }
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
96
src/vite-frontend/src/components/ArticleListGlobal.vue
Normal file
96
src/vite-frontend/src/components/ArticleListGlobal.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="article-preview">
|
||||
<div class="article-meta">
|
||||
<a href="javascript:(0)" @click="showProfile(article.author.username)"><img :src="article.author.image"/></a>
|
||||
<div class="info">
|
||||
<a class="author"
|
||||
href="javascript:void(0)"
|
||||
@click="showProfile(article.author.username)">{{article.author.username}}</a>
|
||||
<span class="date">{{convertDate(article.createdAt)}}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm pull-xs-right">
|
||||
<i class="ion-heart" @click="changeFavorite(article.slug, article.favorited)"></i> {{article.favoritesCount}}
|
||||
</button>
|
||||
</div>
|
||||
<a href="javascript:(0)"
|
||||
class="preview-link"
|
||||
@click="showArticle(article.slug)">
|
||||
<h1>{{article.title}}</h1>
|
||||
<p>{{article.description}}</p>
|
||||
<span>Read more...</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import router from "@/router";
|
||||
import convertDate from '@/ts/common';
|
||||
import { useStore } from "vuex";
|
||||
import { favoriteArticle, unFavoriteArticle } from "@/api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ArticleListGlobal",
|
||||
props:{
|
||||
index: Number,
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
slug: "",
|
||||
title: "",
|
||||
description: "",
|
||||
favorited: false,
|
||||
favoritesCount: 0,
|
||||
createdAt: "",
|
||||
author: {
|
||||
username: "",
|
||||
image: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const token = store.state.token;
|
||||
|
||||
const showProfile = (username: string) => {
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {username: username}
|
||||
})
|
||||
}
|
||||
|
||||
const showArticle = (slug: string) =>{
|
||||
router.push({
|
||||
name: 'ArticleDetail',
|
||||
params: {slug: slug}
|
||||
})
|
||||
}
|
||||
|
||||
const changeFavorite = async (slug: string, favorite : boolean) => {
|
||||
if(token == ''){
|
||||
await router.push({name:"Login"});
|
||||
return;
|
||||
}else{
|
||||
if(favorite){
|
||||
const { data } = await unFavoriteArticle(slug);
|
||||
props.article.favoritesCount = data.article.favoritesCount;
|
||||
props.article.favorited = data.article.favorited;
|
||||
}else{
|
||||
const { data } = await favoriteArticle(slug);
|
||||
props.article.favoritesCount = data.article.favoritesCount;
|
||||
props.article.favorited = data.article.favorited;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { convertDate, changeFavorite, showProfile, showArticle }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
73
src/vite-frontend/src/components/ArticleMy.vue
Normal file
73
src/vite-frontend/src/components/ArticleMy.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="article-preview">
|
||||
<div class="article-meta">
|
||||
<a href="javascript:void(0)" ><img :src="article.author.image"/></a>
|
||||
<div class="info">
|
||||
<a href="javascript:void(0)" class="author">{{article.author.username}}</a>
|
||||
<span class="date">{{convertDate(article.createdAt)}}</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm pull-xs-right">
|
||||
<i class="ion-heart"></i> {{article.favoritesCount}}
|
||||
</button>
|
||||
</div>
|
||||
<a href="javascript:(0)" class="preview-link"
|
||||
@click="showArticle(article.slug)">
|
||||
<h1>{{article.title}}</h1>
|
||||
<p>{{article.description}}</p>
|
||||
<span>Read more...</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import router from "@/router";
|
||||
import convertDate from "@/ts/common";
|
||||
export default defineComponent({
|
||||
name: "ArticleMy",
|
||||
props:{
|
||||
article: {
|
||||
type: Object,
|
||||
default: () =>{
|
||||
return {
|
||||
slug: "",
|
||||
title: "",
|
||||
description: "",
|
||||
body: "",
|
||||
tagList: new Array(),
|
||||
createdAt: "",
|
||||
favorited: false,
|
||||
favoritesCount: 0,
|
||||
author:{
|
||||
username:"",
|
||||
bio: "",
|
||||
image: "",
|
||||
following: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setup(props){
|
||||
const showProfile = (username: string) => {
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {username: username}
|
||||
})
|
||||
}
|
||||
|
||||
const showArticle = (slug: string) =>{
|
||||
router.push({
|
||||
name: 'ArticleDetail',
|
||||
params: {slug: slug}
|
||||
})
|
||||
}
|
||||
|
||||
return { showProfile, convertDate, showArticle }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
136
src/vite-frontend/src/components/PaginationComponent.vue
Normal file
136
src/vite-frontend/src/components/PaginationComponent.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="pagination-container" aria-label="row pagination">
|
||||
<ul v-if="numberOfPages >= 1" class="pagination">
|
||||
<li
|
||||
class="page-item"
|
||||
aria-label="go to previous page"
|
||||
@click="previous()"
|
||||
:class="{
|
||||
disabled: currentPage === 1,
|
||||
}"
|
||||
>
|
||||
<span class="page-link">«</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="index in numberOfPages"
|
||||
:key="index"
|
||||
:aria-label="'go to page ' + index"
|
||||
class="page-item"
|
||||
@click="setCurrentPage(index)"
|
||||
>
|
||||
<div
|
||||
class="page-link"
|
||||
:class="{
|
||||
'active-page': currentPage === index,
|
||||
}"
|
||||
>
|
||||
{{ index }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
class="page-item"
|
||||
:class="{
|
||||
disabled: currentPage === numberOfPages || !numberOfPages,
|
||||
}"
|
||||
aria-label="go to next page"
|
||||
@click="next()"
|
||||
>
|
||||
<div class="page-link">»</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { toRefs } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
numberOfPages: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
modelValue: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
const { numberOfPages, modelValue: currentPage } = toRefs(props);
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const setCurrentPage = (number: Number) => {
|
||||
emit("update:modelValue", number);
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (currentPage.value === 1) return;
|
||||
emit("update:modelValue", currentPage.value - 1);
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (currentPage.value >= numberOfPages.value) return;
|
||||
emit("update:modelValue", currentPage.value + 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pagination-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
background: white;
|
||||
margin: 0px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
align-content: center;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0px;
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
.page-link {
|
||||
color: #666B85;
|
||||
border-radius: 5px;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
&:hover {
|
||||
color: #333333;
|
||||
background-color: #e9e9e9;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.active-page {
|
||||
background-color: #60d394 !important;
|
||||
color: white !important;
|
||||
&:hover {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
.page-link {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
36
src/vite-frontend/src/components/TagList.vue
Normal file
36
src/vite-frontend/src/components/TagList.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="tag-list">
|
||||
<a href="javascript:(0)" class="tag-pill tag-default"
|
||||
v-for = "tag in listTags.tags">{{tag}}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {onMounted, defineComponent, reactive} from "vue";
|
||||
import axios from "axios";
|
||||
import {getTags} from "@/api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TagList",
|
||||
setup(){
|
||||
const listTags = reactive({
|
||||
tags: new Array()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try{
|
||||
const { data } = await getTags();
|
||||
listTags.tags = data.tags;
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
})
|
||||
|
||||
return { listTags }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -2,13 +2,12 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Conduit</title>
|
||||
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
|
||||
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
|
||||
rel="stylesheet" type="text/css">
|
||||
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
|
||||
<link rel="stylesheet" href="//demo.productionready.io/main.css">
|
||||
<title>KMS-real-world</title>
|
||||
<link
|
||||
href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<nav class="navbar navbar-light">
|
||||
@@ -17,19 +16,29 @@
|
||||
<ul class="nav navbar-nav pull-xs-right">
|
||||
<li class="nav-item">
|
||||
<!-- Add "active" class when you're on that page" -->
|
||||
<router-link to="/" class="nav-link active" active-class="active">Home</router-link>
|
||||
<router-link to="/" class="nav-link"
|
||||
@click="selected(home)"
|
||||
:class="{ active : home}" active-class="active">Home</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/article" class="nav-link" active-class="active">New Article</router-link>
|
||||
<li class="nav-item" v-if="isLogin">
|
||||
<router-link to="/editor/" class="nav-link"
|
||||
@click="selected(article)"
|
||||
:class="{ active : article}" active-class="active">New Article</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/settings" class="nav-link" active-class="active"><i class="ion-gear-a"></i>Settings</router-link>
|
||||
<li class="nav-item" v-if="isLogin">
|
||||
<router-link to="/settings" class="nav-link"
|
||||
@click="selected(setting)"
|
||||
:class="{ active : setting}" active-class="active"><i class="ion-gear-a"></i>Settings</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/login" class="nav-link" active-class="active">Sign in</router-link>
|
||||
<li class="nav-item" v-if="!isLogin">
|
||||
<router-link to="/login" class="nav-link"
|
||||
@click="selected(signIn)"
|
||||
:class="{ active : signIn}" active-class="active">Sign in</router-link>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<router-link to="/register" class="nav-link" active-class="active">Sign up</router-link>
|
||||
<li class="nav-item" v-if="!isLogin">
|
||||
<router-link to="/register" class="nav-link"
|
||||
@click="selected(signUp)"
|
||||
:class="{ active : signUp}" active-class="active">Sign up</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -39,8 +48,43 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "TheHeader"
|
||||
name: "TheHeader",
|
||||
setup(){
|
||||
const store = useStore();
|
||||
const isLogin = ref(false);
|
||||
const home = ref(false);
|
||||
const article = ref(false);
|
||||
const setting = ref(false);
|
||||
const signIn = ref(false);
|
||||
const signUp = ref(false);
|
||||
const allHide = () => {
|
||||
home.value = false;
|
||||
article.value = false;
|
||||
setting.value = false;
|
||||
signUp.value = false;
|
||||
signIn.value = false;
|
||||
}
|
||||
|
||||
const selected = (headerName : boolean) => {
|
||||
allHide();
|
||||
headerName = true;
|
||||
}
|
||||
|
||||
onMounted(()=> {
|
||||
if(store.state.token == ""){
|
||||
isLogin.value = false;
|
||||
}else{
|
||||
isLogin.value = true;
|
||||
}
|
||||
})
|
||||
|
||||
return { isLogin, home, article, setting, signIn, signUp, allHide, selected }
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
74
src/vite-frontend/src/components/commentList.vue
Normal file
74
src/vite-frontend/src/components/commentList.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-block">
|
||||
<p class="card-text">{{comment.body}}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="javascript:(0)" class="comment-author" @click="viewProfile">
|
||||
<img :src="comment.author.image" class="comment-author-img"/>
|
||||
</a>
|
||||
<a href="javascript:(0)" class="comment-author" @click="viewProfile">{{comment.author.username}}</a>
|
||||
<span class="date-posted">{{convertDate(comment.updatedAt)}}</span>
|
||||
<span v-if="isMe" class="mod-options" @click="deleteComment">
|
||||
<i class="ion-trash-a"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, defineEmits, onMounted, ref,} from "vue";
|
||||
import convertDate from "@/ts/common";
|
||||
import router from "@/router";
|
||||
|
||||
import {useStore} from "vuex";
|
||||
export default defineComponent({
|
||||
name: "commentList",
|
||||
props:{
|
||||
comment: {
|
||||
type: Object,
|
||||
default: () =>{
|
||||
return {
|
||||
id: 0,
|
||||
body: "",
|
||||
updatedAt: "",
|
||||
author:{
|
||||
username:"",
|
||||
image: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setup(props, {emit}){
|
||||
const store = useStore();
|
||||
|
||||
const isMe = ref(false);
|
||||
|
||||
const viewProfile = () => {
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {username: props.comment.author.username}})
|
||||
}
|
||||
|
||||
const deleteComment = () => {
|
||||
emit('delete:comment', props.comment.id);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if(store.state.username == props.comment.author.username){
|
||||
isMe.value = true;
|
||||
}else{
|
||||
isMe.value = false;
|
||||
}
|
||||
})
|
||||
|
||||
return { isMe, convertDate, viewProfile, deleteComment}
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import '@/assets/css/font.css'
|
||||
import '@/assets/css/main.css'
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.mount('#app');
|
||||
app.config.performance=true;
|
||||
app.use(router).use(store).mount('#app');
|
||||
@@ -23,10 +23,28 @@ const routes = [
|
||||
component: () => import(/* webpackChunkName "inputTag" */ '@/views/TheRegister.vue')
|
||||
},
|
||||
{
|
||||
path: "/article",
|
||||
name: "Article",
|
||||
path: "/article/:slug",
|
||||
name: "ArticleDetail",
|
||||
component: () => import('@/views/TheArticleDetail.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/editor/",
|
||||
name: "ArticleEditor",
|
||||
component: () => import(/* webpackChunkName "inputTag" */ '@/views/TheArticle.vue')
|
||||
},
|
||||
{
|
||||
path: "/editor/:slug",
|
||||
name: "ArticleUpdateEditor",
|
||||
component: () => import(/* webpackChunkName "inputTag" */ '@/views/ArticleUpdate.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: "/@:username",
|
||||
name: "Profile",
|
||||
component: () => import(/* webpackChunkName "inputTag" */ '@/views/TheProfile.vue'),
|
||||
props: true
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
31
src/vite-frontend/src/store/index.ts
Normal file
31
src/vite-frontend/src/store/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createStore } from 'vuex';
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
token: localStorage.getItem("token") || '',
|
||||
username: localStorage.getItem("username") || '',
|
||||
},
|
||||
mutations: {
|
||||
setUsername(state, username){
|
||||
state.username = username;
|
||||
},
|
||||
setToken(state, token){
|
||||
state.token = token;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
LOGIN({commit}, user){
|
||||
commit("setUsername", user.username);
|
||||
commit("setToken", user.token);
|
||||
localStorage.setItem("username", user.username);
|
||||
localStorage.setItem("token", user.token);
|
||||
},
|
||||
LOGOUT({commit}){
|
||||
commit("setUsername","");
|
||||
commit("setToken","");
|
||||
localStorage.removeItem("username");
|
||||
localStorage.removeItem("token");
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
5
src/vite-frontend/src/ts/common.ts
Normal file
5
src/vite-frontend/src/ts/common.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
function convertDate(date: string): string {
|
||||
return new Date(date).toLocaleDateString("en-US", { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
|
||||
export default convertDate;
|
||||
26
src/vite-frontend/src/ts/usePagination.ts
Normal file
26
src/vite-frontend/src/ts/usePagination.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {computed, Ref, ref} from "vue";
|
||||
|
||||
interface PaginationConfig<T> {
|
||||
rowsPerPage?: Ref<number>;
|
||||
arrayToPaginate: Ref<T[]>;
|
||||
currentPage: Ref<number>;
|
||||
}
|
||||
|
||||
export function usePagination<T>(config: PaginationConfig<T>) {
|
||||
const rowsPerPage = config.rowsPerPage || ref(20);
|
||||
|
||||
const paginatedArray = computed(() =>
|
||||
config.arrayToPaginate.value.slice(
|
||||
(config.currentPage.value - 1) * rowsPerPage.value,
|
||||
config.currentPage.value * rowsPerPage.value
|
||||
)
|
||||
);
|
||||
const numberOfPages = computed(() => {
|
||||
return Math.ceil((config.arrayToPaginate.value.length || 0) / rowsPerPage.value)
|
||||
}
|
||||
);
|
||||
return {
|
||||
paginatedArray,
|
||||
numberOfPages
|
||||
};
|
||||
}
|
||||
70
src/vite-frontend/src/views/ArticleUpdate.vue
Normal file
70
src/vite-frontend/src/views/ArticleUpdate.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="editor-page">
|
||||
<div class="container page">
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1 col-xs-12">
|
||||
<form>
|
||||
<fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="Article Title" v-model="article.title">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control" placeholder="What's this article about?" v-model="article.description">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<textarea class="form-control" rows="8"
|
||||
placeholder="Write your article (in markdown)" v-model="article.body"></textarea>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Enter tags using ',' Add tag">
|
||||
<div class="tag-list"></div>
|
||||
</fieldset>
|
||||
<button @click="updateContent" class="btn btn-lg pull-xs-right btn-primary" type="button">
|
||||
Publish Article
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, defineComponent } from "vue";
|
||||
import { updateArticle } from "@/api/index.js";
|
||||
import router from "@/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ArticleUpdate",
|
||||
props:{
|
||||
slug: String,
|
||||
},
|
||||
setup(props){
|
||||
const article = reactive({
|
||||
title: "",
|
||||
description: "",
|
||||
body: "",
|
||||
})
|
||||
|
||||
|
||||
const updateContent = async () => {
|
||||
try{
|
||||
const { data } = await updateArticle(article, props.slug);
|
||||
await router.push({
|
||||
name:"ArticleDetail",
|
||||
params: {slug: data.article.slug}
|
||||
})
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
return { article, updateContent }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,33 +1,30 @@
|
||||
<template>
|
||||
|
||||
<div class="editor-page">
|
||||
<div class="container page">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-10 offset-md-1 col-xs-12">
|
||||
<form>
|
||||
<fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="Article Title">
|
||||
<input type="text" class="form-control form-control-lg" placeholder="Article Title" v-model="article.title">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control" placeholder="What's this article about?">
|
||||
<input type="text" class="form-control" placeholder="What's this article about?" v-model="article.description">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<textarea class="form-control" rows="8"
|
||||
placeholder="Write your article (in markdown)"></textarea>
|
||||
placeholder="Write your article (in markdown)" v-model="article.body"></textarea>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Enter tags">
|
||||
<input type="text" class="form-control" placeholder="Enter tags using ',' Add tag" v-model="tag">
|
||||
<div class="tag-list"></div>
|
||||
</fieldset>
|
||||
<button class="btn btn-lg pull-xs-right btn-primary" type="button">
|
||||
<button @click="addArticle" class="btn btn-lg pull-xs-right btn-primary" type="button">
|
||||
Publish Article
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,8 +33,46 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {reactive ,ref } from "vue";
|
||||
import router from "@/router";
|
||||
import {createArticle} from "@/api";
|
||||
|
||||
export default {
|
||||
name: "TheArticle"
|
||||
name: "TheArticle",
|
||||
setup(){
|
||||
const tag = ref("");
|
||||
const article = reactive({
|
||||
title: "",
|
||||
description: "",
|
||||
body: "",
|
||||
tagList: new Array(),
|
||||
})
|
||||
|
||||
const parsingTag = () => {
|
||||
let tags: string = tag.value;
|
||||
return tags.split(',');
|
||||
}
|
||||
|
||||
const getSlug = (title:string) => {
|
||||
return title.replace(' ','-');
|
||||
}
|
||||
|
||||
const addArticle = async () => {
|
||||
const tags = parsingTag();
|
||||
const slug = getSlug(article.title);
|
||||
article.tagList = tags;
|
||||
try{
|
||||
await createArticle(article);
|
||||
await router.push({
|
||||
name:"ArticleDetail",
|
||||
params: {slug}
|
||||
})
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
return { article, tag, addArticle, parsingTag, getSlug }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
283
src/vite-frontend/src/views/TheArticleDetail.vue
Normal file
283
src/vite-frontend/src/views/TheArticleDetail.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
|
||||
<div class="article-page">
|
||||
|
||||
<div class="banner">
|
||||
<div class="container">
|
||||
<h1>{{ articleDetail.article.title }}</h1>
|
||||
|
||||
<div class="article-meta">
|
||||
<a href=""><img :src="articleDetail.article.author.image"/></a>
|
||||
<div class="info">
|
||||
<a href="javascript:void(0)" class="author" @click="viewProfile">{{ articleDetail.article.author.username }}</a>
|
||||
<span class="date">{{convertDate(articleDetail.article.createdAt)}}</span>
|
||||
</div>
|
||||
<button v-if= "isMe" class="btn btn-sm btn-outline-secondary" @click="articleUpdate()">
|
||||
<i class="ion-edit"></i>
|
||||
Edit Article
|
||||
</button>
|
||||
<button v-if= "isMe" class="btn btn-outline-danger btn-sm" @click="articleDelete()">
|
||||
<i class="ion-trash-a"></i>
|
||||
Delete Article
|
||||
</button>
|
||||
<button v-if= "!isMe" class="btn btn-sm btn-outline-secondary" @click="followUpdate(articleDetail.article.author.following)">
|
||||
<div v-if="articleDetail.article.author.following">
|
||||
<i class="ion-minus-round"></i>
|
||||
unFollow {{articleDetail.article.author.username}}
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="ion-plus-round"></i>
|
||||
Follow {{articleDetail.article.author.username}}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button v-if="!isMe" class="btn btn-sm btn-outline-primary" @click="favoriteUpdate(articleDetail.article.favorited)">
|
||||
<div v-if="articleDetail.article.favorited">
|
||||
<i class="ion-heart"></i>
|
||||
unFavorite Article (<span class="counter">{{articleDetail.article.favoritesCount}}</span>)
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="ion-heart"></i>
|
||||
Favorite Article (<span class="counter">{{articleDetail.article.favoritesCount}}</span>)
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container page">
|
||||
|
||||
<div class="row article-content">
|
||||
<div class="col-md-12">
|
||||
{{articleDetail.article.body}}
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<div class="article-actions">
|
||||
<div class="article-meta">
|
||||
<a href="javascript:void(0)"><img :src="articleDetail.article.author.image"/></a>
|
||||
<div class="info">
|
||||
<a href="javascript:void(0)" class="author" @click="viewProfile">{{ articleDetail.article.author.username }}</a>
|
||||
<span class="date">{{convertDate(articleDetail.article.createdAt)}}</span>
|
||||
</div>
|
||||
<button v-if= "isMe" class="btn btn-sm btn-outline-secondary" @click="articleUpdate()">
|
||||
<i class="ion-edit"></i>
|
||||
Edit Article
|
||||
</button>
|
||||
<button v-if= "isMe" class="btn btn-outline-danger btn-sm" @click="articleDelete()">
|
||||
<i class="ion-trash-a"></i>
|
||||
Delete Article
|
||||
</button>
|
||||
<button v-if= "!isMe" class="btn btn-sm btn-outline-secondary" @click="followUpdate(articleDetail.article.author.following)">
|
||||
<div v-if="articleDetail.article.author.following">
|
||||
<i class="ion-minus-round"></i>
|
||||
unFollow {{articleDetail.article.author.username}}
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="ion-plus-round"></i>
|
||||
Follow {{articleDetail.article.author.username}}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button v-if="!isMe" class="btn btn-sm btn-outline-primary" @click="favoriteUpdate(articleDetail.article.favorited)">
|
||||
<div v-if="articleDetail.article.favorited">
|
||||
<i class="ion-heart"></i>
|
||||
unFavorite Article (<span class="counter">{{articleDetail.article.favoritesCount}}</span>)
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="ion-heart"></i>
|
||||
Favorite Article (<span class="counter">{{articleDetail.article.favoritesCount}}</span>)
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
|
||||
<form class="card comment-form">
|
||||
<div class="card-block">
|
||||
<textarea class="form-control" placeholder="Write a comment..." rows="3" v-model="comment.body"></textarea>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<img :src="articleDetail.article.author.image" class="comment-author-img"/>
|
||||
<button class="btn btn-sm btn-primary" @click="sendComment()">
|
||||
Post Comment
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<comment-list v-for="(comment,index) in getCommentList.comment"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
@delete:comment="deleteComment"
|
||||
:imgs="comment.author.image">
|
||||
</comment-list>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {onMounted, defineComponent, reactive, ref} from "vue";
|
||||
import commentList from "@/components/commentList.vue";
|
||||
import router from "@/router";
|
||||
import { useStore } from "vuex";
|
||||
import convertDate from "@/ts/common";
|
||||
import {
|
||||
addCommentToArticle, deleteArticle, deleteCommentsFromArticle,
|
||||
favoriteArticle,
|
||||
followUser,
|
||||
getArticle,
|
||||
getCommentsFromArticle, unFavoriteArticle,
|
||||
unfollowUser
|
||||
} from "@/api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TheArticleDetail.vue",
|
||||
components:{
|
||||
'comment-list': commentList,
|
||||
},
|
||||
props:{
|
||||
slug: String,
|
||||
},
|
||||
setup(props){
|
||||
const store = useStore();
|
||||
const token = store.state.token;
|
||||
const username = store.state.username;
|
||||
const isMe = ref(false);
|
||||
|
||||
const comment = reactive({
|
||||
body: ""
|
||||
})
|
||||
|
||||
const getCommentList = reactive({
|
||||
comment: reactive([{id:0,author:{username:"",image:""}}])
|
||||
})
|
||||
|
||||
const articleDetail = reactive({
|
||||
article: {
|
||||
slug: "",
|
||||
title: "",
|
||||
description: "",
|
||||
body: "",
|
||||
tagList: new Array(),
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
favorited: false,
|
||||
favoritesCount: 0,
|
||||
author: {
|
||||
username: "",
|
||||
bio: "",
|
||||
image: "",
|
||||
following: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const viewProfile = () => {
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {username: articleDetail.article.author.username}})
|
||||
}
|
||||
|
||||
const articleUpdate = async () => {
|
||||
await router.push({
|
||||
name: 'ArticleUpdateEditor',
|
||||
params: {slug: articleDetail.article.slug}
|
||||
})
|
||||
}
|
||||
|
||||
const articleDelete = async () => {
|
||||
await deleteArticle(articleDetail.article.slug);
|
||||
await router.push({
|
||||
name: 'Home'
|
||||
})
|
||||
}
|
||||
|
||||
const followUpdate = async (followState: boolean) => {
|
||||
if(token == ''){
|
||||
await router.push({name:"Login"});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if(followState){
|
||||
const { data } = await unfollowUser(articleDetail.article.author.username);
|
||||
articleDetail.article.author.following = data.profile.following;
|
||||
|
||||
}else{
|
||||
const { data } = await followUser(articleDetail.article.author.username);
|
||||
articleDetail.article.author.following = data.profile.following;
|
||||
}
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
const favoriteUpdate = async (favoriteState: boolean) => {
|
||||
if(token == ''){
|
||||
await router.push({name:"Login"});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (favoriteState) {
|
||||
const {data} = await unFavoriteArticle(props.slug);
|
||||
articleDetail.article = data.article;
|
||||
|
||||
} else {
|
||||
const {data} = await favoriteArticle(props.slug);
|
||||
articleDetail.article = data.article;
|
||||
}
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
const sendComment = async () => {
|
||||
try{
|
||||
const { data } = await addCommentToArticle(props.slug, comment);
|
||||
comment.body = "";
|
||||
getCommentList.comment.push(data.comment);
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteComment = async (commentId: number) => {
|
||||
await deleteCommentsFromArticle(articleDetail.article.slug,commentId);
|
||||
getCommentList.comment.splice(commentId,1);
|
||||
}
|
||||
|
||||
onMounted(async ()=>{
|
||||
try{
|
||||
const { data } = await getArticle(props.slug);
|
||||
articleDetail.article = data.article;
|
||||
if(articleDetail.article.author.username == username){
|
||||
isMe.value = true;
|
||||
}
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
|
||||
try{
|
||||
const { data } = await getCommentsFromArticle(props.slug);
|
||||
getCommentList.comment = data.comments;
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
})
|
||||
|
||||
return { isMe, articleDetail, comment, getCommentList, convertDate, deleteComment, viewProfile, articleUpdate, followUpdate, favoriteUpdate, sendComment, articleDelete }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -13,84 +13,107 @@
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-9">
|
||||
<div class="feed-toggle">
|
||||
<ul class="nav nav-pills outline-active">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="">Your Feed</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="">Global Feed</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="article-preview">
|
||||
<div class="article-meta">
|
||||
<a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg"/></a>
|
||||
<div class="info">
|
||||
<a href="" class="author">Eric Simons</a>
|
||||
<span class="date">January 20th</span>
|
||||
<div class="feed-toggle">
|
||||
<ul class="nav nav-pills outline-active">
|
||||
<li class="nav-item" v-if="isLogin">
|
||||
<a href="javascript:(0)" class="nav-link"
|
||||
@click="feedSelect"
|
||||
:class="{ active : feedActive}">Your Feed</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="javascript:(0)" class="nav-link"
|
||||
@click="globalSelect"
|
||||
:class="{ active : globalActive }">Global Feed</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="listsAreLoading">
|
||||
Loading articles...
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm pull-xs-right">
|
||||
<i class="ion-heart"></i> 29
|
||||
</button>
|
||||
</div>
|
||||
<a href="" class="preview-link">
|
||||
<h1>How to build webapps that scale</h1>
|
||||
<p>This is the description for the post.</p>
|
||||
<span>Read more...</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="article-preview">
|
||||
<div class="article-meta">
|
||||
<a href="profile.html"><img src="http://i.imgur.com/N4VcUeJ.jpg"/></a>
|
||||
<div class="info">
|
||||
<a href="" class="author">Albert Pai</a>
|
||||
<span class="date">January 20th</span>
|
||||
<div v-if="isEmpty">
|
||||
No articles are here... yet.
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm pull-xs-right">
|
||||
<i class="ion-heart"></i> 32
|
||||
</button>
|
||||
</div>
|
||||
<a href="" class="preview-link">
|
||||
<h1>The song you won't ever stop singing. No matter how hard you try.</h1>
|
||||
<p>This is the description for the post.</p>
|
||||
<span>Read more...</span>
|
||||
</a>
|
||||
<div v-if="feedActive && isLogin">
|
||||
<article-list-feed v-for="(article,index) in articleLists"
|
||||
:key="article.slug"
|
||||
:article="article">
|
||||
</article-list-feed>
|
||||
</div>
|
||||
<div v-else>
|
||||
<article-list-global v-for="(article,index) in articleLists"
|
||||
:key="article.slug"
|
||||
:article="article">
|
||||
</article-list-global>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="sidebar">
|
||||
<p>Popular Tags</p>
|
||||
|
||||
<div class="tag-list">
|
||||
<a href="" class="tag-pill tag-default">programming</a>
|
||||
<a href="" class="tag-pill tag-default">javascript</a>
|
||||
<a href="" class="tag-pill tag-default">emberjs</a>
|
||||
<a href="" class="tag-pill tag-default">angularjs</a>
|
||||
<a href="" class="tag-pill tag-default">react</a>
|
||||
<a href="" class="tag-pill tag-default">mean</a>
|
||||
<a href="" class="tag-pill tag-default">node</a>
|
||||
<a href="" class="tag-pill tag-default">rails</a>
|
||||
</div>
|
||||
<tag-lists></tag-lists>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<pagination-component v-model="currentPage" :numberOfPages="numberOfPages"></pagination-component>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import articleList from '@/components/ArticleListFeed.vue'
|
||||
import articleListGlobal from "@/components/ArticleListGlobal.vue";
|
||||
import tagLists from "@/components/TagList.vue";
|
||||
import pagination from "@/components/PaginationComponent.vue";
|
||||
import { usePaginationApi } from "@/api/usePaginationAPI"
|
||||
import {onMounted, ref} from "vue";
|
||||
import { useStore } from "vuex";
|
||||
export default {
|
||||
name: "TheHome"
|
||||
name: "TheHome",
|
||||
components: {
|
||||
'article-list-feed': articleList,
|
||||
'article-list-global': articleListGlobal,
|
||||
'tag-lists': tagLists,
|
||||
'pagination-component': pagination,
|
||||
},
|
||||
setup(){
|
||||
const store = useStore();
|
||||
const isLogin = ref(false);
|
||||
const feedActive = ref(true);
|
||||
const globalActive = ref(false);
|
||||
|
||||
const currentPage = ref(1);
|
||||
const rowsPerPage = ref(20);
|
||||
|
||||
const { articleLists, listsAreLoading, isEmpty, loadLists, feedLists, numberOfPages } = usePaginationApi(currentPage, rowsPerPage);
|
||||
|
||||
const feedSelect = async () => {
|
||||
feedActive.value=true;
|
||||
globalActive.value=false;
|
||||
await feedLists();
|
||||
}
|
||||
|
||||
const globalSelect = async () => {
|
||||
feedActive.value=false;
|
||||
globalActive.value=true;
|
||||
await loadLists();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isLogin.value = store.state.token ? true : false;
|
||||
if(isLogin.value == false) {
|
||||
await loadLists();
|
||||
globalActive.value = true;
|
||||
}else{
|
||||
await feedLists();
|
||||
feedActive.value = true;
|
||||
}
|
||||
})
|
||||
|
||||
return { listsAreLoading, isEmpty, isLogin, currentPage, rowsPerPage, numberOfPages, feedActive, globalActive, articleLists, feedSelect, globalSelect };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="password" placeholder="Password" v-model="user.password">
|
||||
</fieldset>
|
||||
<button @click = "signin" class="btn btn-lg btn-primary pull-xs-right">
|
||||
<button @click = "Login" class="btn btn-lg btn-primary pull-xs-right">
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
@@ -33,9 +33,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {reactive, ref} from "vue";
|
||||
import axios from "axios";
|
||||
import { reactive, ref } from "vue";
|
||||
import { signIn } from "@/api";
|
||||
import router from "@/router";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "TheLogin",
|
||||
@@ -46,26 +47,25 @@ export default {
|
||||
password: "",
|
||||
})
|
||||
|
||||
const store = useStore();
|
||||
|
||||
let loginValidation = ref(false);
|
||||
|
||||
const signin = () => {
|
||||
const url = import.meta.env.VITE_BASE_URL;
|
||||
axios.post(url+'/api/users/login',{
|
||||
user
|
||||
})
|
||||
.then(response => {
|
||||
window.localStorage.setItem("token",response.data.user.token);
|
||||
router.push("/");
|
||||
})
|
||||
.catch(error =>{
|
||||
console.log(error);
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "EMAIL_NULL_OR_INVALID"){
|
||||
loginValidation.value = true;
|
||||
}
|
||||
})
|
||||
const Login = async () => {
|
||||
try{
|
||||
const { data } = await signIn(user);
|
||||
await store.dispatch("LOGIN",data.user);
|
||||
await router.push({name:"Home"});
|
||||
}catch(error: any){
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "EMAIL_NULL_OR_INVALID"){
|
||||
loginValidation.value = true;
|
||||
}else if(code == "PASSWORD_WRONG"){
|
||||
loginValidation.value = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
return {user, loginValidation, signin};
|
||||
return {user, loginValidation, Login};
|
||||
}
|
||||
|
||||
|
||||
|
||||
175
src/vite-frontend/src/views/TheProfile.vue
Normal file
175
src/vite-frontend/src/views/TheProfile.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<div class="user-info">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-10 offset-md-1">
|
||||
<img :src="profile.image" class="user-img"/>
|
||||
<h4>{{ profile.username }}</h4>
|
||||
<p>
|
||||
{{ profile.bio }}
|
||||
</p>
|
||||
<button class="btn btn-sm btn-outline-secondary action-btn" @click="stateUpdate">
|
||||
<div v-if="isMe">
|
||||
<i class="ion-gear-a"></i>
|
||||
Edit Profile Settings
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="profile.following">
|
||||
<i class="ion-minus-round"></i>
|
||||
unFollow {{profile.username}}
|
||||
</div>
|
||||
<div v-else>
|
||||
<i class="ion-plus-round"></i>
|
||||
Follow {{profile.username}}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-xs-12 col-md-10 offset-md-1">
|
||||
<div class="articles-toggle">
|
||||
<ul class="nav nav-pills outline-active">
|
||||
<li class="nav-item">
|
||||
<a href="javascript:(0)" class="nav-link"
|
||||
@click="myArticleSelect"
|
||||
:class="{ active : myArticleActive}">My Articles</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="javascript:(0)" class="nav-link"
|
||||
@click="favoriteArticleSelect"
|
||||
:class="{ active : favoriteArticleActive}">Favorited Articles</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="listsAreLoading">
|
||||
Loading articles...
|
||||
</div>
|
||||
<div v-if="isEmpty">
|
||||
No articles are here... yet.
|
||||
</div>
|
||||
</div>
|
||||
<article-my v-for="(article,index) in articleLists"
|
||||
:key="article.slug"
|
||||
:article="article">
|
||||
</article-my>
|
||||
<pagination-component v-model="currentPage" :numberOfPages="numberOfPages"></pagination-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { defineComponent } from 'vue';
|
||||
import pagination from "@/components/PaginationComponent.vue";
|
||||
import { usePaginationApi } from "@/api/usePaginationAPI"
|
||||
import router from "@/router";
|
||||
import { followUser, getProfile, unfollowUser } from "@/api";
|
||||
import ArticleMy from "@/components/ArticleMy.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TheProfile.vue",
|
||||
components: {
|
||||
"article-my": ArticleMy,
|
||||
'pagination-component': pagination,
|
||||
},
|
||||
props:{
|
||||
username: String,
|
||||
},
|
||||
setup(props){
|
||||
const store = useStore();
|
||||
const isMe = ref(false);
|
||||
const profile = reactive({
|
||||
image: "",
|
||||
username: "",
|
||||
bio: "",
|
||||
following: false,
|
||||
})
|
||||
const myArticleActive = ref(true);
|
||||
const favoriteArticleActive = ref(false);
|
||||
|
||||
|
||||
const currentPage = ref(1);
|
||||
const rowsPerPage = ref(5);
|
||||
|
||||
const { articleLists, listsAreLoading, isEmpty,loadMyArticles, loadFavoriteArticles, numberOfPages } = usePaginationApi(currentPage, rowsPerPage);
|
||||
|
||||
const setProfile = async ( data: any ) => {
|
||||
profile.image = data.image;
|
||||
profile.bio = data.bio;
|
||||
profile.following = data.following;
|
||||
profile.username = data.username;
|
||||
}
|
||||
|
||||
const stateUpdate = async () => {
|
||||
if(isMe.value){
|
||||
await router.push({name:"Settings"});
|
||||
}else{
|
||||
if(profile.following){
|
||||
try {
|
||||
const { data } = await unfollowUser(profile.username);
|
||||
await setProfile(data.profile);
|
||||
}catch (error: any){
|
||||
alert("error");
|
||||
}
|
||||
}else{
|
||||
try {
|
||||
const { data } = await followUser(profile.username);
|
||||
await setProfile(data.profile);
|
||||
}catch (error: any){
|
||||
alert("error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const myArticleSelect = async () => {
|
||||
myArticleActive.value=true;
|
||||
favoriteArticleActive.value=false;
|
||||
await loadMyArticles(profile.username);
|
||||
}
|
||||
|
||||
const favoriteArticleSelect = async () => {
|
||||
myArticleActive.value=false;
|
||||
favoriteArticleActive.value=true;
|
||||
await loadFavoriteArticles(profile.username);
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () =>{
|
||||
try {
|
||||
const { data } = await getProfile(props.username)
|
||||
await setProfile(data.profile);
|
||||
if(data.profile.username.localeCompare(store.state.username) == 0){
|
||||
isMe.value = true;
|
||||
}else{
|
||||
isMe.value = false;
|
||||
}
|
||||
}catch (error: any){
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "USER_NOT_FOUND")
|
||||
await router.push({name:"home"});
|
||||
}
|
||||
try{
|
||||
await loadMyArticles(profile.username);
|
||||
}catch (error: any){
|
||||
alert(error);
|
||||
}
|
||||
})
|
||||
return { isMe, listsAreLoading, isEmpty, myArticleActive, favoriteArticleActive, profile, articleLists, currentPage, rowsPerPage, stateUpdate, myArticleSelect, favoriteArticleSelect, numberOfPages }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -16,8 +16,6 @@
|
||||
<ul class="error-messages" v-if="usernameDuplicate">
|
||||
<li align="left">username has already been taken</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<form>
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="text" placeholder="Your Name" v-model="user.username">
|
||||
@@ -28,7 +26,7 @@
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="password" placeholder="Password" v-model="user.password">
|
||||
</fieldset>
|
||||
<button @click = "signup" class="btn btn-lg btn-primary pull-xs-right">
|
||||
<button @click = "register" class="btn btn-lg btn-primary pull-xs-right">
|
||||
Sign up
|
||||
</button>
|
||||
</form>
|
||||
@@ -41,9 +39,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import axios from "axios";
|
||||
import { signUp } from "@/api";
|
||||
import router from "@/router";
|
||||
import {reactive, ref} from "vue";
|
||||
import { reactive, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "TheRegister.vue",
|
||||
@@ -51,17 +50,13 @@ export default {
|
||||
let emailDuplicate = ref(false);
|
||||
let usernameDuplicate = ref(false);
|
||||
|
||||
const store = useStore();
|
||||
const user = reactive({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
})
|
||||
|
||||
const allHideError = () => {
|
||||
emailDuplicate.value = false;
|
||||
usernameDuplicate.value = false;
|
||||
}
|
||||
|
||||
const showEmailUsernameError = () => {
|
||||
emailDuplicate.value = true;
|
||||
usernameDuplicate.value = true
|
||||
@@ -76,30 +71,24 @@ export default {
|
||||
emailDuplicate.value = false;
|
||||
};
|
||||
|
||||
const signup = () => {
|
||||
const url = import.meta.env.VITE_BASE_URL;
|
||||
axios.post(url+'/api/users',{
|
||||
user
|
||||
})
|
||||
.then(response => {
|
||||
window.localStorage.setItem("token",response.data.user.token);
|
||||
allHideError();
|
||||
router.push("/");
|
||||
})
|
||||
.catch(error =>{
|
||||
console.log(error);
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "DUPLICATE_EMAIL_USERNAME"){
|
||||
showEmailUsernameError();
|
||||
}else if(code == "DUPLICATE_EMAIL"){
|
||||
showEmailError();
|
||||
}else if(code == "DUPLICATE_USERNAME"){
|
||||
showUsernameError();
|
||||
}
|
||||
})
|
||||
const register = async () => {
|
||||
try{
|
||||
const { data } = await signUp(user);
|
||||
store.dispatch("LOGIN",data.user);
|
||||
router.push({name:"Home"});
|
||||
}catch(error: any){
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "DUPLICATE_EMAIL_USERNAME"){
|
||||
showEmailUsernameError();
|
||||
}else if(code == "DUPLICATE_EMAIL"){
|
||||
showEmailError();
|
||||
}else if(code == "DUPLICATE_USERNAME"){
|
||||
showUsernameError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { user, emailDuplicate, usernameDuplicate, signup, showEmailUsernameError,showEmailError, showUsernameError, allHideError }
|
||||
return { user, emailDuplicate, usernameDuplicate, register, showEmailUsernameError,showEmailError, showUsernameError }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@@ -8,28 +8,39 @@
|
||||
<h1 class="text-xs-center">Your Settings</h1>
|
||||
|
||||
<form>
|
||||
<ul class="error-messages" v-if="emailDuplicate">
|
||||
<li align="left">email has already been taken</li>
|
||||
</ul>
|
||||
<ul class="error-messages" v-if="usernameDuplicate">
|
||||
<li align="left">username has already been taken</li>
|
||||
</ul>
|
||||
|
||||
<fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control" type="text" placeholder="URL of profile picture">
|
||||
<input class="form-control" type="text" placeholder="URL of profile picture" v-model="user.image">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="text" placeholder="Your Name">
|
||||
<input class="form-control form-control-lg" type="text" placeholder="Your Name" v-model="user.username">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<textarea class="form-control form-control-lg" rows="8"
|
||||
placeholder="Short bio about you"></textarea>
|
||||
<textarea class="form-control form-control-lg" rows="8" placeholder="Short bio about you" v-model="user.bio">
|
||||
</textarea>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="text" placeholder="Email">
|
||||
<input class="form-control form-control-lg" type="text" placeholder="Email" v-model="user.email">
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<input class="form-control form-control-lg" type="password" placeholder="Password">
|
||||
<input class="form-control form-control-lg" type="password" placeholder="Password" v-model="password">
|
||||
</fieldset>
|
||||
<button class="btn btn-lg btn-primary pull-xs-right">
|
||||
<button @click="setUser" class="btn btn-lg btn-primary pull-xs-right">
|
||||
Update Settings
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<hr>
|
||||
<button class="btn btn-outline-danger" @click="logout">
|
||||
Or click here to logout.
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -38,8 +49,84 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { getCurrentUser, updateUser } from "@/api";
|
||||
import { useStore } from "vuex";
|
||||
import router from "@/router";
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
|
||||
export default {
|
||||
name: "TheSetting"
|
||||
name: "TheSetting",
|
||||
setup() {
|
||||
const url = import.meta.env.VITE_BASE_URL;
|
||||
const user = reactive({
|
||||
bio: "",
|
||||
email: "",
|
||||
image: "",
|
||||
username: "",
|
||||
password: "",
|
||||
})
|
||||
const password = "";
|
||||
const store = useStore();
|
||||
|
||||
const emailDuplicate = ref(false);
|
||||
const usernameDuplicate = ref(false);
|
||||
|
||||
const getUser = async (getuser: { bio: string; email: string; username: string; image: string; }) => {
|
||||
user.bio = getuser.bio
|
||||
user.email = getuser.email
|
||||
user.username = getuser.username
|
||||
user.image = getuser.image
|
||||
}
|
||||
|
||||
const showEmailError = () => {
|
||||
emailDuplicate.value = true;
|
||||
usernameDuplicate.value = false;
|
||||
};
|
||||
const showUsernameError = () => {
|
||||
usernameDuplicate.value = true;
|
||||
emailDuplicate.value = false;
|
||||
};
|
||||
|
||||
const setUser = async () => {
|
||||
try{
|
||||
const { data } = await updateUser(user);
|
||||
await store.dispatch("LOGIN", data.user);
|
||||
await router.push({
|
||||
name: 'Profile',
|
||||
params: {username: data.user.username}
|
||||
})
|
||||
}catch(error: any){
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "DUPLICATE_EMAIL"){
|
||||
showEmailError();
|
||||
}else if(code == "DUPLICATE_USERNAME"){
|
||||
showUsernameError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logout = async () =>{
|
||||
store.dispatch("LOGOUT").then(()=>{
|
||||
router.push({name: "Home"});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await getCurrentUser();
|
||||
await store.dispatch("LOGIN", data.user)
|
||||
getUser(data.user);
|
||||
} catch (error: any) {
|
||||
const code = error.response.data.errors.code;
|
||||
if(code == "EMAIL_NULL_OR_INVALID"){
|
||||
await router.push({name:"home"});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {user, password, url, emailDuplicate, usernameDuplicate, getUser, setUser, logout};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,14 +16,6 @@ export default defineConfig({
|
||||
server: {
|
||||
https: true,
|
||||
port: 4000,
|
||||
proxy:{
|
||||
'/': {
|
||||
target : "http://3.35.44.58:8080",
|
||||
rewrite: (path) => path.replace(/^\//,''),
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
plugins: [vue(),basicSsl()]
|
||||
|
||||
Reference in New Issue
Block a user