Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b733904
added translations functionality - static translations via i18n, and …
AlenGanopolsky Mar 20, 2026
5260e6d
Merge branch 'Alen-Ganopolsky/Frontend-Translation' of https://github…
AlenGanopolsky Mar 20, 2026
82b2e3a
added .md file for translations
AlenGanopolsky Mar 20, 2026
0291303
Merge branch 'main' into Alen-Ganopolsky/Frontend-Translation
AlenGanopolsky Mar 21, 2026
72d4d19
Merge branch 'main' into Alen-Ganopolsky/Frontend-Translation
AlenGanopolsky Mar 22, 2026
54f8cb0
translation changes
AlenGanopolsky Mar 22, 2026
f4f0e1b
merge conflict fix
AlenGanopolsky Mar 22, 2026
f3fd3d9
changes to backend to fix saved events page
AlenGanopolsky Mar 23, 2026
5e1d46b
fixed tests and saved backend
AlenGanopolsky Mar 23, 2026
63e2b2e
Merge branch 'main' into Alen-Ganopolsky/Frontend-Translation
AlenGanopolsky Mar 23, 2026
82b47a4
fixed saved route tests
AlenGanopolsky Mar 23, 2026
011dd12
merge conflict fixes
AlenGanopolsky Mar 24, 2026
35342ec
merge conflict fixes
AlenGanopolsky Mar 24, 2026
8624dd4
some fixes ig
AlenGanopolsky Mar 24, 2026
e52bf5d
dockerfile fixes
AlenGanopolsky Mar 24, 2026
2104743
misc changes
AlenGanopolsky Mar 24, 2026
7b837e1
fixed merge
AlenGanopolsky Mar 25, 2026
c71fe7e
misc
AlenGanopolsky Mar 25, 2026
74c83f7
Merge branch 'main' of https://github.com/GenerateNU/skillspark
AlenGanopolsky Mar 25, 2026
7ffe237
fixed merge
AlenGanopolsky Mar 25, 2026
beb3549
font fixes
AlenGanopolsky Mar 25, 2026
387ce1b
Merge branch 'main' of https://github.com/GenerateNU/skillspark
AlenGanopolsky Mar 25, 2026
48d032b
merge fix
AlenGanopolsky Mar 25, 2026
e8822a7
fixes to translations
AlenGanopolsky Mar 25, 2026
66ab886
bookmark fixes
AlenGanopolsky Mar 25, 2026
85d3c20
recent changes + merge fixes
AlenGanopolsky Mar 26, 2026
aab1c9e
removed logs
AlenGanopolsky Mar 26, 2026
bed13dd
made fixes that were suggested by comments
AlenGanopolsky Mar 26, 2026
9c36e7a
pr fixes
AlenGanopolsky Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions backend/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,15 @@ paths:
summary: Creates a saved event
description: Creates a saved event
operationId: create-saved
parameters:
- name: Accept-Language
in: header
schema:
type: string
default: en-US
enum:
- en-US
- th-TH
requestBody:
content:
application/json:
Expand Down Expand Up @@ -1922,6 +1931,14 @@ paths:
default: 10
minimum: 1
maximum: 100
- name: Accept-Language
in: header
schema:
type: string
default: en-US
enum:
- en-US
- th-TH
responses:
"200":
description: OK
Expand Down
8 changes: 5 additions & 3 deletions backend/internal/models/saved.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type CreateSavedInput struct {
GuardianID uuid.UUID `json:"guardian_id" db:"guardian_id" doc:"ID of the guardian that saved this."`
EventID uuid.UUID `json:"event_id" db:"event_id" doc:"ID of this saved event."`
}
AcceptLanguage string `header:"Accept-Language" default:"en-US" enum:"en-US,th-TH"`
}

