add drag and drop builder

This commit is contained in:
Krrish Ghimire
2026-07-01 01:14:40 +05:45
parent 65f38d68c5
commit 77b32b4d32
31 changed files with 2187 additions and 1 deletions

View File

@@ -0,0 +1,115 @@
package com.indie.controller;
import com.indie.config.UserPrincipal;
import com.indie.dto.ComponentResponse;
import com.indie.dto.PageWithComponentsResponse;
import com.indie.dto.SaveComponentsRequest;
import com.indie.model.Component;
import com.indie.model.Page;
import com.indie.model.Site;
import com.indie.repository.ComponentRepository;
import com.indie.repository.PageRepository;
import com.indie.repository.SiteRepository;
import com.indie.service.BuilderService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/builder/sites/{siteId}")
public class BuilderController {
private final SiteRepository siteRepository;
private final PageRepository pageRepository;
private final ComponentRepository componentRepository;
private final BuilderService builderService;
public BuilderController(SiteRepository siteRepository, PageRepository pageRepository,
ComponentRepository componentRepository, BuilderService builderService) {
this.siteRepository = siteRepository;
this.pageRepository = pageRepository;
this.componentRepository = componentRepository;
this.builderService = builderService;
}
@GetMapping("/pages")
public ResponseEntity<List<PageWithComponentsResponse>> listPagesWithComponents(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable UUID siteId) {
Site site = findSiteForUser(siteId, principal.id());
List<Page> pages = pageRepository.findBySiteIdOrderByOrderIndexAsc(site.getId());
List<PageWithComponentsResponse> response = pages.stream()
.map(page -> {
List<ComponentResponse> components = componentRepository
.findByPageIdOrderByOrderIndexAsc(page.getId())
.stream()
.map(ComponentResponse::from)
.toList();
return PageWithComponentsResponse.from(page, components);
})
.toList();
return ResponseEntity.ok(response);
}
@PutMapping("/pages/{pageId}/components")
public ResponseEntity<List<ComponentResponse>> saveComponents(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable UUID siteId,
@PathVariable UUID pageId,
@Valid @RequestBody SaveComponentsRequest request) {
Site site = findSiteForUser(siteId, principal.id());
Page page = findPageForSite(pageId, site.getId());
List<Component> components = builderService.saveComponents(page.getId(), request);
List<ComponentResponse> response = components.stream()
.map(ComponentResponse::from)
.toList();
return ResponseEntity.ok(response);
}
@GetMapping("/theme")
public ResponseEntity<Map<String, Object>> getTheme(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable UUID siteId) {
Site site = findSiteForUser(siteId, principal.id());
return ResponseEntity.ok(site.getThemeConfig());
}
@PutMapping("/theme")
public ResponseEntity<Map<String, Object>> updateTheme(
@AuthenticationPrincipal UserPrincipal principal,
@PathVariable UUID siteId,
@RequestBody Map<String, Object> themeConfig) {
Site site = findSiteForUser(siteId, principal.id());
site.setThemeConfig(themeConfig);
siteRepository.save(site);
return ResponseEntity.ok(site.getThemeConfig());
}
private Site findSiteForUser(UUID siteId, UUID userId) {
Site site = siteRepository.findById(siteId)
.orElseThrow(() -> new IllegalArgumentException("Site not found"));
if (!site.getUserId().equals(userId)) {
throw new IllegalArgumentException("Site not found");
}
return site;
}
private Page findPageForSite(UUID pageId, UUID siteId) {
Page page = pageRepository.findById(pageId)
.orElseThrow(() -> new IllegalArgumentException("Page not found"));
if (!page.getSiteId().equals(siteId)) {
throw new IllegalArgumentException("Page not found");
}
return page;
}
}

View File

@@ -0,0 +1,34 @@
package com.indie.dto;
import com.indie.model.Component;
import com.indie.model.ComponentType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
import java.util.UUID;
@Data
@AllArgsConstructor
@Builder
public class ComponentResponse {
private UUID id;
private UUID pageId;
private ComponentType type;
private Map<String, Object> config;
private Map<String, Object> styles;
private int orderIndex;
public static ComponentResponse from(Component component) {
return ComponentResponse.builder()
.id(component.getId())
.pageId(component.getPageId())
.type(component.getType())
.config(component.getConfig())
.styles(component.getStyles())
.orderIndex(component.getOrderIndex())
.build();
}
}

View File

@@ -0,0 +1,40 @@
package com.indie.dto;
import com.indie.model.Page;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Data
@AllArgsConstructor
@Builder
public class PageWithComponentsResponse {
private UUID id;
private UUID siteId;
private String title;
private String slug;
private String seoTitle;
private String seoDescription;
private int orderIndex;
private LocalDateTime createdAt;
private List<ComponentResponse> components;
public static PageWithComponentsResponse from(Page page, List<ComponentResponse> components) {
return PageWithComponentsResponse.builder()
.id(page.getId())
.siteId(page.getSiteId())
.title(page.getTitle())
.slug(page.getSlug())
.seoTitle(page.getSeoTitle())
.seoDescription(page.getSeoDescription())
.orderIndex(page.getOrderIndex())
.createdAt(page.getCreatedAt())
.components(components)
.build();
}
}

View File

@@ -0,0 +1,28 @@
package com.indie.dto;
import com.indie.model.ComponentType;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Data
public class SaveComponentsRequest {
@Valid
@NotNull
private List<ComponentDTO> components;
@Data
public static class ComponentDTO {
private UUID id;
@NotNull
private ComponentType type;
private Map<String, Object> config;
private Map<String, Object> styles;
private int orderIndex;
}
}

View File

@@ -0,0 +1,55 @@
package com.indie.service;
import com.indie.dto.SaveComponentsRequest;
import com.indie.model.Component;
import com.indie.repository.ComponentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class BuilderService {
private final ComponentRepository componentRepository;
static final int MAX_COMPONENTS_PER_PAGE = 50;
public void validateComponents(SaveComponentsRequest request) {
if (request.getComponents() == null || request.getComponents().isEmpty()) {
throw new IllegalArgumentException("Component list must not be empty");
}
if (request.getComponents().size() > MAX_COMPONENTS_PER_PAGE) {
throw new IllegalArgumentException("Maximum " + MAX_COMPONENTS_PER_PAGE + " components allowed per page");
}
for (SaveComponentsRequest.ComponentDTO dto : request.getComponents()) {
if (dto.getType() == null) {
throw new IllegalArgumentException("Component type is required");
}
}
}
@Transactional
public List<Component> saveComponents(UUID pageId, SaveComponentsRequest request) {
validateComponents(request);
componentRepository.deleteByPageId(pageId);
List<Component> components = request.getComponents().stream()
.map(dto -> Component.builder()
.id(dto.getId() != null ? dto.getId() : UUID.randomUUID())
.pageId(pageId)
.type(dto.getType())
.config(dto.getConfig())
.styles(dto.getStyles())
.orderIndex(dto.getOrderIndex())
.build())
.toList();
return componentRepository.saveAll(components);
}
}