├── .byebug_history ├── .deploy ├── .helmignore ├── Chart.yaml ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── db-migrate-job.yaml │ ├── deployment.yaml │ ├── env-cm.yaml │ ├── env-secret.yaml │ ├── hpa.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── tests │ │ └── test-connection.yaml ├── values.yaml └── values │ └── production │ ├── config.yaml │ └── secrets.yaml ├── .dockerignore ├── .env.docker ├── .env.example ├── .env.test ├── .github └── workflows │ ├── ci.yaml │ ├── production.yaml │ └── release-please.yaml ├── .gitignore ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile.dev ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── articles │ │ │ └── fallback.jpg │ │ ├── logo-pic.svg │ │ └── users │ │ │ └── defaut_avatar.png │ ├── javascripts │ │ ├── alert_custom.js │ │ ├── application.js │ │ ├── navigation.js │ │ ├── widget.js.coffee │ │ └── widgets │ │ │ ├── phone.js.coffee │ │ │ ├── scroll_sidebr.js.coffee │ │ │ └── sticky_nav.js.coffee │ └── stylesheets │ │ ├── _iphone.sass │ │ ├── application.sass │ │ ├── article │ │ ├── _article.sass │ │ └── _article_page.sass │ │ ├── framework_and_overrides.sass │ │ ├── general │ │ ├── _btn.sass │ │ ├── _extends.sass │ │ ├── _form.sass │ │ ├── _general.sass │ │ ├── _mixin.sass │ │ └── _vars.sass │ │ └── layout │ │ ├── _admin.sass │ │ ├── _alert.sass │ │ ├── _authform.sass │ │ ├── _breadcrumb.sass │ │ ├── _courses-wrap.sass │ │ ├── _floating.sass │ │ ├── _footer.sass │ │ ├── _form.sass │ │ ├── _hero.sass │ │ ├── _list.sass │ │ ├── _logo.sass │ │ ├── _navigation.sass │ │ ├── _navigation_opened.sass │ │ ├── _pagination.sass │ │ ├── _phone.sass │ │ ├── _recent_posts.sass │ │ ├── _sidebar.sass │ │ ├── _sticky.sass │ │ ├── _tags.sass │ │ ├── _users.sass │ │ ├── _wrapper.sass │ │ └── comments.sass ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── articles_controller.rb │ │ │ ├── base_controller.rb │ │ │ ├── categories_controller.rb │ │ │ ├── comments_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── articles_controller.rb │ ├── categories_controller.rb │ ├── comments_controller.rb │ ├── concerns │ │ └── .keep │ ├── courses_controller.rb │ ├── home_controller.rb │ ├── registrations_controller.rb │ ├── search_controller.rb │ ├── tags_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── articles_helper.rb │ ├── btn_helper.rb │ ├── comments_helper.rb │ ├── devise_helper.rb │ ├── svg_helper.rb │ └── user_helper.rb ├── mailers │ └── .keep ├── models │ ├── application_record.rb │ ├── article.rb │ ├── articles_tag.rb │ ├── category.rb │ ├── comment.rb │ ├── concerns │ │ └── .keep │ ├── tag.rb │ └── user.rb ├── serializers │ ├── article_serializer.rb │ ├── comment_serializer.rb │ └── user_serializer.rb ├── services │ ├── create_admin_service.rb │ ├── create_category_service.rb │ ├── create_demo_article_service.rb │ ├── create_demo_user_service.rb │ └── create_tag_service.rb ├── uploaders │ ├── article_image_uploader.rb │ └── avatar_uploader.rb └── views │ ├── articles │ ├── _article_date.html.erb │ ├── _article_meta.html.erb │ ├── _form.html.erb │ ├── _social.html.erb │ ├── _tags.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── categories │ ├── _form.html.erb │ ├── _list.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── comments │ ├── _comments.html.erb │ ├── _form.html.erb │ └── _list.html.erb │ ├── courses │ └── index.html.erb │ ├── devise │ ├── passwords │ │ └── new.html.erb │ ├── registrations │ │ ├── edit.html.erb │ │ └── new.html.erb │ └── sessions │ │ └── new.html.erb │ ├── home │ ├── _banner.html.erb │ └── index.html.erb │ ├── layouts │ ├── _breadcrumb.html.erb │ ├── _devise_links.html.erb │ ├── _footer.html.erb │ ├── _head.html.erb │ ├── _messages.html.erb │ ├── _navigation.html.erb │ ├── _navigation_links.html.erb │ ├── _sidebar.html.erb │ ├── application.html.erb │ ├── articles │ │ ├── article.html.erb │ │ └── articles.html.erb │ ├── courses.html.erb │ └── devise.html.erb │ ├── search │ └── index.html.erb │ ├── tags │ └── show.html.erb │ └── users │ ├── index.html.erb │ └── show.html.erb ├── bin ├── bundle ├── rails ├── rake ├── setup └── update ├── config.ru ├── config ├── application.rb ├── autoprefixer.yml ├── boot.rb ├── cable.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── carrierwave.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── devise.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── sass_rails.rb │ ├── session_store.rb │ ├── simple_form.rb │ ├── simple_form_bootstrap.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ ├── devise_invitable.en.yml │ ├── en.yml │ ├── responders.en.yml │ └── simple_form.en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── smtp.yml └── storage.yml ├── db ├── migrate │ ├── 20140410133425_devise_create_users.rb │ ├── 20140410133426_add_name_to_users.rb │ ├── 20140410133428_add_confirmable_to_users.rb │ ├── 20140410133436_devise_invitable_add_to_users.rb │ ├── 20140411155852_create_articles.rb │ ├── 20140411161021_create_comments.rb │ ├── 20150921135616_remove_devise_invitable_from_users.rb │ ├── 20160825090203_add_is_admin_to_users.rb │ ├── 20180402093426_add_image_filename_to_articles.rb │ ├── 20180402135256_add_user_avatar.rb │ ├── 20180412144426_create_categories.rb │ ├── 20180412144437_create_tags.rb │ ├── 20180413083906_add_category_to_article.rb │ └── 20180414211624_create_articles_tags.rb └── seeds.rb ├── docker-compose.production.yml ├── docker-compose.yml ├── lib ├── application_responder.rb ├── assets │ └── .keep ├── tasks │ └── .keep └── templates │ └── erb │ └── scaffold │ └── _form.html.erb ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico └── robots.txt ├── spec ├── factories │ ├── articles.rb │ ├── categories.rb │ ├── comments.rb │ ├── tags.rb │ └── users.rb ├── factories_spec.rb ├── models │ ├── articles_spec.rb │ ├── articles_tag_spec.rb │ ├── category_spec.rb │ ├── comments_spec.rb │ ├── tag_spec.rb │ └── users_spec.rb ├── rails_helper.rb ├── requests │ └── api │ │ └── v1 │ │ ├── articles_spec.rb │ │ ├── auth_spec.rb │ │ ├── categories_spec.rb │ │ ├── comments_spec.rb │ │ └── users_spec.rb ├── spec_helper.rb └── support │ ├── database_cleaner.rb │ ├── email_helper.rb │ ├── factory_girl.rb │ ├── json_spec_helper.rb │ └── request_helper.rb └── vendor └── assets ├── javascripts ├── .keep ├── affix.js ├── alert.js └── util.js └── stylesheets ├── .keep ├── bootstrap-grid.css ├── bootstrap-grid.css.map ├── bootstrap-reboot.css ├── bootstrap-reboot.css.map ├── bootstrap.css └── bootstrap.css.map /.byebug_history: -------------------------------------------------------------------------------- 1 | exit 2 | Article.create(title: FFaker::HipsterIpsum.sentence, text: FFaker::HipsterIpsum.paragraph) 3 | -------------------------------------------------------------------------------- /.deploy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.deploy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: demoapp 3 | description: A Helm chart for Demo Web Application 4 | type: application 5 | version: 0.1.0 6 | appVersion: 1.1.5 # {x-release-please-version} 7 | -------------------------------------------------------------------------------- /.deploy/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "strongqa.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "strongqa.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "strongqa.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "strongqa.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /.deploy/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "demoapp.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "demoapp.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "demoapp.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "demoapp.labels" -}} 37 | helm.sh/chart: {{ include "demoapp.chart" . }} 38 | {{ include "demoapp.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "demoapp.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "demoapp.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "demoapp.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "demoapp.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /.deploy/templates/db-migrate-job.yaml: -------------------------------------------------------------------------------- 1 | {{- $fullName := include "demoapp.fullname" . -}} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ $fullName }}-db-migrate 6 | annotations: 7 | "helm.sh/hook": pre-install,pre-upgrade 8 | "helm.sh/hook-delete-policy": hook-succeeded,hook-failed 9 | "helm.sh/hook-weight": "2" 10 | spec: 11 | template: 12 | spec: 13 | {{- with .Values.imagePullSecrets }} 14 | imagePullSecrets: 15 | {{- toYaml . | nindent 8 }} 16 | {{- end }} 17 | restartPolicy: Never 18 | containers: 19 | - name: {{ $fullName }}-db-migrate-container 20 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 21 | args: 22 | - rails db:migrate 23 | env: 24 | - name: RAILS_ENV 25 | value: "production" 26 | envFrom: 27 | - secretRef: 28 | name: {{ $fullName }} 29 | - configMapRef: 30 | name: {{ $fullName }} 31 | -------------------------------------------------------------------------------- /.deploy/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- $fullName := include "demoapp.fullname" . -}} 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{ include "demoapp.fullname" . }} 6 | labels: 7 | {{- include "demoapp.labels" . | nindent 4 }} 8 | spec: 9 | {{- if not .Values.autoscaling.enabled }} 10 | replicas: {{ .Values.replicaCount }} 11 | {{- end }} 12 | selector: 13 | matchLabels: 14 | {{- include "demoapp.selectorLabels" . | nindent 6 }} 15 | template: 16 | metadata: 17 | annotations: 18 | checksum/rails-env-cm: {{ include (print $.Template.BasePath "/env-cm.yaml") . | sha256sum }} 19 | checksum/rails-env-secret: {{ include (print $.Template.BasePath "/env-secret.yaml") . | sha256sum }} 20 | labels: 21 | {{- include "demoapp.selectorLabels" . | nindent 8 }} 22 | app.kubernetes.io/component: rails 23 | spec: 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | serviceAccountName: {{ include "demoapp.serviceAccountName" . }} 29 | securityContext: 30 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 31 | containers: 32 | - name: {{ .Chart.Name }} 33 | securityContext: 34 | {{- toYaml .Values.securityContext | nindent 12 }} 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | ports: 38 | - name: rails-port 39 | containerPort: 3000 40 | protocol: TCP 41 | # livenessProbe: 42 | # httpGet: 43 | # scheme: HTTP 44 | # path: /health_check/standard 45 | # port: rails-port 46 | # initialDelaySeconds: 120 47 | # timeoutSeconds: 12 48 | # periodSeconds: 60 49 | # failureThreshold: 9 50 | # readinessProbe: 51 | # httpGet: 52 | # scheme: HTTP 53 | # path: /health_check/site 54 | # port: rails-port 55 | envFrom: 56 | - secretRef: 57 | name: {{ $fullName }} 58 | - configMapRef: 59 | name: {{ $fullName }} 60 | resources: 61 | {{- toYaml .Values.rails.resources | nindent 12 }} 62 | {{- with .Values.nodeSelector }} 63 | nodeSelector: 64 | {{- toYaml . | nindent 8 }} 65 | {{- end }} 66 | {{- with .Values.affinity }} 67 | affinity: 68 | {{- toYaml . | nindent 8 }} 69 | {{- end }} 70 | {{- with .Values.tolerations }} 71 | tolerations: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | -------------------------------------------------------------------------------- /.deploy/templates/env-cm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ include "demoapp.fullname" . }} 6 | annotations: 7 | "helm.sh/hook": pre-install,pre-upgrade 8 | "helm.sh/resource-policy": keep 9 | "helm.sh/hook-weight": "1" 10 | labels: 11 | {{- include "demoapp.labels" . | nindent 4 }} 12 | data: 13 | RELEASE_ENV: {{ .Values.releaseEnv | quote }} 14 | IMAGE_TAG: {{ .Values.image.tag | default .Chart.AppVersion | quote }} 15 | ADMIN_EMAIL: {{ .Values.adminEmail | quote }} 16 | AWS_REGION: {{ .Values.aws.region | quote }} 17 | S3_BUCKET: {{ .Values.aws.bucketName | quote }} 18 | SMTP_ADDRESS: {{ .Values.smtp.address | quote }} 19 | SMTP_PORT: {{ .Values.smtp.port | quote }} 20 | SMTP_DEFAULT_URL_HOST: {{ .Values.smtp.defaultURLHost | quote }} 21 | SMTP_DOMAIN: {{ .Values.smtp.domain | quote }} 22 | SMTP_AUTHENTICATION: {{ .Values.smtp.authentication | quote }} 23 | SMTP_ENABLE_STARTTTLS_AUTO: {{ .Values.smtp.enableStartTTLsAuto | quote }} 24 | -------------------------------------------------------------------------------- /.deploy/templates/env-secret.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | {{- $fullName := include "demoapp.fullname" . -}} 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: {{ $fullName }} 7 | annotations: 8 | "helm.sh/hook": pre-install,pre-upgrade 9 | "helm.sh/resource-policy": keep 10 | "helm.sh/hook-weight": "1" 11 | labels: 12 | {{- include "demoapp.labels" . | nindent 4 }} 13 | data: 14 | ADMIN_PASSWORD: {{ .Values.secrets.adminPassword | toString | b64enc }} 15 | AWS_ACCESS_KEY_ID: {{ .Values.secrets.aws.s3.accessKeyId | b64enc }} 16 | AWS_SECRET_ACCESS_KEY: {{ .Values.secrets.aws.s3.secretAccessKey | b64enc }} 17 | AWS_ECR_ACCESS_KEY_ID: {{ .Values.secrets.aws.ecr.accessKeyId | b64enc }} 18 | AWS_ECR_SECRET_ACCESS_KEY: {{ .Values.secrets.aws.ecr.secretAccessKey | b64enc }} 19 | DATABASE_URL: {{ .Values.secrets.database.url | b64enc }} 20 | HOWITZER_TOKEN: {{ .Values.secrets.howitzerToken | toString | b64enc }} 21 | SECRET_KEY_BASE: {{ .Values.secrets.secretKeyBase | toString | b64enc }} 22 | SMTP_USER_NAME: {{ .Values.secrets.smtp.userName | toString | b64enc }} 23 | SMTP_PASSWORD: {{ .Values.secrets.smtp.password | b64enc }} 24 | -------------------------------------------------------------------------------- /.deploy/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "demoapp.fullname" . }} 6 | labels: 7 | {{- include "demoapp.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "demoapp.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /.deploy/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "demoapp.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "demoapp.labels" . | nindent 4 }} 21 | app.kubernetes.io/component: rails 22 | {{- with .Values.ingress.annotations }} 23 | annotations: 24 | {{- toYaml . | nindent 4 }} 25 | {{- end }} 26 | spec: 27 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 28 | ingressClassName: {{ .Values.ingress.className }} 29 | {{- end }} 30 | {{- if .Values.ingress.tls }} 31 | tls: 32 | {{- range .Values.ingress.tls }} 33 | - hosts: 34 | {{- range .hosts }} 35 | - {{ . | quote }} 36 | {{- end }} 37 | secretName: {{ .secretName }} 38 | {{- end }} 39 | {{- end }} 40 | rules: 41 | {{- range .Values.ingress.hosts }} 42 | - host: {{ .host | quote }} 43 | http: 44 | paths: 45 | {{- range .paths }} 46 | - path: {{ .path }} 47 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 48 | pathType: {{ .pathType }} 49 | {{- end }} 50 | backend: 51 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 52 | service: 53 | name: {{ $fullName }} 54 | port: 55 | number: {{ $svcPort }} 56 | {{- else }} 57 | serviceName: {{ $fullName }} 58 | servicePort: {{ $svcPort }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /.deploy/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "demoapp.fullname" . }} 5 | labels: 6 | {{- include "demoapp.labels" . | nindent 4 }} 7 | app.kubernetes.io/component: rails 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: 3000 13 | protocol: TCP 14 | name: rails-port 15 | selector: 16 | {{- include "demoapp.selectorLabels" . | nindent 4 }} 17 | app.kubernetes.io/component: rails -------------------------------------------------------------------------------- /.deploy/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "demoapp.serviceAccountName" . }} 6 | labels: 7 | {{- include "demoapp.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /.deploy/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "demoapp.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "demoapp.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "demoapp.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /.deploy/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for demoapp. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Annotations to add to the service account 21 | annotations: {} 22 | # The name of the service account to use. 23 | # If not set and create is true, a name is generated using the fullname template 24 | name: "" 25 | 26 | podAnnotations: {} 27 | 28 | podSecurityContext: {} 29 | # fsGroup: 2000 30 | 31 | securityContext: {} 32 | # capabilities: 33 | # drop: 34 | # - ALL 35 | # readOnlyRootFilesystem: true 36 | # runAsNonRoot: true 37 | # runAsUser: 1000 38 | 39 | service: 40 | type: ClusterIP 41 | port: 3000 42 | 43 | ingress: 44 | enabled: false 45 | className: "" 46 | annotations: {} 47 | # kubernetes.io/ingress.class: nginx 48 | # kubernetes.io/tls-acme: "true" 49 | hosts: 50 | - host: chart-example.local 51 | paths: 52 | - path: / 53 | pathType: ImplementationSpecific 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: false 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | # targetMemoryUtilizationPercentage: 80 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | 84 | secrets: 85 | database: 86 | url: postgres://user:pass@host:5432/db_name 87 | 88 | 89 | -------------------------------------------------------------------------------- /.deploy/values/production/config.yaml: -------------------------------------------------------------------------------- 1 | ingress: 2 | enabled: true 3 | annotations: 4 | kubernetes.io/ingress.class: nginx 5 | nginx.ingress.kubernetes.io/force-ssl-redirect: "true" 6 | nginx.ingress.kubernetes.io/ssl-passthrough: "true" 7 | nginx.ingress.kubernetes.io/proxy-body-size: '0' 8 | 9 | ingressClassName: nginx 10 | tls: 11 | - hosts: 12 | - demoapp.strongqa.com 13 | secretName: demoapp-tls 14 | 15 | hosts: 16 | - host: 'demoapp.strongqa.com' 17 | paths: 18 | - path: / 19 | pathType: Prefix 20 | 21 | releaseEnv: production 22 | 23 | adminEmail: admin@strongqa.com 24 | aws: 25 | bucketName: demoapp-production-assets 26 | region: eu-central-1 27 | smtp: 28 | address: 'email-smtp.eu-central-1.amazonaws.com' 29 | port: 587 30 | defaultURLHost: 'demoapp.strongqa.com' 31 | domain: 'mail.strongqa.com' 32 | authentication: ':plain' 33 | enableStartTTLsAuto: true 34 | 35 | rails: 36 | resources: 37 | limits: 38 | # cpu: "1" 39 | # memory: 200Mi 40 | requests: 41 | # cpu: 500m 42 | memory: 200Mi 43 | 44 | secrets: 45 | smtp: 46 | user: userName 47 | password: password 48 | -------------------------------------------------------------------------------- /.deploy/values/production/secrets.yaml: -------------------------------------------------------------------------------- 1 | secrets: 2 | adminPassword: ENC[AES256_GCM,data:V0716tF+KpbtiQ==,iv:AF/Mm0YkiDX6Z+5zvEzYeMxsKXMtM07vLmX2GzdZN68=,tag:IjLdosTDtNKXfjXBlDQWFQ==,type:int] 3 | aws: 4 | s3: 5 | accessKeyId: ENC[AES256_GCM,data:0itzIWu5Mig9gvD7,iv:9nn0JKcIWyXaOl/MlE/UG1KbXRzSgOyRMp0E0B1PZbk=,tag:tDmcLLOFCwZAwEZ53IYmWg==,type:str] 6 | secretAccessKey: ENC[AES256_GCM,data:nRn1NvBc/n5+Zh7h,iv:jQPTA3FUxj1z6Ren+kvINoUhvh5+CCPwtAbOyfOq8uk=,tag:tOdHP0d/Q0Y8PSVRcrGm0Q==,type:str] 7 | howitzerToken: ENC[AES256_GCM,data:zE/ysoJS0xXgjGFoiBqU5PeaTmheRWZOJRL/urlpjgdf9NLu/XGz/HYw6w9XWFkeay8=,iv:JG3AUsErAmXOTmjGU8bgD3Cya3+dkKMpJg/M0ms4oFM=,tag:x1iTP4JkFCjVdzAUh5/CKA==,type:str] 8 | secretKeyBase: ENC[AES256_GCM,data:js5/MY6ZNGHOFsZdX21CIWfEVWMVqdfUBsjJ2D16kPUHGC2XnSJipQJ6bZtmcoeQEuvheiGxYSO8J27FNYnl/+tou9WVgJZInwrGWVunR4BFiu1Nr+QXfbv/7lt0JTVzoFHOJ0BFltSOndIKsejuOnTkM4KMeeMrbQm7oZVVyN4=,iv:P6K8+3jvMvAUX4HksCDmsn4eFs+0yrBTMLDRrR1LXgg=,tag:MOOJztU03VjRcr3OU2sv1Q==,type:str] 9 | smtp: 10 | password: ENC[AES256_GCM,data:8H28cz43qC14D1/b,iv:kl+Jka8eIu0tYdLqcEDRCK7cSpvGrK+sWul9RjfXcYw=,tag:3f74mYzoISMehj8rXMQbBQ==,type:str] 11 | userName: ENC[AES256_GCM,data:uhSSSYZIlXI0GX4u,iv:qrHApTrrBIpUP0xklrWlIV7xQI5gFVwso/Ji0jJo0KI=,tag:u6U6XOLAjZYKN56eEu9Khg==,type:str] 12 | sops: 13 | kms: [] 14 | gcp_kms: [] 15 | azure_kv: [] 16 | hc_vault: [] 17 | age: [] 18 | lastmodified: "2023-12-20T14:35:41Z" 19 | mac: ENC[AES256_GCM,data:6/1AqVtWZ0lrgPpUQKiBJtrpY3yVyrABu+V0HaoR3n+Nsb/Z7fLnv2gQUQjpBv5ghQ3MlsCphPbgs28PdDd1vqxiQ/DTz5eIYy26drtxgpAlyiVKgNmZbQrwPjzaTEDzn0kjRFek2xHD6npL7S9RXSFH91WX7+oeddi4G0s5Fk0=,iv:j9HvJLxEsTw7RR0d/VohOt+od4mnmu/Al+JMYJDrWjg=,tag:iuzqH8u4SyjaOtfw1NDY1g==,type:str] 20 | pgp: 21 | - created_at: "2023-09-28T10:01:05Z" 22 | enc: | 23 | -----BEGIN PGP MESSAGE----- 24 | 25 | hQIMA68A2f29GLmEAQ/+Lf05sEXKKmFehXh8BORVtAs3HMHIBpKX2whG4CLtmXvh 26 | kPoji0zgjK/NbnEOlHP4UiAGdw9wi9o0uVHQZ0FLis/00w9SjjzxvAk3t7CVfV13 27 | MUj94oHcj3W1YNSJL5tcM5I6tr1KyEu/Mz3+/D6TMPLtW3xR23mCgGaDuDIajuJ8 28 | /r7QHe818YM4H6njezpvs3muABvKxmaZwXtbxmiUWSh8WkqJ37Pdm/PgFxawQMuh 29 | +frxU3cGIB7KAA2a8J1xf24RnjhG5gYdFtQ+LzeQyfuqkIjnyh3JhbOj7Z+lcx9u 30 | 96z8zWS+rrhPqwzDeHuqFZ/7cWnqhRNhHx7VzeuOQprFROlbsM0ADm5cIAc+tWH8 31 | e6KuFrJEZ7NZYC7BDqMWnFfT9Si6/pw2sEAtz+h6LgmsGrkZDydm+ZcRYEzGnWTD 32 | IgEa9MDgk/7wsjKJ4QdnzeRgsXXVv+KLN0v1gCL4qK2/IbbWKoqX6A2mCJ8Z5H3P 33 | daKwHL/InATwQIUUDkCPZMQcBxayQku04p4Fy8AylVu9CZyfD3yIe4Jnw2cUTrHC 34 | ZKiDy3RRAzBWwlMY2uodpK2KjiY4sNDed7o7xhukdiKX1LljSxHy9D1RGGe4QoEw 35 | ppUVWCsGcwHguqdKdGUE6Eoxnal1lYGTnnjnqznAQPkgR8epDcMtLsjKNotcE2nU 36 | aAEJAhCAaz+xV02RYbB/5W89881IUTeWvdx1FKbSyTcgoHtnU7+6/4y30r1KGTHq 37 | sa9UDG1kJwEn/xvbOlG1M0bzFlO9xb4jQgReXWIVcFWLFnAIL+9YL79PeqTWXjxh 38 | 8F8ZwrI8kkfS 39 | =eeo+ 40 | -----END PGP MESSAGE----- 41 | fp: 9CA169CFCD24D92F0F93206B19C940EFFCE57CA4 42 | unencrypted_suffix: _unencrypted 43 | version: 3.7.3 44 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/** 4 | tmp/** 5 | .DS_Store 6 | public/uploads/** 7 | public/assets/** 8 | public/csvs/** 9 | .tags* 10 | vendor/ruby/** 11 | .powenv 12 | .rvmrc 13 | coverage 14 | .idea 15 | .env 16 | .env.development 17 | .env.staging 18 | .env.production 19 | .env.docker.production 20 | screenshot* 21 | .vagrant 22 | Vagrantfile 23 | .git 24 | -------------------------------------------------------------------------------- /.env.docker: -------------------------------------------------------------------------------- 1 | ADMIN_EMAIL=admin@strongqa.com 2 | ADMIN_PASSWORD=1234567890 3 | AWS_ACCESS_KEY_ID=xxx 4 | AWS_SECRET_ACCESS_KEY=xxx 5 | DATABASE_URL=postgres://postgres:postgres@db:5432/demo_web_app_dev 6 | EMAIL_DEBUG_MODE=true 7 | HOWITZER_TOKEN=97f85fa997125c758a67213c44e1c0543a603f3819b31456b9 8 | SMTP_ADDRESS= 9 | SMTP_PORT= 10 | SMTP_DEFAULT_URL_HOST= 11 | SMTP_DOMAIN= 12 | SMTP_USER_NAME= 13 | SMTP_PASSWORD= 14 | SMTP_AUTHENTICATION= 15 | SMTP_ENABLE_STARTTTLS_AUTO= 16 | SMTP_SSL= 17 | SMTP_TLS= 18 | SOCIAL_ENABLED=true 19 | SECRET_KEY_BASE=2ed67cd64bff279dc467274ba56dc946fbb12ddec7c3b2784546fdaef32eef943eb088ee3f284ac786397100a6028e73e9673f273e86bb7c8afbd027cef59bc7 20 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ADMIN_EMAIL= 2 | ADMIN_PASSWORD= 3 | DATABASE_URL=postgres://user:pass@localhost:5432/demo_web_app_development 4 | SECRET_KEY_BASE= 5 | SMTP_ADDRESS= 6 | SMTP_PORT= 7 | SMTP_DEFAULT_URL_HOST= 8 | SMTP_DOMAIN= 9 | SMTP_USER_NAME= 10 | SMTP_PASSWORD= 11 | SMTP_AUTHENTICATION= 12 | SMTP_ENABLE_STARTTTLS_AUTO= 13 | SMTP_SSL= 14 | SMTP_TLS= 15 | SOCIAL_ENABLED= 16 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://localhost:5432/demo_web_app_test 2 | SMTP_USER_NAME='Super user' 3 | HOWITZER_TOKEN=97f85fa997125c758a67213c44e1c0543a603f3819b31456b9 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | env: 10 | DATABASE_URL: postgres://postgres:postgres@localhost/demo_web_app_test 11 | services: 12 | postgres: 13 | image: postgres:latest 14 | ports: 15 | - 5432:5432 16 | env: 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - name: Set up Ruby environment 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: 3.2.2 28 | bundler-cache: true 29 | 30 | - name: Restore bundle cache 31 | uses: actions/cache@v4.2.0 32 | with: 33 | path: vendor/bundle 34 | key: ${{ runner.os }}-demo-web-app-${{ hashFiles('Gemfile.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-demo-web-app- 37 | 38 | - name: Install dependencies 39 | run: bundle install 40 | 41 | - name: Save bundle cache 42 | uses: actions/cache@v4.2.0 43 | with: 44 | path: vendor/bundle 45 | key: ${{ runner.os }}-demo-web-app-${{ hashFiles('Gemfile.lock') }} 46 | 47 | - name: Check code formatting 48 | run: bundle exec rubocop 49 | 50 | - name: Migrate DB 51 | env: 52 | ADMIN_EMAIL: admin@strongqa.com 53 | ADMIN_PASSWORD: "1234567890" 54 | RAILS_ENV: test 55 | run: bundle exec rake db:create db:migrate db:seed 56 | - name: Run RSpec tests 57 | env: 58 | HOWITZER_TOKEN: 97f85fa997125c758a67213c44e1c0543a603f3819b31456b9 59 | run: | 60 | bundle exec rspec --format RspecJunitFormatter \ 61 | --out tmp/test-results/rspec.xml \ 62 | --color \ 63 | --format Fuubar 64 | 65 | - name: Upload test results 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: test-results 69 | path: tmp/test-results 70 | -------------------------------------------------------------------------------- /.github/workflows/production.yaml: -------------------------------------------------------------------------------- 1 | name: Demo Web App [Production] 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | id-token: write 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | if: ${{ github.ref_type == 'tag' }} 13 | name: Step1 14 | uses: strongsdcom/github-actions/.github/workflows/aws-docker-build.yaml@main 15 | secrets: 16 | build-args: | 17 | GITHUB_SECRET_TOKEN=${{ secrets.READ_GITHUB_PKG_TOKEN }} 18 | SECRET_KEY_BASE=${{ secrets.SECRET_KEY_BASE }} 19 | with: 20 | build-args: | 21 | GITHUB_USER=strongsd-tech 22 | aws-account: ${{ vars.AWS_ACCOUNT_ID_SD_PROD }} 23 | aws-region: ${{ vars.AWS_REGION }} 24 | gha-role-name: gha-ecr-demoapp 25 | environment-name: production 26 | registry: ${{ vars.AWS_ECR_URL_PROD }} 27 | repository: ${{ vars.AWS_ECR_REPO_PROD }} 28 | tags: | 29 | ${{ github.ref_name }} 30 | latest 31 | 32 | deploy: 33 | name: Step2 34 | uses: strongsdcom/github-actions/.github/workflows/linode-lke-deploy.yaml@main 35 | needs: build 36 | secrets: 37 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 38 | linode-token: ${{ secrets.LINODE_TOKEN }} 39 | helm-set-values: | 40 | secrets.database.url=${{ secrets.DATABASE_URL_PROD }} 41 | secrets.smtp.userName=${{ secrets.SMTP_USER_PROD }} 42 | secrets.smtp.password=${{ secrets.SMTP_PASSWORD_PROD }} 43 | secrets.aws.s3.accessKeyId=${{ secrets.AWS_ACCESS_KEY_ID }} 44 | secrets.aws.s3.secretAccessKey=${{ secrets.AWS_SECRET_ACCESS_KEY }} 45 | secrets.aws.ecr.accessKeyId=${{ secrets.AWS_ECR_USERNAME_PROD }} 46 | secrets.aws.ecr.secretAccessKey=${{ secrets.AWS_ECR_PASSWORD_PROD }} 47 | with: 48 | helm-set-values: | 49 | aws.bucketName=${{ vars.AWS_S3_BUCKET }} 50 | aws.region=${{ vars.AWS_REGION }} 51 | cluster-name: ${{ vars.K8S_CLUSTER_NAME_SD_PROD }} 52 | environment-name: production 53 | environment-url: ${{ vars.ENVIRONMENT_URL_PROD }} 54 | helm-values: | 55 | ./.deploy/values/production/config.yaml 56 | ./.deploy/values/production/secrets.yaml 57 | image-tag: ${{ needs.build.outputs.image-tag }} 58 | image-registry: ${{ needs.build.outputs.registry }} 59 | image-repository: ${{ needs.build.outputs.repository }} 60 | namespace: demoapp 61 | release-name: ${{ vars.HELM_RELEASE_NAME_PROD }} 62 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | release-please: 14 | uses: strongsdcom/github-actions/.github/workflows/release-please.yaml@v1.0.0 15 | with: 16 | bump-minor-pre-major: true 17 | bump-patch-for-minor-pre-major: true 18 | extra-files: | 19 | .deploy/Chart.yaml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------- 2 | # Ignore these files when commiting to a git repository. 3 | # 4 | # See http://help.github.com/ignore-files/ for more about ignoring files. 5 | # 6 | # The original version of this file is found here: 7 | # https://github.com/RailsApps/rails-composer/blob/master/files/gitignore.txt 8 | # 9 | # Corrections? Improvements? Create a GitHub issue: 10 | # http://github.com/RailsApps/rails-composer/issues 11 | #---------------------------------------------------------------------------- 12 | 13 | # bundler state 14 | .bundle 15 | vendor/bundle/ 16 | vendor/ruby/ 17 | 18 | # minimal Rails specific artifacts 19 | db/*.sqlite3 20 | db/*.sqlite3-journal 21 | log/* 22 | tmp/* 23 | 24 | # various artifacts 25 | **.war 26 | *.rbc 27 | *.sassc 28 | .rspec 29 | .redcar/ 30 | .sass-cache 31 | config/config.yml 32 | config/database.yml 33 | coverage.data 34 | coverage/ 35 | db/*.javadb/ 36 | db/schema.rb 37 | doc/api/ 38 | doc/app/ 39 | doc/features.html 40 | doc/specs.html 41 | public/cache 42 | public/stylesheets/compiled 43 | public/system/* 44 | public/uploads 45 | spec/tmp/* 46 | cache 47 | capybara* 48 | capybara-*.html 49 | gems 50 | specifications 51 | rerun.txt 52 | pickle-email-*.html 53 | .zeus.sock 54 | 55 | # If you find yourself ignoring temporary files generated by your text editor 56 | # or operating system, you probably want to add a global ignore instead: 57 | # git config --global core.excludesfile ~/.gitignore_global 58 | # 59 | # Here are some files you may want to ignore globally: 60 | 61 | # scm revert files 62 | **.orig 63 | 64 | # Mac finder artifacts 65 | .DS_Store 66 | 67 | # Netbeans project directory 68 | /nbproject/ 69 | 70 | # RubyMine project files 71 | .idea 72 | 73 | # Textmate project files 74 | /*.tmproj 75 | 76 | # vim artifacts 77 | **.swp 78 | 79 | # Environment files that may contain sensitive data 80 | .env 81 | .env.docker.production 82 | .powenv 83 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rails 3 | - rubocop-rspec 4 | - rubocop-factory_bot 5 | 6 | AllCops: 7 | NewCops: enable 8 | TargetRubyVersion: 3.2.2 9 | Exclude: 10 | - bin/**/* 11 | - db/**/* 12 | - log/**/* 13 | - public/**/* 14 | - vendor/**/* 15 | 16 | Rails/ApplicationController: 17 | Exclude: 18 | - app/controllers/api/v1/base_controller.rb 19 | 20 | Style/LineLength: 21 | Max: 120 22 | 23 | Layout/CaseIndentation: 24 | Enabled: false 25 | 26 | Style/EmptyElse: 27 | Enabled: false 28 | 29 | Lint/AmbiguousRegexpLiteral: 30 | Enabled: false 31 | 32 | Style/CaseEquality: 33 | Enabled: false 34 | 35 | Style/Documentation: 36 | Enabled: false 37 | 38 | Style/FrozenStringLiteralComment: 39 | Enabled: false 40 | 41 | Naming/FileName: 42 | Exclude: 43 | - Capfile 44 | 45 | Naming/MemoizedInstanceVariableName: 46 | Enabled: false 47 | 48 | Rails/OutputSafety: 49 | Enabled: false 50 | 51 | Metrics/BlockLength: 52 | Exclude: 53 | - spec/**/* 54 | - config/**/* 55 | 56 | Naming/VariableNumber: 57 | Enabled: false 58 | 59 | Rails/I18nLocaleTexts: 60 | Enabled: false 61 | 62 | RSpec/LetSetup: 63 | Enabled: false 64 | 65 | RSpec/IndexedLet: 66 | Enabled: false 67 | 68 | RSpec/NestedGroups: 69 | Enabled: false 70 | 71 | RSpec/MultipleExpectations: 72 | Enabled: false 73 | 74 | RSpec/ContextWording: 75 | Enabled: false 76 | 77 | RSpec/NamedSubject: 78 | Enabled: false 79 | 80 | RSpec/RepeatedExampleGroupBody: 81 | Enabled: false 82 | 83 | RSpec/DescribeClass: 84 | Enabled: false 85 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | demo_web_app -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.2.2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.5](https://github.com/strongqa/demo_web_app/compare/v1.1.4...v1.1.5) (2024-06-17) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * change TLS name ([0a7598a](https://github.com/strongqa/demo_web_app/commit/0a7598afad68dafc06cda7a8ece21ab3f8b82f2a)) 9 | 10 | ## [1.1.4](https://github.com/strongqa/demo_web_app/compare/v1.1.3...v1.1.4) (2024-01-18) 11 | 12 | 13 | ### Miscellaneous Chores 14 | 15 | * release 1.1.4 ([9d99a1d](https://github.com/strongqa/demo_web_app/commit/9d99a1d13be37c68d4644d5be5854b161f6dc450)) 16 | 17 | ## [1.1.3](https://github.com/strongqa/demo_web_app/compare/v1.1.2...v1.1.3) (2023-12-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * use placeholders in secrets for infra secrets ([cc0d1bd](https://github.com/strongqa/demo_web_app/commit/cc0d1bd76804c6cc2470017a6e1344b74f48266b)) 23 | 24 | ## [1.1.2](https://github.com/strongqa/demo_web_app/compare/v1.1.1...v1.1.2) (2023-12-20) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * add s3 credentials ([250f5f8](https://github.com/strongqa/demo_web_app/commit/250f5f8ea12f11e53fe7ea2daea411a2b2ce38aa)) 30 | 31 | ## [1.1.1](https://github.com/strongqa/demo_web_app/compare/v1.1.0...v1.1.1) (2023-12-20) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * resolve found issues ([fe3db12](https://github.com/strongqa/demo_web_app/commit/fe3db120e5026b49299ef2643a837187eea4428e)) 37 | 38 | ## [1.1.0](https://github.com/strongqa/demo_web_app/compare/v1.0.4...v1.1.0) (2023-12-20) 39 | 40 | 41 | ### Features 42 | 43 | * Upload to S3 in prod. ([#82](https://github.com/strongqa/demo_web_app/issues/82)) ([83edcf3](https://github.com/strongqa/demo_web_app/commit/83edcf3a3ba0eb2488428274f9e7ad19aeb95435)) 44 | 45 | ## [1.0.4](https://github.com/strongqa/demo_web_app/compare/v1.0.3...v1.0.4) (2023-11-28) 46 | 47 | 48 | ### Miscellaneous Chores 49 | 50 | * release 1.0.4 ([993df67](https://github.com/strongqa/demo_web_app/commit/993df6719234dcee462d2a995b0c287c488b3e7e)) 51 | 52 | ## [1.0.3](https://github.com/strongqa/demo_web_app/compare/v1.0.2...v1.0.3) (2023-11-28) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * upgrade devise ([943d0dd](https://github.com/strongqa/demo_web_app/commit/943d0dde16be24c68d79a86394e26909d70b5cb7)) 58 | 59 | ## [1.0.2](https://github.com/strongqa/demo_web_app/compare/v1.0.1...v1.0.2) (2023-11-28) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * deployment issues ([#77](https://github.com/strongqa/demo_web_app/issues/77)) ([5d9679d](https://github.com/strongqa/demo_web_app/commit/5d9679d43c248208d7f272df9b5eb1055280ccc3)) 65 | 66 | ## [1.0.1](https://github.com/strongqa/demo_web_app/compare/v1.0.0...v1.0.1) (2023-11-28) 67 | 68 | 69 | ### Miscellaneous Chores 70 | 71 | * release 1.0.1 ([2218afe](https://github.com/strongqa/demo_web_app/commit/2218afe337f55bf201f839d4b17363c3b82600e8)) 72 | 73 | ## 1.0.0 (2023-11-24) 74 | 75 | 76 | ### Miscellaneous Chores 77 | 78 | * release 1.0.0 ([57e5a91](https://github.com/strongqa/demo_web_app/commit/57e5a91d0907c9014c8ddf59b72163ad7b95a925)) 79 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.2.2-alpine 2 | LABEL vendor="StrongQA" 3 | 4 | RUN apk --update add build-base nodejs tzdata libxslt-dev libxml2-dev imagemagick postgresql-dev postgresql-client gcompat 5 | 6 | ENV INSTALL_PATH /demo_web_app 7 | RUN mkdir -p $INSTALL_PATH 8 | WORKDIR $INSTALL_PATH 9 | 10 | COPY Gemfile Gemfile.lock ./ 11 | 12 | RUN bundle check || bundle install --jobs 4 --binstubs --without test development 13 | 14 | ENV DATABASE_URL=postgres://@localhost:5432/demo_web_app_production 15 | ENV RACK_ENV production 16 | ENV RAILS_ENV production 17 | ENV RAILS_SERVE_STATIC_FILES="true" 18 | ENV SECRET_KEY_BASE pickasecuretoken 19 | ENV AWS_ACCESS_KEY_ID xxx 20 | ENV AWS_SECRET_ACCESS_KEY xxx 21 | 22 | COPY . . 23 | 24 | RUN bundle exec rake assets:precompile 25 | 26 | VOLUME ["$INSTALL_PATH/public"] 27 | EXPOSE 3000 28 | # Provide a Healthcheck for Docker risk mitigation 29 | # HEALTHCHECK --interval=3600s --timeout=20s --retries=2 CMD curl http://localhost:3000 || exit 1 30 | 31 | ENTRYPOINT ["bundle", "exec"] 32 | CMD [ "rails", "server"] 33 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM ruby:3.2.2-alpine 2 | LABEL vendor="StrongQA" 3 | 4 | RUN apk --update add build-base nodejs tzdata libxslt-dev libxml2-dev imagemagick postgresql-dev postgresql-client gcompat 5 | 6 | ENV INSTALL_PATH /demo_web_app 7 | RUN mkdir -p $INSTALL_PATH 8 | WORKDIR $INSTALL_PATH 9 | 10 | COPY Gemfile Gemfile.lock ./ 11 | 12 | RUN bundle check || bundle install --jobs 4 --binstubs --without test production 13 | 14 | COPY . . 15 | VOLUME ["$INSTALL_PATH/public"] 16 | EXPOSE 3000 17 | # Provide a Healthcheck for Docker risk mitigation 18 | # HEALTHCHECK --interval=3600s --timeout=20s --retries=2 CMD curl http://localhost:3000 || exit 1 19 | ENTRYPOINT ["bundle", "exec"] 20 | CMD [ "rails", "server"] 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '3.2.2' 3 | gem 'active_model_serializers' 4 | gem 'autoprefixer-rails' 5 | gem 'bootstrap-sass' 6 | gem 'breadcrumbs_on_rails' 7 | gem 'carrierwave', '~> 1.3' 8 | gem 'coffee-rails' 9 | gem 'devise' 10 | gem 'dotenv-rails' 11 | gem 'ffaker' 12 | gem 'fog-aws' 13 | gem 'font-awesome-rails' 14 | gem 'globalid', '~> 1.0' 15 | gem 'jbuilder' 16 | gem 'jquery-rails' 17 | gem 'kaminari' 18 | gem 'pg' 19 | gem 'puma', '~> 5' 20 | gem 'rails', '~> 6' 21 | gem 'responders' 22 | gem 'sass-rails' 23 | gem 'simple_form' 24 | gem 'turbolinks' 25 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw] 26 | gem 'uglifier' 27 | 28 | group :development do 29 | gem 'bcrypt_pbkdf' 30 | gem 'better_errors' 31 | gem 'binding_of_caller', platforms: %i[mri_19 mri_20 mri_21 rbx] 32 | gem 'ed25519' 33 | gem 'letter_opener' 34 | gem 'net-ssh' 35 | gem 'pry' 36 | gem 'pry-byebug' 37 | gem 'rails_layout' 38 | end 39 | 40 | group :development, :test do 41 | gem 'factory_bot_rails' 42 | gem 'fuubar' 43 | gem 'rspec-rails', '~> 3.7' 44 | gem 'rubocop' 45 | gem 'rubocop-factory_bot', require: false 46 | gem 'rubocop-rails', require: false 47 | gem 'rubocop-rspec', require: false 48 | end 49 | 50 | group :test do 51 | gem 'database_cleaner' 52 | gem 'json_spec' 53 | gem 'rspec-its' 54 | gem 'rspec_junit_formatter' 55 | gem 'simplecov' 56 | gem 'timecop' 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple demo web application 2 | 3 | ## Installation 4 | 5 | ``` 6 | bin/setup 7 | bin/rails s 8 | ``` 9 | 10 | ## Run development environment locally on docker 11 | 12 | ``` 13 | docker-compose build 14 | docker-compose run --rm web rake db:create db:migrate db:seed 15 | docker-compose up 16 | ``` 17 | 18 | ## Run production environment locally on docker 19 | 20 | ``` 21 | docker-compose -f docker-compose.production.yml build 22 | docker-compose -f docker-compose.production.yml up 23 | ``` 24 | 25 | Useful commands: 26 | 27 | ``` 28 | docker-compose -f docker-compose.production.yml exec web rake db:create db:migrate db:seed 29 | docker-compose -f docker-compose.production.yml exec web rake assets:precompile 30 | docker-compose -f docker-compose.production.yml stop 31 | ``` 32 | 33 | ## DEMO SERVER 34 | 35 | http://demoapp.strongqa.com 36 | 37 | ``` 38 | Default admin email: admin@strongqa.com 39 | Default password: 1234567890 40 | ``` 41 | 42 | _Note_: all test data, except 3 test articles and 5 demo users are cleaning up every day at 00:00 UTC 43 | 44 | ## API 45 | 46 | curl 'http://demoapp.strongqa.com/api/v1/articles' -H 'Authorization: Token token="97f85fa997125c758a67213c44e1c0543a603f3819b31456b9"' 47 | 48 | ## Contribution 49 | 50 | [![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/0)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/0)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/1)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/1)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/2)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/2)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/3)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/3)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/4)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/4)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/5)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/5)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/6)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/6)[![](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/images/7)](https://sourcerer.io/fame/romikoops/strongqa/demo_web_app/links/7) 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require File.expand_path('config/application', __dir__) 2 | 3 | Rails.application.load_tasks 4 | 5 | begin 6 | require 'rubocop/rake_task' 7 | RuboCop::RakeTask.new do |task| 8 | task.requires << 'rubocop-rails' 9 | task.requires << 'rubocop-rspec' 10 | task.requires << 'rubocop-factory_bot' 11 | end 12 | rescue LoadError 13 | nil 14 | end 15 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .sass 4 | //= link application.css 5 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/articles/fallback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/assets/images/articles/fallback.jpg -------------------------------------------------------------------------------- /app/assets/images/logo-pic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/assets/images/users/defaut_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/assets/images/users/defaut_avatar.png -------------------------------------------------------------------------------- /app/assets/javascripts/alert_custom.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $(".alert-success").each(function () { 3 | var $this = $(this); 4 | var timeout = $this.data('timeout'); 5 | var timer; 6 | window.clearTimeout(timer); 7 | timer = window.setTimeout(function () { 8 | $this.fadeOut(); 9 | setTimeout(function () {$this.remove()}, 3000); 10 | },timeout); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require util 16 | //= require alert 17 | //= require affix 18 | //= require_tree . 19 | 20 | //= require_tree ./widgets 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/navigation.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | function setMenuHeight() { 3 | var navWrapper = $('.navigation__wrapper'); 4 | if ($(window).width() < 992) { 5 | var navHeight = $('.navigation').height(); 6 | navWrapper.css('height', window.innerHeight - navHeight).css('top', navHeight); 7 | } else { 8 | navWrapper.css('height', 'auto').css('top', 'auto'); 9 | } 10 | } 11 | 12 | $('.navigation__button').click(function() { 13 | $('body').toggleClass('closed opened'); 14 | setMenuHeight(); 15 | }); 16 | 17 | $(window).resize(function () { 18 | setTimeout(function () { 19 | setMenuHeight(); 20 | }, 300); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/widget.js.coffee: -------------------------------------------------------------------------------- 1 | window.Widgets = {} 2 | window.Widget = 3 | init: (el)-> 4 | el.find('[data-widget]:not(.initialized)').each (i, widget_container) -> 5 | element = $(widget_container) 6 | klassName = element.data('widget') 7 | klass = window[klassName] 8 | if klass 9 | Widgets[klassName] = new klass(element) 10 | else 11 | console.error("Widget '#{klassName}' is not defined!") 12 | element.addClass('initialized') 13 | el.addClass('initialized') 14 | $ -> 15 | Widget.init($(document)) 16 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/phone.js.coffee: -------------------------------------------------------------------------------- 1 | class window.PhoneOrientation 2 | constructor: (el) -> 3 | setPhoneOrientation = -> 4 | @el = el 5 | windowWidth = $(window).width() 6 | if windowWidth < 1200 and windowWidth > 767 7 | @el.addClass 'landscape' 8 | else 9 | @el.removeClass 'landscape' 10 | return 11 | 12 | setPhoneOrientation() 13 | $(window).resize -> 14 | setTimeout (-> 15 | setPhoneOrientation() 16 | return 17 | ), 100 18 | return 19 | return -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/scroll_sidebr.js.coffee: -------------------------------------------------------------------------------- 1 | class window.ScrollSidebar 2 | constructor: (el) -> 3 | $window = $(window) 4 | floatingElement = el 5 | 6 | NexToFloatingElement = $(floatingElement.data('next')) 7 | prevToFloatingElement = $(floatingElement.data('prev')) 8 | elementOffset = floatingElement.data('element-offset') 9 | floatingElement.addClass('floating') 10 | floatingSidebar = -> 11 | floatingElement.css 'width', floatingElement.parent().width() 12 | if $window.width() > 1199 13 | floatingElement.affix offset: 14 | top: $(prevToFloatingElement).offset().top + $(prevToFloatingElement).outerHeight() - elementOffset 15 | bottom: NexToFloatingElement.outerHeight(true) 16 | else 17 | floatingElement.removeClass 'affix affix-top affix-bottom' 18 | return 19 | 20 | floatingSidebar() 21 | $window.resize -> 22 | setTimeout (-> 23 | floatingSidebar() 24 | return 25 | ), 100 26 | return 27 | return 28 | -------------------------------------------------------------------------------- /app/assets/javascripts/widgets/sticky_nav.js.coffee: -------------------------------------------------------------------------------- 1 | class window.StickyNav 2 | constructor: (el) -> 3 | $(window).on 'scroll', -> 4 | @el = el 5 | navbarOffset = @el.offset() 6 | if navbarOffset.top >= @el.data('offset') 7 | @el.addClass 'sticky' 8 | else 9 | @el.removeClass 'sticky' 10 | return 11 | return 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.sass: -------------------------------------------------------------------------------- 1 | // core 2 | @import bootstrap 3 | @import font-awesome 4 | @import general/vars 5 | @import general/mixin 6 | @import general/extends 7 | @import general/btn 8 | @import general/general 9 | @import general/form 10 | 11 | // layout 12 | @import layout/floating 13 | @import layout/wrapper 14 | @import layout/logo 15 | @import layout/navigation 16 | @import layout/navigation_opened 17 | @import layout/hero 18 | @import layout/breadcrumb 19 | @import layout/pagination 20 | @import layout/footer 21 | @import layout/admin 22 | @import layout/form 23 | @import layout/sidebar 24 | @import layout/list 25 | @import layout/recent_posts 26 | @import layout/tags 27 | @import layout/sticky 28 | @import layout/authform 29 | @import layout/users 30 | @import layout/alert 31 | @import layout/comments 32 | @import layout/phone 33 | @import layout/courses-wrap 34 | 35 | // articles 36 | @import article/article 37 | @import article/article_page 38 | 39 | @import iphone 40 | -------------------------------------------------------------------------------- /app/assets/stylesheets/article/_article.sass: -------------------------------------------------------------------------------- 1 | .article 2 | &__item + &__item 3 | padding-top: 30px 4 | border-top: 1px solid $semi-white 5 | 6 | &__item 7 | padding-bottom: 30px 8 | 9 | .admin__wrapper 10 | margin-top: 30px 11 | margin-bottom: 0 12 | 13 | &__img 14 | margin-bottom: 15px 15 | display: block 16 | 17 | img 18 | max-width: 100% 19 | display: block 20 | margin: 0 auto 21 | transition: .5s 22 | 23 | @include haf 24 | img 25 | opacity: .8 26 | 27 | &__desc 28 | overflow: hidden 29 | display: flex 30 | 31 | &__date 32 | float: left 33 | padding: 13px 0 34 | margin-right: 30px 35 | width: 70px 36 | min-width: 70px 37 | height: 70px 38 | border: 3px solid $dark 39 | font-family: $heading 40 | color: $grey 41 | font-weight: 700 42 | text-align: center 43 | 44 | span 45 | display: block 46 | 47 | &:first-child 48 | font-size: 21px 49 | line-height: 1 50 | 51 | &:last-child 52 | font-size: 13px 53 | text-transform: uppercase 54 | 55 | &__title 56 | font-size: 16px 57 | color: $dark 58 | 59 | @include haf 60 | color: inherit 61 | 62 | &__meta 63 | overflow: hidden 64 | padding: 0 65 | 66 | li 67 | display: block 68 | float: left 69 | font-size: 13px 70 | color: $grey-light 71 | margin-bottom: 7px 72 | white-space: nowrap 73 | 74 | + li 75 | &:before 76 | content: "|" 77 | opacity: 0.5 78 | margin: 0 7px 79 | 80 | a 81 | color: $grey-light 82 | 83 | @include haf 84 | color: $dark 85 | 86 | &__text 87 | margin-top: 15px 88 | text-align: justify 89 | 90 | &__readmore 91 | font-size: 12px 92 | font-family: $heading 93 | text-transform: uppercase 94 | letter-spacing: 0.02em 95 | font-weight: 700 96 | color: $dark 97 | 98 | &:after 99 | @extend .font-awesome 100 | content: '\f178' 101 | margin-left: 7px 102 | font-weight: normal 103 | 104 | &__footer 105 | margin: 30px 0 106 | 107 | &_title 108 | margin-right: 7px 109 | margin-bottom: 0 110 | display: inline-block 111 | 112 | a 113 | font-style: italic 114 | font-size: 14px 115 | line-height: 1.2 116 | color: $grey 117 | 118 | &__social 119 | margin-top: 10px 120 | 121 | a 122 | display: inline-block 123 | width: 32px 124 | height: 32px 125 | line-height: 32px 126 | 127 | i 128 | line-height: 32px 129 | color: $grey-light 130 | font-size: 14px 131 | font-style: normal 132 | width: 32px 133 | height: 32px 134 | border-radius: 50% 135 | text-align: center 136 | transition: all 0.2s linear 137 | 138 | @include haf 139 | i 140 | background-color: $brown 141 | color: white 142 | 143 | &__group 144 | display: flex 145 | padding: 15px 0 146 | 147 | &_title 148 | border-left: 1px solid $semi-white 149 | padding-left: 14px 150 | 151 | &_heading + &_heading 152 | margin-top: 15px 153 | 154 | @media screen and (min-width: 400px) 155 | .article 156 | &__title 157 | font-size: 21px 158 | 159 | @media screen and (min-width: 576px) 160 | .article 161 | &_title 162 | padding-left: 28px 163 | 164 | &__text 165 | margin-top: 30px 166 | 167 | @media screen and (min-width: 768px) 168 | .article 169 | &__img 170 | margin-bottom: 30px 171 | 172 | &__title 173 | margin-top: 30px 174 | 175 | &__social 176 | float: right 177 | margin-top: 0 178 | 179 | @media screen and (min-width: 1200px) 180 | .article 181 | &__home 182 | min-height: 650px 183 | -------------------------------------------------------------------------------- /app/assets/stylesheets/article/_article_page.sass: -------------------------------------------------------------------------------- 1 | .article__page 2 | &_desc 3 | padding-bottom: 15px 4 | display: flex 5 | flex-direction: column 6 | align-items: center 7 | 8 | &:after 9 | content: '' 10 | display: block 11 | width: 48px 12 | border-bottom: 3px solid $semi-white 13 | margin: 15px auto 15px 14 | 15 | &_title 16 | text-align: center 17 | font-size: 18px 18 | margin-bottom: 10px 19 | 20 | &_content 21 | p 22 | &:first-child:first-letter 23 | float: left 24 | color: #333 25 | font-size: 52px 26 | line-height: 46px 27 | padding-top: 4px 28 | padding-right: 10px 29 | 30 | @media screen and (min-width: 400px) 31 | .article__page 32 | &_title 33 | font-size: 20px 34 | 35 | @media screen and (min-width: 576px) 36 | .article__page 37 | &_desc 38 | padding-bottom: 30px 39 | 40 | &_title 41 | font-size: 26px 42 | margin-top: 15px 43 | 44 | @media screen and (min-width: 768px) 45 | .article__page 46 | &_title 47 | margin-top: 30px 48 | font-size: 28px 49 | #error_explanation 50 | @extend .alert 51 | @extend .alert-danger 52 | 53 | #error_explanation h2 54 | font-size: 16px 55 | -------------------------------------------------------------------------------- /app/assets/stylesheets/framework_and_overrides.sass: -------------------------------------------------------------------------------- 1 | // import the CSS framework 2 | //@import "bootstrap-sprockets" 3 | @import bootstrap 4 | 5 | // make all images responsive by default 6 | img 7 | //@extend .img-responsive 8 | margin: 0 auto 9 | 10 | // override for the 'Home' navigation link 11 | .navbar-brand 12 | font-size: inherit 13 | 14 | 15 | // THESE ARE EXAMPLES YOU CAN MODIFY 16 | // create your own classes 17 | // to make views framework-neutral 18 | .column 19 | @extend .col-md-6 20 | //@extend .text-center 21 | 22 | .form 23 | @extend .col-md-6 24 | 25 | .form-centered 26 | @extend .col-md-6 27 | //@extend .text-center 28 | 29 | .submit 30 | @extend .btn 31 | @extend .btn-primary 32 | @extend .btn-lg 33 | 34 | // apply styles to HTML elements 35 | // to make views framework-neutral 36 | main 37 | @extend .container 38 | background-color: #eee 39 | padding-bottom: 80px 40 | width: 100% 41 | margin-top: 51px // accommodate the navbar 42 | 43 | section 44 | @extend .row 45 | margin-top: 20px 46 | 47 | 48 | // Styles for Devise views 49 | // using Bootstrap 50 | // generated by the rails_layout gem 51 | .authform 52 | padding-top: 30px 53 | max-width: 320px 54 | margin: 0 auto 55 | 56 | .authform form 57 | //@extend .well 58 | //@extend .well-lg 59 | padding-bottom: 40px 60 | 61 | .authform .right 62 | float: right !important 63 | 64 | .authform .button 65 | @extend .btn 66 | @extend .btn-primary 67 | 68 | .authform fieldset 69 | //@extend .well 70 | 71 | #error_explanation 72 | @extend .alert 73 | @extend .alert-danger 74 | 75 | #error_explanation h2 76 | font-size: 16px 77 | 78 | .button-xs 79 | @extend .btn 80 | @extend .btn-primary 81 | //@extend .btn-xs 82 | 83 | 84 | .admin-user-title 85 | color: red 86 | display: block 87 | padding: 15px 16px 88 | 89 | .checkbox 90 | display: inline-block 91 | input[type=checkbox] 92 | cursor: pointer 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_btn.sass: -------------------------------------------------------------------------------- 1 | .btn 2 | font-family: $heading 3 | font-weight: 700 4 | text-transform: uppercase 5 | text-decoration: none 6 | text-align: center 7 | letter-spacing: 0.02em 8 | border-radius: 0 9 | border: none 10 | transition: all 0.3s ease-in-out 11 | color: white 12 | backface-visibility: hidden 13 | outline: none 14 | padding: 10px 15px 15 | font-size: 14px 16 | 17 | @include haf 18 | color: white 19 | 20 | &__underline 21 | padding-bottom: 5px 22 | 23 | &__big 24 | width: 100% 25 | 26 | &__form 27 | margin-top: 15px 28 | background-color: $brown 29 | 30 | @include haf 31 | background-color: $dark 32 | 33 | &__icon 34 | &:before 35 | @extend .font-awesome 36 | content: '' 37 | position: relative 38 | padding-right: .4em 39 | top: -.06em 40 | 41 | &__create 42 | &:before 43 | content: '\f067' 44 | top: 0 45 | 46 | &__edit 47 | &:before 48 | content: '\f040' 49 | 50 | &__delete 51 | &:before 52 | content: '\f1f8' 53 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_extends.sass: -------------------------------------------------------------------------------- 1 | .font-awesome 2 | font-family: $fa 3 | font-size: 1em 4 | font-weight: inherit 5 | line-height: 1em 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_form.sass: -------------------------------------------------------------------------------- 1 | .form-control 2 | position: relative 3 | display: block 4 | line-height: 1.2 5 | border: 2px solid $semi-white 6 | border-radius: 0 7 | background-color: white 8 | width: 100% 9 | margin-bottom: 16px 10 | padding: 8px 16px 11 | color: $dark 12 | transition: $transition-base 13 | -moz-appearance: none 14 | -webkit-appearance: none 15 | background-clip: unset 16 | 17 | @include haf 18 | outline: none 19 | box-shadow: none !important 20 | color: $dark 21 | 22 | &:focus 23 | background-color: white !important 24 | border-color: $brown 25 | 26 | input 27 | &[type="text"], 28 | &[type="password"], 29 | &[type="email"], 30 | &[type="url"], 31 | &[type="tel"], 32 | &[type="number"], 33 | &[type="date"], 34 | &[type="search"] 35 | @extend .form-control 36 | height: 47px 37 | 38 | select, 39 | textarea 40 | @extend .form-control 41 | 42 | textarea 43 | padding: 16px 44 | resize: none 45 | 46 | input, 47 | textarea, 48 | select 49 | &:-webkit-autofill 50 | background-color: white !important 51 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_general.sass: -------------------------------------------------------------------------------- 1 | body 2 | font-family: $text 3 | display: flex 4 | min-height: 100vh 5 | flex-direction: column 6 | 7 | main 8 | flex-grow: 1 9 | 10 | h1,h2,h3,h4,h5,h6 11 | font-family: $heading 12 | font-weight: 700 13 | margin-top: 0 14 | text-transform: uppercase 15 | color: #111 16 | letter-spacing: 0.05em 17 | font-size: 16px 18 | 19 | p 20 | font-size: 15px 21 | color: #7a7a7a 22 | font-weight: normal 23 | line-height: 25px 24 | 25 | a 26 | color: $brown 27 | text-shadow: 0 0 0 rgba(black, .8) 28 | text-decoration: none 29 | transition: all 0.3s ease-in-out 30 | 31 | @include haf 32 | text-decoration: inherit 33 | color: $dark 34 | 35 | ul, 36 | ol 37 | margin: 0 38 | padding: 0 39 | list-style: none 40 | 41 | p 42 | text-align: justify 43 | 44 | * 45 | @include haf 46 | outline: none 47 | 48 | @media screen and (min-width: 400px) 49 | h1 50 | font-size: 28px 51 | h2 52 | font-size: 24px 53 | h3 54 | font-size: 21px 55 | h4 56 | font-size: 20px 57 | h5 58 | font-size: 18px 59 | h6 60 | font-size: 15px 61 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_mixin.sass: -------------------------------------------------------------------------------- 1 | @mixin haf 2 | &:hover, 3 | &:active, 4 | &:focus 5 | @content 6 | 7 | &.active 8 | @content 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/general/_vars.sass: -------------------------------------------------------------------------------- 1 | // fonts 2 | $heading: "Montserrat", sans-serif 3 | $text: "Pt Serif", serif 4 | $fa: 'FontAwesome' 5 | 6 | // colors 7 | $dark: #111111 8 | $grey: #3b3b3b 9 | $grey-light: #919191 10 | $semi-white: #e5e5e5 11 | $semi-white-light: #f7f7f7 12 | $brown: #bfa67a 13 | $blue: #1f2841 14 | 15 | $transition-base: all .3s ease-in-out 16 | 17 | // navigation 18 | $nav-height: 90px 19 | $nav-height-sticky: 60px 20 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_admin.sass: -------------------------------------------------------------------------------- 1 | .admin 2 | &__wrapper 3 | padding: 10px 4 | margin-bottom: 30px 5 | background-color: $semi-white 6 | border: 2px dashed $brown 7 | position: relative 8 | 9 | &:before 10 | @extend .font-awesome 11 | 12 | .btn 13 | margin: 5px 14 | 15 | + .article__item 16 | padding-top: 30px 17 | border-top: 1px solid $semi-white 18 | 19 | @media screen and (min-width: 576px) 20 | .admin 21 | &__wrapper 22 | padding: 10px 20px 10px 55px 23 | 24 | &:before 25 | content: '\f13e' 26 | position: absolute 27 | color: $grey-light 28 | font-size: 30px 29 | left: 20px 30 | margin-top: 11px 31 | 32 | @media screen and (min-width: 768px) 33 | .admin 34 | &__wrapper 35 | padding: 25px 30px 25px 80px 36 | 37 | &:before 38 | font-size: 40px 39 | left: 30px 40 | margin-top: 6px 41 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_alert.sass: -------------------------------------------------------------------------------- 1 | .alert 2 | text-align: center 3 | 4 | &__wrapper 5 | display: block 6 | position: absolute 7 | top: 70px 8 | width: 260px 9 | height: auto 10 | z-index: 99 11 | left: 50% 12 | transform: translatex(-50%) 13 | 14 | &-dismissible 15 | padding-right: 3em 16 | 17 | .devise 18 | .alert__wrapper 19 | position: relative 20 | top: 10px 21 | 22 | @media screen and (min-width: 460px) 23 | .alert 24 | &__wrapper 25 | width: auto 26 | min-width: 320px 27 | max-width: 80% 28 | 29 | @media screen and (min-width: 992px) 30 | .alert 31 | &__wrapper 32 | top: 100px 33 | 34 | @media screen and (min-width: 1199px) 35 | .alert__wrapper 36 | max-width: 1140px 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_authform.sass: -------------------------------------------------------------------------------- 1 | .authform 2 | color: $grey 3 | 4 | a 5 | color: $brown 6 | text-shadow: 0 0 0 rgba(black, 0.8) 7 | 8 | p 9 | color: $grey 10 | margin-top: 15px 11 | margin-bottom: 5px 12 | line-height: 1.2 13 | 14 | h3 15 | margin: 30px 0 15px 16 | text-align: center 17 | 18 | &__wrapper 19 | max-width: 430px 20 | margin: 0 auto 21 | padding-bottom: 60px 22 | 23 | &__logo 24 | height: 70px 25 | position: relative 26 | text-align: center 27 | 28 | svg 29 | height: 100% 30 | 31 | &__title 32 | text-align: center 33 | margin: 15px 0 40px 34 | 35 | &__checkbox 36 | label 37 | display: inline-block 38 | font-size: 16px 39 | position: relative 40 | margin-top: 10px 41 | cursor: pointer 42 | padding-left: 1.2em 43 | 44 | &:before 45 | @extend .font-awesome 46 | content: '\f096' 47 | position: absolute 48 | left: 0 49 | top: .3em 50 | font-size: 1em 51 | padding-right: .5em 52 | transition: $transition-base 53 | 54 | input[type=checkbox] 55 | display: none 56 | 57 | &:checked + label 58 | &:before 59 | content: '\f046' 60 | 61 | &__forgot-password, 62 | &__sign-up 63 | margin-top: 40px 64 | display: block 65 | color: $grey 66 | text-align: center 67 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_breadcrumb.sass: -------------------------------------------------------------------------------- 1 | .breadcrumb 2 | background: none 3 | padding: 0 4 | text-align: center 5 | display: inherit 6 | 7 | li 8 | display: inline-block 9 | color: white 10 | position: relative 11 | 12 | a 13 | color: white 14 | 15 | + li 16 | &:before 17 | position: relative 18 | content: "/" 19 | padding: 0 5px 20 | font-size: 1em 21 | font-weight: 700 22 | opacity: 0.5 23 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_courses-wrap.sass: -------------------------------------------------------------------------------- 1 | .courses-wrap 2 | margin-top: 110px -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_floating.sass: -------------------------------------------------------------------------------- 1 | .affix 2 | position: static 3 | 4 | .floating 5 | overflow: hidden 6 | display: block 7 | 8 | &.affix-top 9 | position: static 10 | 11 | &.affix 12 | top: 120px 13 | 14 | 15 | &.affix-bottom 16 | position: absolute 17 | 18 | @media screen and (min-width: 1200px) 19 | .affix 20 | position: fixed 21 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_footer.sass: -------------------------------------------------------------------------------- 1 | .footer 2 | background-color: $dark 3 | color: white 4 | padding: 45px 0 5 | text-align: center 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_form.sass: -------------------------------------------------------------------------------- 1 | .form 2 | &__textarea 3 | resize: none 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_hero.sass: -------------------------------------------------------------------------------- 1 | .hero 2 | display: flex 3 | min-height: 400px 4 | width: 100% 5 | position: relative 6 | overflow: hidden 7 | background-attachment: fixed !important 8 | background-repeat: no-repeat 9 | background-position: 50% 0 10 | justify-content: center 11 | align-items: center 12 | 13 | &__content 14 | margin-top: 90px 15 | 16 | &__heading 17 | display: inline-block 18 | position: relative 19 | left: 50% 20 | transform: translatex(-50%) 21 | border: 4px solid white 22 | color: white 23 | font-size: 16px 24 | padding: 15px 25 | text-align: center 26 | 27 | @media screen and (min-width: 400px) 28 | .hero 29 | &__heading 30 | font-size: 22px 31 | 32 | @media screen and (min-width: 576px) 33 | .hero 34 | &__heading 35 | padding: 30px 45px 36 | font-size: 32px 37 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_list.sass: -------------------------------------------------------------------------------- 1 | .list 2 | &__underline 3 | 4 | li + li 5 | border-top: 1px solid $semi-white 6 | 7 | li 8 | a 9 | padding: 10px 0 10 | color: $grey-light 11 | display: block 12 | 13 | @include haf 14 | color: $dark 15 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_logo.sass: -------------------------------------------------------------------------------- 1 | .logo 2 | display: block 3 | 4 | svg 5 | height: $nav-height-sticky 6 | width: auto 7 | padding: 10px 8 | transition: $transition-base 9 | 10 | path 11 | fill: white 12 | 13 | @media screen and (min-width: 992px) 14 | .logo 15 | svg 16 | height: $nav-height 17 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_navigation.sass: -------------------------------------------------------------------------------- 1 | .navigation 2 | position: fixed 3 | top: 0 4 | width: 100% 5 | z-index: 100 6 | background-color: rgba($dark, 1) 7 | height: $nav-height-sticky 8 | transition: $transition-base 9 | 10 | .container 11 | display: flex 12 | justify-content: space-between 13 | 14 | &__menu 15 | display: none 16 | 17 | li 18 | a, 19 | span 20 | display: block 21 | text-align: center 22 | text-transform: uppercase 23 | font-family: $heading 24 | padding: 0 20px 25 | line-height: $nav-height-sticky 26 | color: white 27 | font-size: 13px 28 | letter-spacing: 0.02em 29 | font-weight: bold 30 | transition: $transition-base 31 | 32 | @include haf 33 | color: $brown 34 | 35 | span 36 | @include haf 37 | color: white 38 | 39 | &__button 40 | display: block 41 | width: 26px 42 | height: 19px 43 | position: relative 44 | margin: 20px 0 45 | transform: rotate(0deg) 46 | transition: .5s ease-in-out 47 | cursor: pointer 48 | 49 | span 50 | display: block 51 | position: absolute 52 | height: 3px 53 | width: 100% 54 | background: white 55 | border-radius: 1px 56 | opacity: 1 57 | left: 0 58 | transform: rotate(0deg) 59 | transition: $transition-base 60 | 61 | &:nth-child(1) 62 | top: 0 63 | &:nth-child(2), 64 | &:nth-child(3) 65 | top: 8px 66 | 67 | &:nth-child(4) 68 | top: 16px 69 | 70 | &__admin 71 | padding-left: 45px !important 72 | 73 | &:before 74 | @extend .font-awesome 75 | content: '\f007' 76 | padding-right: 7px 77 | 78 | 79 | @media screen and (min-width: 992px) 80 | .navigation 81 | background-color: rgba($dark, .5) 82 | height: $nav-height 83 | 84 | &__menu 85 | display: block 86 | 87 | li 88 | float: left 89 | a, 90 | span 91 | line-height: $nav-height 92 | 93 | &__button 94 | display: none 95 | 96 | @media screen and (max-width: 991px) 97 | .navigation 98 | &__wrapper 99 | position: fixed 100 | top: 0 101 | left: 0 102 | opacity: 0 103 | visibility: hidden 104 | max-height: 1px 105 | transition: $transition-base 106 | width: 100% 107 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_navigation_opened.sass: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 992px) 2 | .opened 3 | overflow: hidden 4 | .navigation 5 | &__button 6 | span 7 | &:nth-child(1), 8 | &:nth-child(4) 9 | top: 18px 10 | width: 0% 11 | left: 50% 12 | 13 | &:nth-child(2) 14 | transform: rotate(45deg) 15 | &:nth-child(3) 16 | transform: rotate(-45deg) 17 | 18 | .navigation 19 | &__wrapper 20 | display: flex 21 | flex-direction: column 22 | justify-content: center 23 | align-items: center 24 | opacity: 1 25 | visibility: visible 26 | max-height: 3000px 27 | background-color: $dark 28 | 29 | &__menu 30 | display: block 31 | position: relative 32 | width: 100% 33 | overflow-y: auto 34 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_pagination.sass: -------------------------------------------------------------------------------- 1 | .pagination 2 | margin: 0 auto 3 | border-radius: 0 4 | display: block 5 | text-align: center 6 | padding: 42px 0 7 | border-top: 1px solid $semi-white 8 | 9 | a, 10 | .current, 11 | .prev, 12 | .next 13 | font-style: normal 14 | color: $grey-light 15 | background-color: white 16 | font-size: 12px 17 | display: inline-block 18 | height: 25px 19 | line-height: 20px 20 | text-align: center 21 | padding: 0 9px 22 | font-family: $heading 23 | font-weight: 700 24 | text-transform: uppercase 25 | 26 | @include haf 27 | color: $dark 28 | 29 | &.current 30 | @include haf 31 | color: $grey-light 32 | 33 | &.current 34 | border-bottom: 2px solid $grey-light 35 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_phone.sass: -------------------------------------------------------------------------------- 1 | .phone 2 | font-size: .68px 3 | 4 | &__screen 5 | margin-left: -2px 6 | margin-top: -2px 7 | 8 | iframe 9 | width: 261px !important 10 | height: 500px !important 11 | 12 | .iphone8 13 | margin: 0 auto 30px 14 | display: block 15 | 16 | @media screen and (min-width: 361px) 17 | .phone 18 | font-size: .7px 19 | &__screen 20 | iframe 21 | width: 267px !important 22 | 23 | @media screen and (min-width: 768px) and (max-width: 1199px) 24 | .phone 25 | &__screen 26 | iframe 27 | width: 472px !important 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_recent_posts.sass: -------------------------------------------------------------------------------- 1 | .recent-posts 2 | overflow: hidden 3 | 4 | &__img 5 | margin-right: 24px 6 | float: left 7 | width: 80px 8 | height: 80px 9 | position: relative 10 | overflow: hidden 11 | 12 | img 13 | max-width: 100% 14 | position: absolute 15 | display: block 16 | top: 0 17 | left: 50% 18 | transform: translatex(-50%) 19 | 20 | &__desc 21 | font-size: 13px 22 | font-family: $heading 23 | text-transform: uppercase 24 | color: $dark 25 | line-height: 1 26 | 27 | a 28 | display: block 29 | margin-bottom: 7px 30 | color: $dark 31 | 32 | span 33 | color: $grey-light 34 | font-size: 13px 35 | font-family: $text 36 | text-transform: none 37 | margin-top: 5px 38 | display: block 39 | 40 | + .recent-posts 41 | margin-top: 30px 42 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_sidebar.sass: -------------------------------------------------------------------------------- 1 | .sidebar 2 | &__item 3 | margin-bottom: 45px 4 | overflow: hidden 5 | 6 | &__title 7 | font-size: 15px 8 | margin-bottom: 20px 9 | 10 | &__form 11 | position: relative 12 | 13 | &__input 14 | border: 2px solid $semi-white 15 | margin-bottom: 0 16 | padding: 0 20px 17 | position: relative 18 | height: 47px 19 | line-height: 47px 20 | background-color: transparent 21 | width: 100% 22 | color: $grey-light 23 | transition: $transition-base 24 | 25 | @include haf 26 | border-color: $brown 27 | 28 | &__search-button 29 | position: absolute 30 | top: 10px 31 | right: 15px 32 | background-color: transparent 33 | border: none 34 | color: $grey-light 35 | cursor: pointer 36 | 37 | @media screen and (min-width: 992px) 38 | .sidebar 39 | padding-left: 30px 40 | 41 | &__item 42 | margin-bottom: 60px 43 | 44 | &__title 45 | margin-bottom: 28px 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_sticky.sass: -------------------------------------------------------------------------------- 1 | .sticky 2 | &.navigation 3 | background-color: rgba($dark, 1) 4 | height: $nav-height-sticky 5 | 6 | li 7 | a, 8 | span 9 | line-height: $nav-height-sticky 10 | 11 | .logo 12 | svg 13 | height: $nav-height-sticky 14 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_tags.sass: -------------------------------------------------------------------------------- 1 | .tags 2 | &__item 3 | background-color: transparent 4 | border: 3px solid $semi-white 5 | font-family: $heading 6 | text-transform: uppercase 7 | padding: 9px 16px 8 | line-height: 1 9 | margin: 0 8px 8px 0 10 | font-size: 10px 11 | color: $grey-light 12 | display: block 13 | float: left 14 | transition: all 0.3s ease-in-out 15 | 16 | @include haf 17 | border-color: $dark 18 | color: black 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_users.sass: -------------------------------------------------------------------------------- 1 | .users 2 | &__item + &__item 3 | padding-top: 30px 4 | padding-bottom: 30px 5 | 6 | &__item 7 | &:nth-child(odd) 8 | background-color: $semi-white-light 9 | 10 | &:nth-child(1) 11 | background-color: transparent 12 | 13 | [class^="col-"] 14 | border-bottom: 1px solid $grey-light 15 | padding-bottom: 15px 16 | 17 | &__label 18 | display: inline-block 19 | 20 | &__line 21 | margin-bottom: 10px 22 | 23 | @media screen and (min-width: 992px) 24 | .users 25 | &__label 26 | display: none 27 | 28 | &__line 29 | margin-bottom: 0 30 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/_wrapper.sass: -------------------------------------------------------------------------------- 1 | .wrapper 2 | padding-top: 15px 3 | padding-bottom: 15px 4 | 5 | &__full-height 6 | height: 100vh 7 | padding: 0 !important 8 | display: flex 9 | flex-direction: column 10 | justify-content: center 11 | align-items: center 12 | 13 | &>* 14 | display: block 15 | position: relative 16 | width: 100% 17 | overflow-y: auto 18 | 19 | @media screen and (min-width: 576px) 20 | .wrapper 21 | padding-top: 30px 22 | padding-bottom: 30px 23 | 24 | @media screen and (min-width: 768px) 25 | .wrapper 26 | padding-top: 60px 27 | padding-bottom: 60px 28 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout/comments.sass: -------------------------------------------------------------------------------- 1 | .comments 2 | background-color: $semi-white-light 3 | 4 | &__heading 5 | text-align: center 6 | 7 | &__item 8 | padding: 30px 0 9 | 10 | .admin__wrapper 11 | margin-bottom: 0 12 | margin-top: 15px 13 | 14 | &__item + &__item 15 | border-top: 1px solid #dbdbdb 16 | 17 | &__img 18 | float: left 19 | position: relative 20 | 21 | img 22 | max-width: 60px 23 | 24 | &__author 25 | display: block 26 | margin-bottom: 5px 27 | font-family: $heading 28 | font-weight: 700 29 | font-size: 15px 30 | color: $dark 31 | 32 | &__created-at 33 | font-size: 13px 34 | color: $grey-light 35 | 36 | &__body 37 | margin-top: 15px 38 | 39 | @media screen and (min-width: 576px) 40 | .comments 41 | &__heading 42 | margin-bottom: 15px 43 | 44 | @media screen and (min-width: 768px) 45 | .comments 46 | &__heading 47 | margin-bottom: 50px 48 | 49 | &__img 50 | img 51 | max-width: 100px 52 | -------------------------------------------------------------------------------- /app/controllers/api/v1/articles_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class ArticlesController < BaseController 4 | respond_to :json 5 | 6 | def index 7 | respond_with Article.all 8 | end 9 | 10 | def show 11 | respond_with Article.find(params[:id]) 12 | end 13 | 14 | def create 15 | article = Article.new(article_params) 16 | if article.save 17 | render json: article, status: :created, location: [:api, :v1, article] 18 | else 19 | render json: { errors: article.errors }, status: :unprocessable_entity 20 | end 21 | end 22 | 23 | def update 24 | article = Article.find(params[:id]) 25 | 26 | if article.update(article_params) 27 | render json: article, status: :ok, location: [:api, :v1, article] 28 | else 29 | render json: { errors: article.errors }, status: :unprocessable_entity 30 | end 31 | end 32 | 33 | def destroy 34 | Article.find(params[:id]).destroy 35 | head :no_content 36 | end 37 | 38 | private 39 | 40 | def article_params 41 | params.require(:article).permit(:title, :text, :category_id) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/api/v1/base_controller.rb: -------------------------------------------------------------------------------- 1 | require 'application_responder' 2 | 3 | module API 4 | module V1 5 | class BaseController < ::ActionController::Base 6 | protect_from_forgery unless: -> { request.format.json? } 7 | 8 | self.responder = ApplicationResponder 9 | 10 | before_action :restrict_access 11 | 12 | private 13 | 14 | def default_serializer_options 15 | { 16 | root: false 17 | } 18 | end 19 | 20 | def restrict_access 21 | authenticate_or_request_with_http_token do |token, _options| 22 | token == ENV['HOWITZER_TOKEN'] 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/api/v1/categories_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class CategoriesController < BaseController 4 | respond_to :json 5 | 6 | def index 7 | if params[:id].present? 8 | respond_with Category.where(id: params[:id]) 9 | else 10 | respond_with Category.all 11 | end 12 | end 13 | 14 | def show 15 | respond_with Category.find(params[:id]) 16 | end 17 | 18 | def create 19 | category = Category.new(category_params) 20 | if category.save 21 | render json: category, status: :created, location: [:api, :v1, category] 22 | else 23 | render json: { errors: category.errors }, status: :unprocessable_entity 24 | end 25 | end 26 | 27 | def update 28 | category = Category.find(params[:id]) 29 | if category.update(category_params) # TODO: Net::SMTPAuthenticationError 30 | render json: category, status: :ok, location: [:api, :v1, category] 31 | else 32 | render json: { errors: category.errors }, status: :unprocessable_entity 33 | end 34 | end 35 | 36 | def destroy 37 | Category.find(params[:id]).destroy 38 | head :no_content 39 | end 40 | 41 | private 42 | 43 | def category_params 44 | params.require(:category).permit(:name) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /app/controllers/api/v1/comments_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class CommentsController < BaseController 4 | respond_to :json 5 | 6 | def index 7 | respond_with article.comments 8 | end 9 | 10 | def show 11 | respond_with comment 12 | end 13 | 14 | def create 15 | comment = article.comments.build(comment_params) 16 | if comment.save 17 | render json: comment, status: :created, location: [:api, :v1, article, comment] 18 | else 19 | render json: { errors: comment.errors }, status: :unprocessable_entity 20 | end 21 | end 22 | 23 | def update 24 | if comment.update(comment_params) 25 | render json: comment, status: :ok, location: [:api, :v1, article, comment] 26 | else 27 | render json: { errors: comment.errors }, status: :unprocessable_entity 28 | end 29 | end 30 | 31 | def destroy 32 | comment.destroy 33 | head :no_content 34 | end 35 | 36 | private 37 | 38 | def comment_params 39 | params.require(:comment).permit(:body, :user_id) 40 | end 41 | 42 | def article 43 | Article.find(params[:article_id]) 44 | end 45 | 46 | def comment 47 | @_comment ||= article.comments.find(params[:id]) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | module V1 3 | class UsersController < BaseController 4 | respond_to :json 5 | 6 | def index 7 | if params[:email].present? 8 | respond_with User.where(email: params[:email]) 9 | else 10 | respond_with User.all 11 | end 12 | end 13 | 14 | def show 15 | respond_with User.find(params[:id]) 16 | end 17 | 18 | def create 19 | user = User.new(user_params) 20 | user.confirm 21 | if user.save 22 | render json: user, status: :created, location: [:api, :v1, user] 23 | else 24 | render json: { errors: user.errors }, status: :unprocessable_entity 25 | end 26 | end 27 | 28 | def update 29 | user = User.find(params[:id]) 30 | if user.update(user_params) # TODO: Net::SMTPAuthenticationError 31 | render json: user, status: :ok, location: [:api, :v1, user] 32 | else 33 | render json: { errors: user.errors }, status: :unprocessable_entity 34 | end 35 | end 36 | 37 | def destroy 38 | User.find(params[:id]).destroy 39 | head :no_content 40 | end 41 | 42 | private 43 | 44 | def user_params 45 | params.require(:user).permit(:email, :name, :password, :password_confirmation, :is_admin) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | before_action :require_login, unless: :devise_controller? 6 | before_action :require_admin, unless: :devise_controller? 7 | 8 | add_breadcrumb 'Home', :root_path 9 | 10 | helper_method :signed_in_as_admin? 11 | 12 | def signed_in_as_admin? 13 | user_signed_in? && current_user.admin? 14 | end 15 | 16 | private 17 | 18 | def require_login 19 | redirect_to root_path unless current_user 20 | end 21 | 22 | def require_admin 23 | redirect_to root_path unless signed_in_as_admin? 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/articles_controller.rb: -------------------------------------------------------------------------------- 1 | class ArticlesController < ApplicationController 2 | before_action :signed_in_as_admin?, except: %i[show index] 3 | layout 'application' 4 | skip_before_action :require_login, only: %i[index show] 5 | skip_before_action :require_admin, only: %i[index show] 6 | 7 | def index # rubocop:disable Metrics/AbcSize 8 | @articles = Article.ordered.page(params[:page]).per(5) 9 | @categories = Category.joins(:articles).group(:id).order('COUNT(articles.id) DESC').limit(10) 10 | @recent_posts = Article.order('created_at DESC').limit(3) 11 | @popular_tags = Tag.joins(:articles).group(:id).order('COUNT(articles.id) DESC').limit(10) 12 | add_breadcrumb 'Articles' 13 | 14 | render layout: 'articles/articles' 15 | end 16 | 17 | def show 18 | @article = Article.find(params[:id]) 19 | add_breadcrumb 'Articles', articles_path 20 | add_breadcrumb @article.title 21 | 22 | render layout: 'articles/article' 23 | end 24 | 25 | def new 26 | @article = Article.new 27 | add_breadcrumb 'Articles', articles_path 28 | add_breadcrumb 'New article' 29 | end 30 | 31 | def edit 32 | @article = Article.find(params[:id]) 33 | add_breadcrumb 'Articles', articles_path 34 | add_breadcrumb "Edit #{@article.title}" 35 | end 36 | 37 | def create 38 | @article = Article.new(article_params) 39 | @article.user = current_user 40 | 41 | if @article.save 42 | flash[:notice] = 'New article has been created.' 43 | redirect_to @article 44 | else 45 | render 'new' 46 | end 47 | end 48 | 49 | def update 50 | @article = Article.find(params[:id]) 51 | if @article.update(article_params) 52 | redirect_to @article 53 | else 54 | render 'edit' 55 | end 56 | end 57 | 58 | def destroy 59 | @article = Article.find(params[:id]) 60 | @article.destroy 61 | 62 | redirect_to articles_path 63 | end 64 | 65 | private 66 | 67 | def article_params 68 | params.require(:article).permit(:title, :text, :image_filename, :category_id, :tag_list) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/controllers/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class CategoriesController < ApplicationController 2 | skip_before_action :require_login, only: %i[index show] 3 | skip_before_action :require_admin, only: %i[index show] 4 | 5 | def index 6 | @categories = Category.all 7 | end 8 | 9 | def show 10 | @category = Category.find(params[:id]) 11 | @articles = @category.articles.ordered.page.per(5) 12 | end 13 | 14 | def new 15 | @category = Category.new 16 | end 17 | 18 | def edit 19 | @category = Category.find(params[:id]) 20 | end 21 | 22 | def create 23 | @category = Category.new(article_params) 24 | if @category.save 25 | flash[:notice] = 'New category has been created.' 26 | redirect_to categories_path 27 | else 28 | render 'new' 29 | end 30 | end 31 | 32 | def update 33 | @category = Category.find(params[:id]) 34 | if @category.update(article_params) 35 | redirect_to categories_path 36 | else 37 | render 'new' 38 | end 39 | end 40 | 41 | def delete 42 | Category.find(params[:id]).destroy 43 | redirect_to categories_path 44 | end 45 | 46 | private 47 | 48 | def article_params 49 | params.require(:category).permit(:name) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | before_action :signed_in_as_admin? 3 | skip_before_action :require_admin 4 | 5 | def create 6 | @article = find_article 7 | @comment = @article.comments.build(comment_params.merge(user: current_user)) 8 | if @article.save 9 | flash[:notice] = 'Comment was successfully added to current article.' 10 | else 11 | flash[:danger] = @comment.errors.full_messages.join 12 | end 13 | redirect_to article_path(@article) 14 | end 15 | 16 | def destroy 17 | @article = Article.find(params[:article_id]) 18 | @comment = @article.comments.find(params[:id]) 19 | @comment.destroy 20 | redirect_to article_path(@article) 21 | end 22 | 23 | private 24 | 25 | def comment_params 26 | params.require(:comment).permit(:body) 27 | end 28 | 29 | def find_article 30 | Article.find(params[:article_id]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/courses_controller.rb: -------------------------------------------------------------------------------- 1 | class CoursesController < ApplicationController 2 | before_action :signed_in_as_admin?, except: %i[index] 3 | layout 'courses' 4 | skip_before_action :require_login, only: %i[index] 5 | skip_before_action :require_admin, only: %i[index] 6 | 7 | def index; end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | skip_before_action :require_login 3 | skip_before_action :require_admin 4 | 5 | def index 6 | @articles = Article.ordered 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class RegistrationsController < Devise::RegistrationsController 2 | before_action :update_sanitized_params, if: :devise_controller? 3 | 4 | def update_sanitized_params 5 | devise_parameter_sanitizer.permit(:sign_up, keys: %i[name email password avatar password_confirmation]) 6 | devise_parameter_sanitizer.permit( 7 | :account_update, keys: %i[name email password password_confirmation avatar current_password] 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/search_controller.rb: -------------------------------------------------------------------------------- 1 | class SearchController < ApplicationController 2 | skip_before_action :require_login 3 | skip_before_action :require_admin 4 | 5 | def index 6 | if search_string.empty? 7 | flash[:danger] = "Search field can't be blank" 8 | redirect_to :articles 9 | else 10 | @articles = Article.ordered.where('title like ?', "%#{search_string}%").page.per(5) 11 | @q = search_string 12 | end 13 | end 14 | 15 | private 16 | 17 | def search_string 18 | params[:q] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/tags_controller.rb: -------------------------------------------------------------------------------- 1 | class TagsController < ApplicationController 2 | skip_before_action :require_login, only: %i[index show] 3 | skip_before_action :require_admin, only: %i[index show] 4 | 5 | def index; end 6 | 7 | def show 8 | @tag = Tag.find(params[:id]) 9 | @articles = @tag.articles.ordered.page.per(5) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_action :authenticate_user! 3 | skip_before_action :require_login, only: %i[index show] 4 | 5 | def index 6 | @users = User.all 7 | add_breadcrumb 'Users' 8 | end 9 | 10 | def show 11 | @user = User.find(params[:id]) 12 | add_breadcrumb 'Users', users_path 13 | add_breadcrumb @user.name 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | APPLICATION_NAME = 'Demo web application'.freeze 3 | 4 | def title(text, with_h3: true) 5 | content_for(:title) { text } 6 | content_tag(:h3, text) if with_h3 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/articles_helper.rb: -------------------------------------------------------------------------------- 1 | module ArticlesHelper 2 | def group_by_created_at(articles) 3 | articles.each_with_object(Hash.new([])) do |el, res| 4 | res[el.created_at.to_date] = res[el.created_at.to_date] + [el] 5 | res 6 | end 7 | end 8 | 9 | def today_or_date(date) 10 | date == Time.zone.today ? 'Today' : date 11 | end 12 | 13 | def show_day(created_at) 14 | created_at.strftime('%d') 15 | end 16 | 17 | def show_month_short(created_at) 18 | created_at.strftime('%b') 19 | end 20 | 21 | def show_date(created_at) 22 | created_at.strftime('%d %B, %Y') 23 | end 24 | 25 | def show_date_time(created_at) 26 | created_at.strftime('%d %B %Y, %H:%m') 27 | end 28 | 29 | def count_comments(comments) 30 | if comments.any? 31 | comments_count = comments.count 32 | comments_count > 1 ? "#{comments_count} Comments" : '1 Comment' 33 | else 34 | 'No comments yet' 35 | end 36 | end 37 | 38 | def fallback_image 39 | 'articles/fallback.jpg' 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/helpers/btn_helper.rb: -------------------------------------------------------------------------------- 1 | module BtnHelper 2 | def btn_create(anchor, link) 3 | link_to anchor, link, class: 'btn btn-success btn__icon btn__create' 4 | end 5 | 6 | def btn_edit(anchor, link) 7 | link_to anchor, link, class: 'btn btn-success btn__icon btn__edit' 8 | end 9 | 10 | def btn_delete(anchor, link, confirm = 'Are you sure?') 11 | link_to anchor, link, 12 | method: :delete, data: { confirm: }, 13 | class: 'btn btn-danger btn__icon btn__delete' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/comments_helper.rb: -------------------------------------------------------------------------------- 1 | module CommentsHelper 2 | def comment_created_date(comment) 3 | comment.created_at.strftime('%B %d, %Y at %I:%M %P') if comment.created_at.present? 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/helpers/devise_helper.rb: -------------------------------------------------------------------------------- 1 | module DeviseHelper 2 | def devise_error_messages! # rubocop:disable Metrics/MethodLength 3 | return '' if resource.errors.empty? 4 | 5 | messages = resource.errors.full_messages.map { |msg| content_tag(:li, msg) }.join 6 | html = <<-HTML 7 | 16 | HTML 17 | 18 | html.html_safe 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/helpers/svg_helper.rb: -------------------------------------------------------------------------------- 1 | module SvgHelper 2 | def svg(name) 3 | file_path = Rails.root.join('app', 'assets', 'images', "#{name}.svg") 4 | return File.read(file_path).html_safe if File.exist?(file_path) 5 | 6 | '(not found)' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/user_helper.rb: -------------------------------------------------------------------------------- 1 | module UserHelper 2 | def default_avatar 3 | 'users/defaut_avatar.png' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ApplicationRecord 2 | belongs_to :category 3 | belongs_to :user, optional: true 4 | 5 | has_many :articles_tags, dependent: :destroy 6 | has_many :tags, through: :articles_tags 7 | 8 | has_many :comments, dependent: :destroy 9 | validates :title, presence: true, length: { minimum: 5, allow_blank: true } 10 | 11 | scope :ordered, -> { order('created_at DESC') } 12 | mount_uploader :image_filename, ArticleImageUploader 13 | 14 | def self.tagged_with(name) 15 | Tag.find_by!(name:).articles 16 | end 17 | 18 | def tag_list 19 | tags.map(&:name).join(', ') 20 | end 21 | 22 | def tag_list=(names) 23 | new_tags = names.split(', ') - tag_list.split(', ') 24 | tags << new_tags.map do |n| 25 | Tag.where(name: n.strip).first_or_create! 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/models/articles_tag.rb: -------------------------------------------------------------------------------- 1 | class ArticlesTag < ApplicationRecord 2 | belongs_to :article 3 | belongs_to :tag 4 | end 5 | -------------------------------------------------------------------------------- /app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ApplicationRecord 2 | has_many :articles, dependent: :destroy 3 | 4 | validates :name, presence: true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ApplicationRecord 2 | belongs_to :article 3 | belongs_to :user 4 | validates :body, presence: true 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/strongqa/demo_web_app/5afa46b222403f336401737cee00a2d1a003fd25/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ApplicationRecord 2 | has_many :articles_tags, dependent: :destroy 3 | has_many :articles, through: :articles_tags 4 | end 5 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | mount_uploader :avatar, AvatarUploader 3 | # Include default devise modules. Others available are: 4 | # :confirmable, :lockable, :timeoutable and :omniauthable 5 | devise :database_authenticatable, :registerable, :confirmable, 6 | :recoverable, :rememberable, :trackable, :validatable 7 | 8 | def admin? 9 | is_admin? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/serializers/article_serializer.rb: -------------------------------------------------------------------------------- 1 | class ArticleSerializer < ActiveModel::Serializer 2 | attributes :id, :title, :text, :created_at, :updated_at 3 | end 4 | -------------------------------------------------------------------------------- /app/serializers/comment_serializer.rb: -------------------------------------------------------------------------------- 1 | class CommentSerializer < ActiveModel::Serializer 2 | attributes :id, :body, :user_id, :article_id, :created_at, :updated_at 3 | end 4 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ActiveModel::Serializer 2 | attributes :id, :email, :name, :is_admin, :created_at, :updated_at 3 | end 4 | -------------------------------------------------------------------------------- /app/services/create_admin_service.rb: -------------------------------------------------------------------------------- 1 | class CreateAdminService 2 | def call 3 | User.find_or_create_by!(email: ENV.fetch('ADMIN_EMAIL')) do |user| 4 | user.password = ENV.fetch('ADMIN_PASSWORD') 5 | user.password_confirmation = ENV.fetch('ADMIN_PASSWORD', nil) 6 | user.name = 'Admin User' 7 | user.is_admin = true 8 | user.confirm 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/create_category_service.rb: -------------------------------------------------------------------------------- 1 | class CreateCategoryService 2 | def call 3 | Category.find_or_create_by!(name: FFaker::Product.brand) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/services/create_demo_article_service.rb: -------------------------------------------------------------------------------- 1 | class CreateDemoArticleService 2 | def call 3 | article = create_article 4 | [*(0..5)].sample.times do 5 | user = User.offset(rand(User.count)).first 6 | article.comments << Comment.create!(article:, body: FFaker::Lorem.sentence, user:) 7 | end 8 | end 9 | 10 | private 11 | 12 | def create_article 13 | article = Article.create!(article_params) 14 | article.update(created_at: [*(0..5)].sample.days.ago + [*(0..64_000)].sample) 15 | article 16 | end 17 | 18 | def article_params 19 | { 20 | title: FFaker::HipsterIpsum.sentence, 21 | text: FFaker::HipsterIpsum.paragraph, 22 | category: Category.all.sample, 23 | tags: Tag.all.sample(2), 24 | user: User.all.sample 25 | } 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/services/create_demo_user_service.rb: -------------------------------------------------------------------------------- 1 | class CreateDemoUserService 2 | def call 3 | User.find_or_create_by!(email: FFaker::Internet.free_email, name: FFaker::Internet.user_name) do |user| 4 | user.password = 'Password1234' 5 | user.password_confirmation = 'Password1234' 6 | user.confirm 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/services/create_tag_service.rb: -------------------------------------------------------------------------------- 1 | class CreateTagService 2 | def call 3 | Tag.find_or_create_by!(name: FFaker::Food.fruit) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/uploaders/article_image_uploader.rb: -------------------------------------------------------------------------------- 1 | class ArticleImageUploader < CarrierWave::Uploader::Base 2 | def store_dir 3 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/uploaders/avatar_uploader.rb: -------------------------------------------------------------------------------- 1 | class AvatarUploader < CarrierWave::Uploader::Base 2 | def store_dir 3 | "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/views/articles/_article_date.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= show_day(created_at) %> 3 | <%= show_month_short(created_at) %> 4 |
5 | -------------------------------------------------------------------------------- /app/views/articles/_article_meta.html.erb: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/views/articles/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form_for @article do |f| %> 3 | <% if @article.errors.any? %> 4 |
5 |