type CreateSavedOutput struct {
Expand All @@ -36,9 +37,10 @@ type DeleteSavedOutput struct {
}

type GetSavedInput struct {
ID uuid.UUID `path:"id"`
Page int `query:"page" minimum:"1" default:"1" doc:"Page number (starts at 1)"`
PageSize int `query:"page_size" minimum:"1" maximum:"100" default:"10" doc:"Number of items per page"`
ID uuid.UUID `path:"id"`
Page int `query:"page" minimum:"1" default:"1" doc:"Page number (starts at 1)"`
PageSize int `query:"page_size" minimum:"1" maximum:"100" default:"10" doc:"Number of items per page"`
AcceptLanguage string `header:"Accept-Language" default:"en-US" enum:"en-US,th-TH"`
}

type GetSavedOutput struct {
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/service/handler/saved/getByGuardianID.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import (
"github.com/google/uuid"
)

func (h *Handler) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) {
func (h *Handler) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error) {

if _, err := h.GuardianRepository.GetGuardianByID(ctx, id); err != nil {
return nil, errs.BadRequest("Invalid guardian_id: guardian does not exist")
}

reviews, httpErr := h.SavedRepository.GetByGuardianID(ctx, id, pagination)
reviews, httpErr := h.SavedRepository.GetByGuardianID(ctx, id, pagination, AcceptLanguage)
if httpErr != nil {
return nil, httpErr
}
Expand Down
4 changes: 3 additions & 1 deletion backend/internal/service/handler/saved/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func TestHandler_GetByGuardianID(t *testing.T) {
mock.Anything,
uuid.MustParse("11111111-1111-1111-1111-111111111111"),
mock.AnythingOfType("utils.Pagination"),
mock.AnythingOfType("string"),
).Return([]models.Saved{
{
ID: uuid.MustParse("20000000-0000-0000-0000-000000000001"),
Expand Down Expand Up @@ -81,6 +82,7 @@ func TestHandler_GetByGuardianID(t *testing.T) {
mock.Anything,
uuid.MustParse("22222222-2222-2222-2222-222222222222"),
mock.AnythingOfType("utils.Pagination"),
mock.AnythingOfType("string"),
).Return(nil, errs.BadRequest("cannot fetch saved"))
},
wantSaved: nil,
Expand All @@ -105,7 +107,7 @@ func TestHandler_GetByGuardianID(t *testing.T) {

pagination := utils.Pagination{Page: 1, Limit: 10}

saved, err := handler.GetByGuardianID(context.Background(), tt.guardianID, pagination)
saved, err := handler.GetByGuardianID(context.Background(), tt.guardianID, pagination, "en-US")

if tt.wantErr {
assert.Error(t, err)
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/service/routes/saved.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func SetUpSavedRoutes(api huma.API, repo *storage.Repository) {
Limit: limit,
}

saveds, err := savedHandler.SavedRepository.GetByGuardianID(ctx, input.ID, pagination)
saveds, err := savedHandler.SavedRepository.GetByGuardianID(ctx, input.ID, pagination, input.AcceptLanguage)
if err != nil {
return nil, err
}
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/service/routes/saved_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func TestGetSavedByGuardianID_Success(t *testing.T) {
mock.Anything,
guardianID,
utils.Pagination{Page: 1, Limit: 10},
mock.AnythingOfType("string"),
).Return(expectedSaved, nil)

app, _ := setupSavedTestAPI(mockRepo)
Expand Down Expand Up @@ -141,6 +142,7 @@ func TestGetSavedByGuardianID_WithPagination(t *testing.T) {
mock.Anything,
guardianID,
utils.Pagination{Page: 2, Limit: 10},
mock.AnythingOfType("string"),
).Return(expectedSaved, nil)

app, _ := setupSavedTestAPI(mockRepo)
Expand Down
3 changes: 2 additions & 1 deletion backend/internal/storage/postgres/schema/saved/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
"skillspark/internal/storage/postgres/schema"
)

const language = "en-US"
var language string

func (r *SavedRepository) CreateSaved(ctx context.Context, saved *models.CreateSavedInput) (*models.Saved, error) {
language = saved.AcceptLanguage

query, err := schema.ReadSQLBaseScript("create.sql", SqlSavedFiles)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/google/uuid"
)

func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) {
func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error) {

query, err := schema.ReadSQLBaseScript("get_all_for_user.sql", SqlSavedFiles)
if err != nil {
Expand Down Expand Up @@ -64,7 +64,7 @@ func (r *SavedRepository) GetByGuardianID(ctx context.Context, user_id uuid.UUID
return nil, &err
}

switch language {
switch AcceptLanguage {
case "th-TH":
if titleTH != nil {
s.Event.Title = *titleTH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestGetReviewsByGuardianID(t *testing.T) {
}

pagination := utils.Pagination{Limit: 10, Page: 1}
reviews, err := repo.GetByGuardianID(ctx, firstSaved.GuardianID, pagination)
reviews, err := repo.GetByGuardianID(ctx, firstSaved.GuardianID, pagination, "en-US")
require.Nil(t, err)
require.Len(t, reviews, len(expectedSaved))
}
Expand All @@ -60,7 +60,7 @@ func TestGetReviewsByGuardianID_NoReviews(t *testing.T) {
g := guardian.CreateTestGuardian(t, ctx, testDB)

pagination := utils.Pagination{Limit: 10, Page: 1}
reviews, err := repo.GetByGuardianID(ctx, g.ID, pagination)
reviews, err := repo.GetByGuardianID(ctx, g.ID, pagination, "en-US")

require.Nil(t, err)
require.NotNil(t, reviews)
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/storage/repo-mocks/savedMock.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ func (m *MockSavedRepository) CreateSaved(ctx context.Context, input *models.Cre
return args.Get(0).(*models.Saved), args.Error(1)
}

func (m *MockSavedRepository) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error) {
args := m.Called(ctx, id, pagination)
func (m *MockSavedRepository) GetByGuardianID(ctx context.Context, id uuid.UUID, pagination utils.Pagination, acceptLanguage string) ([]models.Saved, error) {
args := m.Called(ctx, id, pagination, acceptLanguage)
if args.Get(0) == nil {
if args.Get(1) == nil {
return nil, nil
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ type NotificationRepository interface {
type SavedRepository interface {
CreateSaved(ctx context.Context, saved *models.CreateSavedInput) (*models.Saved, error)
DeleteSaved(ctx context.Context, id uuid.UUID) error
GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination) ([]models.Saved, error)
GetByGuardianID(ctx context.Context, user_id uuid.UUID, pagination utils.Pagination, AcceptLanguage string) ([]models.Saved, error)
}

type Repository struct {
Expand Down
8 changes: 5 additions & 3 deletions frontend/apps/mobile/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { HapticTab } from "@/components/haptic-tab";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
import { useTranslation } from "react-i18next";

export default function TabLayout() {
const colorScheme = useColorScheme();
const { t: translate } = useTranslation();

return (
<Tabs
Expand All @@ -20,7 +22,7 @@ export default function TabLayout() {
<Tabs.Screen
name="index"
options={{
title: "Home",
title: translate('nav.home'),
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
Expand All @@ -29,7 +31,7 @@ export default function TabLayout() {
<Tabs.Screen
name="map"
options={{
title: "Map",
title: translate('nav.map'),
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="map.fill" color={color} />
),
Expand All @@ -38,7 +40,7 @@ export default function TabLayout() {
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
title: translate('nav.profile'),
tabBarIcon: ({ color }) => <IconSymbol size={28} name="person.fill" color={color} />,
}}
/>
Expand Down
21 changes: 12 additions & 9 deletions frontend/apps/mobile/app/(tabs)/event/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { AppColors } from "@/constants/theme";
import { StarRating } from "@/components/StarRating";
import { BookmarkButton } from "@/components/BookmarkButton";
import { formatDuration } from "@/utils/format";
import { useTranslation } from "react-i18next";

function formatAddress(occurrence: EventOccurrence) {
const loc = occurrence.location;
Expand All @@ -33,6 +34,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
const [descriptionTruncated, setDescriptionTruncated] = useState(false);
const duration = formatDuration(occurrence.start_time, occurrence.end_time);
const address = formatAddress(occurrence);
const { t: translate } = useTranslation();

return (
<View style={{ flex: 1, backgroundColor: "#F4F6F8" }}>
Expand Down Expand Up @@ -87,7 +89,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
}}
>
<MaterialIcons name="chevron-left" size={20} color={AppColors.primaryText} />
<Text style={{ fontSize: 15, color: AppColors.primaryText, fontWeight: "500" }}>Back</Text>
<Text style={{ fontSize: 15, color: AppColors.primaryText, fontWeight: "500" }}>{translate('event.back')}</Text>
</TouchableOpacity>
</View>
<View
Expand Down Expand Up @@ -116,7 +118,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
}}
/>
<View style={{ position: "absolute", top: -5, left: 10 }}>
<BookmarkButton occurrenceId={occurrence.id} />
<BookmarkButton eventId={occurrence.event.id} event={occurrence.event} />
</View>
<Text
style={{
Expand Down Expand Up @@ -165,7 +167,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
{descriptionTruncated && (
<Pressable onPress={() => setDescriptionExpanded((prev) => !prev)} style={{ marginBottom: 14 }}>
<Text style={{ fontSize: 13, color: AppColors.primaryText, fontWeight: "600" }}>
{descriptionExpanded ? "See less" : "See more"}
{descriptionExpanded ? translate('event.seeLess') : translate('event.seeMore')}
</Text>
</Pressable>
)}
Expand All @@ -188,13 +190,13 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
paddingVertical: 7,
}}
>
<Text style={{ fontSize: 13, color: AppColors.secondaryText }}>{cat}</Text>
<Text style={{ fontSize: 13, color: AppColors.secondaryText }}>{translate(`dashboard.categories.${cat.toLowerCase()}`, { defaultValue: cat })}</Text>
</View>
))}
</View>
<View style={{ alignItems: "flex-end", marginLeft: 14 }}>
<Text style={{ fontSize: 20, fontWeight: "700", color: AppColors.primaryText }}>{occurrence.price} THB</Text>
<Text style={{ fontSize: 12, color: AppColors.subtleText }}>/Session</Text>
<Text style={{ fontSize: 12, color: AppColors.subtleText }}>{translate('event.perSession')}</Text>
</View>
</View>
</View>
Expand Down Expand Up @@ -269,7 +271,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
>
<View style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: "#fff" }} />
</View>
<Text style={{ fontSize: 14, color: AppColors.secondaryText, fontWeight: "500" }}>Home</Text>
<Text style={{ fontSize: 14, color: AppColors.secondaryText, fontWeight: "500" }}>{translate('event.home')}</Text>
</View>
<View style={{ paddingLeft: 6, paddingVertical: 2 }}>
<Text style={{ fontSize: 14, color: AppColors.subtleText, lineHeight: 10 }}>•</Text>
Expand All @@ -278,7 +280,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
</View>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<MaterialIcons name="location-on" size={16} color={AppColors.secondaryText} />
<Text style={{ fontSize: 14, color: AppColors.secondaryText, fontWeight: "500" }}>Location</Text>
<Text style={{ fontSize: 14, color: AppColors.secondaryText, fontWeight: "500" }}>{translate('event.location')}</Text>
</View>
</View>
<TouchableOpacity
Expand All @@ -291,7 +293,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
paddingVertical: 14,
}}
>
<Text style={{ color: "#fff", fontSize: 17, fontWeight: "700" }}>Register</Text>
<Text style={{ color: "#fff", fontSize: 17, fontWeight: "700" }}>{translate('event.register')}</Text>
</TouchableOpacity>
</View>
</View>
Expand All @@ -304,6 +306,7 @@ function EventOccurrenceDetail({ occurrence }: { occurrence: EventOccurrence })
export default function EventOccurrenceScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { data: response, isLoading, error } = useGetEventOccurrencesById(id);
const { t: translate } = useTranslation();

if (isLoading) {
return (
Expand All @@ -317,7 +320,7 @@ export default function EventOccurrenceScreen() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 24 }}>
<Text style={{ color: AppColors.danger, fontWeight: "600", fontSize: 16 }}>
Event not found
{translate('event.notFound')}
</Text>
</View>
);
Expand Down
Loading
Loading