Skip to content

Commit

Permalink
feat: now we can upload video per log (#138)
Browse files Browse the repository at this point in the history
* feat: now we can upload video per log

* chore: remove un-used code
  • Loading branch information
wajeht authored Jul 31, 2022
1 parent 25fe8d6 commit 0409524
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 12 deletions.
30 changes: 30 additions & 0 deletions src/apps/api/v1/logs/logs.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as LogsQueries from './logs.queries.js';
import * as VideosQueries from '../videos/videos.queries.js';
import { StatusCodes } from 'http-status-codes';
import CustomError from '../../api.errors.js';
import logger from '../../../../utils/logger.js';
Expand All @@ -23,3 +24,32 @@ export async function createLogs(req, res) {
data: created,
});
}

/**
* It uploads a video to the server and inserts the video's path and url into the database
* @param req - The request object.
* @param res - The response object.
*/
export async function uploadAVideo(req, res) {
const { path: video_path } = req.file;
const video_url = req.file.path.split('public')[1];
const { user_id, session_id } = req.body;
const { log_id } = req.params;

const inserted = await VideosQueries.insertVideo({
video_path,
video_url,
user_id,
log_id,
session_id,
});

logger.info(`User id ${user_id} has inserted video id ${inserted[0].id} !`);

res.status(StatusCodes.CREATED).json({
status: 'success',
request_url: req.originalUrl,
message: 'The resource was created successfully!',
data: inserted,
});
}
15 changes: 15 additions & 0 deletions src/apps/api/v1/logs/logs.router.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validator, catchAsyncErrors } from '../../api.middlewares.js';
import { uploadVideo } from '../../../../utils/multer.js';

import * as LogsValidation from './logs.validation.js';
import * as LogsController from './logs.controller.js';
Expand All @@ -18,4 +19,18 @@ const logs = express.Router();
*/
logs.post('/', validator(LogsValidation.createLogs), catchAsyncErrors(LogsController.createLogs));

/**
* POST /api/v1/logs/{log_id}/upload-a-video
* @tags logs
* @summary add a video to a log
* @param {number} log_id.form.required - the name of the log - application/x-www-form-urlencoded
* @param {number} user_id.form.required - current session_id - application/x-www-form-urlencoded
*/
logs.post(
'/:log_id/upload-a-video',
uploadVideo,
validator(LogsValidation.uploadAVideo),
catchAsyncErrors(LogsController.uploadAVideo),
);

export default logs;
44 changes: 44 additions & 0 deletions src/apps/api/v1/logs/logs.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,47 @@ export const createLogs = [
}
}),
];