<%= pluralize(@article.errors.count, "error") %> prohibited 6 | this article from being saved:

7 | 12 |
13 | <% end %> 14 | 15 | <%= f.label :title %> 16 | <%= f.text_field :title, class: 'form-control' %> 17 | 18 | <%= f.label :text %> 19 | <%= f.text_area :text, class: 'form-control', cols: '30', rows: '10' %> 20 | 21 | <%= f.label :category %> 22 | <%= f.collection_select :category_id, Category.all, :id, :name, class: 'form-control' %> 23 | 24 | <%= f.label 'tags (separated by comas)' %> 25 | <%= f.text_field :tag_list, class: 'form-control' %> 26 | 27 | <%= f.label :image %> 28 | <%= f.file_field :image_filename, class: 'form-control' %> 29 | 30 | <%= f.submit(class: 'btn btn__form') %> 31 | <% end %> 32 |
33 | -------------------------------------------------------------------------------- /app/views/articles/_social.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Share:
3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /app/views/articles/_tags.html.erb: -------------------------------------------------------------------------------- 1 |
Tags:
2 | <% @article.tags.each do |tag|%> 3 | <%= link_to tag.name, tag_path(tag) %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/articles/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title 'Edit Article' %> 2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/articles/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title 'Listing Articles' %> 2 | <% if signed_in_as_admin? %> 3 |
4 | <%= btn_create 'New article', new_article_path %> 5 |
6 | <% end %> 7 | 8 | <% @articles.each do |article| %> 9 | 10 |
11 | 12 | <%= link_to article, class: 'article__img' do %> 13 | <%=image_tag( article.image_filename.present? ? article.image_filename : fallback_image )%> 14 | <% end %> 15 | 16 |
17 | <%= render 'article_date', created_at: article.created_at %> 18 |
19 |

<%= link_to article.title, article, class: 'article__title' %>

20 | <%= render 'article_meta', created_at: article.created_at, comments: article.comments, user: article.user, category: article.category %> 21 |
22 |
23 | 24 |

25 | <%= article.text %> 26 |

27 | 28 | <%= link_to 'Read more', article, class: 'article__readmore btn__underline' %> 29 | 30 | <% if signed_in_as_admin? %> 31 |
32 | <%= btn_edit 'Edit', edit_article_path(article) %> 33 | <%= btn_delete 'Delete', article_path(article) %> 34 |
35 | <% end %> 36 | 37 |
38 | <% end %> 39 | <%= paginate @articles %> 40 | -------------------------------------------------------------------------------- /app/views/articles/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title 'New Article' %> 2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/articles/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @article.title %> 2 | 3 |
4 |
5 | <%=image_tag( @article.image_filename.present? ? @article.image_filename : fallback_image )%> 6 |
7 | 8 |
9 |

10 | <%= @article.title %> 11 |

12 | <%= render 'article_meta', created_at: @article.created_at, comments: @article.comments, user: @article.user, category: @article.category %> 13 |
14 |
15 | 16 |
17 |
18 |

<%= @article.text %>

19 |
20 |
21 |
22 |
23 | <%= render 'tags' %> 24 |
25 | <% if ENV['SOCIAL_ENABLED'] == true %> 26 |
27 | <%= render 'social' %> 28 |
29 | <% end %> 30 |
31 |
32 | 33 | <% if signed_in_as_admin? %> 34 |
35 | <%= btn_edit 'Edit Article', edit_article_path(@article) %> 36 |
37 | <% end %> 38 | 39 |
40 | -------------------------------------------------------------------------------- /app/views/categories/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form_for @category do |f| %> 3 | <% if @category.errors.any? %> 4 |
5 |