export const uploadAVideo = [
param('log_id')
.trim()
.notEmpty()
.withMessage('lid must not be empty!')
.bail()
.isNumeric()
.withMessage('lid must be an ID!')
.bail()
.toInt()
.custom(async (log_id) => {
const log = await LogsQueries.getLogById(log_id);
if (log.length === 0) throw new Error('Log does not exist!');
return true;
})
.toInt(),
body('user_id')
.trim()
.notEmpty()
.withMessage('User id must not be empty!')
.isInt()
.withMessage('User id must be an number!')
.custom(async (user_id, { req }) => {
if (user_id) {
const user = await UserQueries.findUserById(user_id);
if (user.length === 0) throw new Error('User does not exist!');
}
return true;
})
.toInt(),
body('session_id')
.trim()
.notEmpty()
.withMessage('Session id must not be empty!')
.isInt()
.withMessage('Session id must be an ID!')
.custom(async (sid) => {
const user = await SessionsQueries.getSessionBySessionId(sid);
if (user.length === 0) throw new Error('Session id does not exist!');
return true;
})
.toInt(),
];
18 changes: 17 additions & 1 deletion src/apps/api/v1/sessions/sessions.queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export async function getSessionBySessionId(sid) {
`
select
l.*,
(select coalesce(jsonb_agg(s.* order by s.id asc) filter (where s.id is not null and s.deleted = false), '[]') ) as sets
(select coalesce(jsonb_agg(s.* order by s.id asc) filter (where s.id is not null and s.deleted = false), '[]')) as sets
from
sets s
full join logs l on l.id = s.log_id
Expand All @@ -152,6 +152,22 @@ export async function getSessionBySessionId(sid) {
[sid],
);

const { rows: videos } = await db.raw(
`
select
(select coalesce(jsonb_agg(v.*) filter (where v.id is not null and v.deleted = false), '[]')) as videos
from
videos v
inner join logs l on l.id = v.log_id
where
v.session_id = ?
`,
[sid],
);

// TODO: this is not good for performance
sets.forEach((set, idx) => (set.videos = videos[idx]?.videos));

// // session with block info
// const joined = await db
// .select(
Expand Down
Empty file.
5 changes: 5 additions & 0 deletions src/apps/api/v1/videos/videos.queries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import db from '../../../../database/db.js';

export function insertVideo(details) {
return db.insert(details).into('videos').returning('*');
}
Empty file.
Empty file.
157 changes: 156 additions & 1 deletion src/apps/ui/components/dashboard/SessionDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ const completeCurrentSessionShowHideOtherFields = ref(false);
const completeCurrentSessionLoading = ref(false);
const deleteCurrentSessionLoading = ref(false);
const uploadAVideoLoading = ref(false);
const video = ref(null);
// watches
// update exercise db as changes in categories
watch(chooseExerciseCategoryId, async (currentValue, oldValue) => {
Expand Down Expand Up @@ -582,6 +585,58 @@ function clearDataAndDismissDeleteSessionModal() {
);
modal.hide();
}
async function uploadAVideo() {
try {
uploadAVideoLoading.value = true;
const file = video.value.files[0];
let formData = new FormData();
formData.append('video', file);
formData.append('user_id', userStore.user.id);
formData.append('session_id', currentSessionDetails.id);
const data = {
method: 'POST',
body: formData,
};
const res = await window.fetch(`/api/v1/logs/${addASetLogId.value}/upload-a-video`, data); // prettier-ignore
const json = await res.json();
if (res.status === 403 || res.status === 401) {
userStore.logOut();
return;
}
if (!res.ok) {
if (json.errors) {
throw json.errors;
} else {
throw json.message;
}
}
uploadAVideoLoading.value = false;
clearDataAndDismissUploadAVideoModal();
alert.type = 'success';
alert.msg = 'A video has been uploaded!';
} catch (e) {
alert.type = 'danger';
if (Array.isArray(e)) {
alert.msg = e.map((cur) => cur.msg).join(' ');
return;
} else {
alert.msg = e;
}
}
}
function clearDataAndDismissUploadAVideoModal() {
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('upload-a-video'));
modal.hide();
}
</script>
<template>
Expand Down Expand Up @@ -779,6 +834,16 @@ function clearDataAndDismissDeleteSessionModal() {
</span>
</h6>
<!-- video -->
<div
v-if="log.videos?.length && log.collapsed"
class="card card-body p-0 m-0 pt-2 pb-1 border-0"
>
<video v-for="v in log.videos" controls preload="none" poster="">
<source :src="v.video_url" type="video/mp4" />
</video>
</div>
<!-- notes -->
<p
v-if="log.notes && log.collapsed"
Expand Down Expand Up @@ -915,7 +980,14 @@ function clearDataAndDismissDeleteSessionModal() {
<!-- add a video group -->
<span>
<button type="button" class="btn btn-sm btn-outline-dark" disabled>
<button
@click="(addASetLogId = log.id), (set.exercise_name = log.name)"
type="button"
class="btn btn-sm btn-outline-dark"
data-bs-toggle="modal"
data-bs-target="#upload-a-video"
:disabled="log.videos?.length || currentSessionDetails.end_date"
>
<i class="bi bi-play-circle"></i>
</button>
</span>
Expand Down Expand Up @@ -1823,6 +1895,89 @@ function clearDataAndDismissDeleteSessionModal() {
</div>
</div>
</form>
<!-- upload a video modal -->
<form
@submit.prevent="uploadAVideo()"
class="modal fade px-2 py-5"
id="upload-a-video"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<!-- header -->
<div class="modal-header">
<h5 class="modal-title">
<span> Upload a video for </span>
<span class="fw-light">{{ set.exercise_name }}</span>
</h5>
<button
@click="clearDataAndDismissUploadAVideoModal()"
type="reset"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<!-- body -->
<div class="modal-body text-center">
<div>
<input
ref="video"
class="form-control"
id="video"
type="file"
accept="video/*"
hidden
/>
<div
@click="$refs.video.click()"
class="alert alert-primary d-flex flex-column gap-1 m-0 p-0 py-3 my-1"
role="button"
>
<i class="bi bi-cloud-arrow-up-fill"></i>
<span> Click here to choose video! </span>
</div>
</div>
</div>
<!-- footer -->
<div class="modal-footer">
<!-- cancel -->
<button
@click="clearDataAndDismissUploadAVideoModal()"
v-if="!uploadAVideoLoading"
type="reset"
class="btn btn-danger"
data-bs-dismiss="modal"
>
<i class="bi bi-x-circle-fill"></i>
Cancel
</button>
<!-- confirm -->
<button type="submit" class="btn btn-success" :disabled="uploadAVideoLoading">
<div
v-if="uploadAVideoLoading"
class="spinner-border spinner-border-sm"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
<span v-if="!uploadAVideoLoading"
><i class="bi bi-check-circle-fill"></i> Confirm
</span>
<span v-if="uploadAVideoLoading"> Loading... </span>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</XyzTransition>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ onMounted(async () => {
const res = await api.get(`/api/v1/users/${userStore.user.id}`);
const json = await res.json();
const [data] = json.data;
console.log({ data });
first_name.value = data.first_name;
last_name.value = data.last_name;
profile_picture_url.value = data.profile_picture_url;
Expand Down
2 changes: 1 addition & 1 deletion src/database/migrations/20220718015544_user_details.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function up(knex) {
role VARCHAR(250) NOT NULL DEFAULT 'user',
birth_date DATE,
weight INT,
profile_picture_url VARCHAR(500),
profile_picture_url VARCHAR(1000),
profile_picture_path VARCHAR(1000),
verified BOOLEAN DEFAULT FALSE,
verification_token VARCHAR(500) NOT NULL,
Expand Down
31 changes: 31 additions & 0 deletions src/database/migrations/20220731101203_videos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function up(knex) {
// videos
await knex.schema.raw(`
CREATE TABLE IF NOT EXISTS videos (
id SERIAL PRIMARY KEY,
video_url VARCHAR(1000) DEFAULT NULL,
video_path VARCHAR(1000) DEFAULT NULL,
screenshot_url VARCHAR(1000) DEFAULT NULL,
screenshot_path VARCHAR(1000) DEFAULT NULL,
user_id INT REFERENCES users on DELETE CASCADE NOT NULL,
log_id INT REFERENCES logs on DELETE CASCADE NOT NULL,
session_id INT REFERENCES sessions on DELETE CASCADE,
json jsonb DEFAULT NULL,
deleted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
`);
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function down(knex) {
await knex.schema.raw(`DROP TABLE IF EXISTS videos;`);
}
Loading

0 comments on commit 0409524

Please sign in to comment.