<%= pluralize(@category.errors.count, "error") %> prohibited 6 | this article from being saved:

7 | 12 |
13 | <% end %> 14 | 15 | <%= f.label :name %> 16 | <%= f.text_field :name, class: 'form-control' %> 17 | 18 | <%= f.submit(class: 'btn btn__form') %> 19 | <% end %> 20 |
21 | -------------------------------------------------------------------------------- /app/views/categories/_list.html.erb: -------------------------------------------------------------------------------- 1 | <% @articles.each do |article| %> 2 | 3 |
4 | 5 | <%= link_to article, class: 'article__img' do %> 6 | <%=image_tag( article.image_filename.present? ? article.image_filename : fallback_image )%> 7 | <% end %> 8 | 9 |
10 | <%= render 'articles/article_date', created_at: article.created_at %> 11 |
12 |

<%= link_to article.title, article, class: 'article__title' %>

13 | <%= render 'articles/article_meta', created_at: article.created_at, comments: article.comments, user: article.user, category: article.category %> 14 |
15 |
16 | 17 |

18 | <%= article.text %> 19 |

20 | 21 | <%= link_to 'Read more', article, class: 'article__readmore btn__underline' %> 22 | 23 |
24 | 25 | <% if signed_in_as_admin? %> 26 |
27 | <%= btn_edit 'Edit', edit_article_path(article) %> 28 | <%= btn_delete 'Delete', article_path(article) %> 29 |
30 | <% end %> 31 | 32 | <% end %> 33 | <%= paginate @articles %> 34 | -------------------------------------------------------------------------------- /app/views/categories/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title 'Edit Category' %> 2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/categories/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% title 'Categories' %> 3 |
4 | 5 | <% if signed_in_as_admin?%> 6 |
7 | <%= btn_create 'New Category', new_category_path %> 8 |
9 | <% end %> 10 | 11 |
12 |
13 |
#
14 |
Category
15 |
Available action
16 |
17 | 18 | <% @categories.each do |category| %> 19 |
20 |
21 | <%= category.id %> 22 |
23 |
24 |
User name:
25 | <%=link_to category.name, category_path(category) %> 26 |
27 |
28 | <% if signed_in_as_admin?%> 29 | <% if category %> 30 | <%= btn_edit 'edit', edit_category_path(category) %> 31 | <%= btn_delete 'delete', delete_category_path(category) %> 32 | <% end %> 33 | <% end %> 34 |
35 |
36 | <% end %> 37 |
38 | -------------------------------------------------------------------------------- /app/views/categories/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title 'New Category' %> 2 | 3 | <%= render 'form' %> 4 | -------------------------------------------------------------------------------- /app/views/categories/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Category: #{@category.name}" %> 2 | 3 | <% if @articles.empty? %> 4 | 5 |

There is no related Article

6 | 7 | <% else %> 8 | <%= render 'list' %> 9 | <% end %> 10 | -------------------------------------------------------------------------------- /app/views/comments/_comments.html.erb: -------------------------------------------------------------------------------- 1 | <% if @article.comments.present? %> 2 |
3 |
4 |
5 |
6 |

Comments

7 | <%= render 'comments/list' %> 8 |
9 |
10 |
11 |
12 | <% end %> 13 | <% if signed_in? %> 14 |
15 |
16 |
17 |

Leave a comment

18 | <%= render "comments/form" %> 19 |
20 |
21 |
22 | <% end %> 23 | -------------------------------------------------------------------------------- /app/views/comments/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for([@article, @article.comments.build]) do |f| %> 2 | <%= f.text_area :body, class: 'form__textarea' %> 3 | <%= f.submit('Create comment', class: 'btn btn__form') %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /app/views/comments/_list.html.erb: -------------------------------------------------------------------------------- 1 | <% @article.comments.each do |comment| %> 2 |
3 |
4 |
5 |
6 | <%=image_tag( comment.user.avatar.present? ? comment.user.avatar : default_avatar )%> 7 |
8 |
9 |
10 |
<%= comment.user.name %>
11 |
12 | <%= comment_created_date(comment) %> 13 |
14 |

<%= comment.body %>

15 |
16 |
17 | <% if signed_in_as_admin? %> 18 |
19 | <%= btn_delete 'Destroy Comment', [comment.article, comment] %> 20 |
21 | <% end %> 22 |
23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/devise/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= title "Forgot your password?", with_h3: false %> 2 |
3 | <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :role => 'form'}) do |f| %> 4 |

We'll send password reset instructions.

5 | <%= devise_error_messages! %> 6 |
7 | <%= f.email_field :email, :autofocus => true, class: 'form-control', placeholder: :email %> 8 |
9 | <%= f.submit 'Reset Password', :class => 'btn btn__form btn__big' %> 10 | <% end %> 11 |
12 | -------------------------------------------------------------------------------- /app/views/devise/registrations/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= title "Edit #{resource_name.to_s.humanize}", with_h3: false %> 2 |
3 | <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, :role => 'form'}) do |f| %> 4 | <%= devise_error_messages! %> 5 |
6 | <%= f.text_field :name, :autofocus => true, class: 'form-control', placeholder: :name %> 7 |
8 |
9 | <%= f.email_field :email, class: 'form-control', placeholder: :email %> 10 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> 11 |
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
12 | <% end %> 13 |
14 | 15 |
16 |

Leave these fields blank if you don't want to change your password.

17 |
18 | <%= f.password_field :password, :autocomplete => 'off', class: 'form-control', placeholder: :password %> 19 |
20 |
21 | <%= f.password_field :password_confirmation, class: 'form-control', placeholder: :password_confirmation %> 22 |
23 |
24 | 25 |
26 | <%= f.label :avatar %> 27 | <%= f.file_field :avatar, class: 'form-control' %> 28 |
29 | 30 |
31 |

You must enter your current password to make changes.

32 |
33 | <%= f.password_field :current_password, class: 'form-control', placeholder: :current_password %> 34 |
35 |
36 | 37 | <%= f.submit 'Update', :class => 'btn btn__form btn__big' %> 38 | <% end %> 39 | 40 |

Cancel Account

41 |

Unhappy? We'll be sad to see you go.

42 | <%= button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete, :class => 'btn btn__form btn__big' %> 43 |
44 | -------------------------------------------------------------------------------- /app/views/devise/registrations/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= title 'Sign Up', with_h3: false %> 2 |
3 | <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :role => 'form'}) do |f| %> 4 | <%= devise_error_messages! %> 5 |
6 | <%= f.text_field :name, :autofocus => true, class: 'form-control', placeholder: :name %> 7 |
8 |
9 | <%= f.email_field :email, class: 'form-control', placeholder: :email %> 10 |
11 |
12 | <%= f.password_field :password, class: 'form-control', placeholder: :password %> 13 |
14 |
15 | <%= f.password_field :password_confirmation, class: 'form-control', placeholder: :password_confirmation %> 16 |
17 | <%= f.submit 'Sign up', :class => 'btn btn__form btn__big' %> 18 | <% end %> 19 |
20 | Already have the account? 21 | <%= link_to 'Sign in', new_user_session_path %> 22 |
23 |
24 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= title 'Login form', with_h3: false %> 2 |
3 | <%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :role => 'form'}) do |f| %> 4 | <%= devise_error_messages! %> 5 |
6 | <%= f.email_field :email, :autofocus => true, class: 'form-control', placeholder: :email %> 7 |
8 |
9 | <%= f.password_field :password, class: 'form-control', placeholder: :password %> 10 |
11 | <%= f.submit 'Log in', :class => 'btn btn__form btn__big' %> 12 | <% if devise_mapping.rememberable? -%> 13 |
14 | <%= f.check_box :remember_me %> 15 | <%= f.label :remember_me %> 16 |
17 | <% end -%> 18 | <%- if devise_mapping.recoverable? %> 19 | <%= link_to "Forgot password?", new_password_path(resource_name), class: 'authform__forgot-password' %> 20 | <% end -%> 21 | <%- if devise_mapping.registerable? %> 22 |
23 | Don't have an account yet? 24 | <%= link_to 'Sign up', new_registration_path(resource_name), id: 'new_user_sign_up' %> 25 |
26 | <% end -%> 27 | <% end %> 28 |
29 | -------------------------------------------------------------------------------- /app/views/home/_banner.html.erb: -------------------------------------------------------------------------------- 1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 | <%= javascript_tag do %> 21 | $( window ).ready(function() { 22 | setTimeout(function(){ 23 | $('#banner').html( 24 | $('